mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 00:11:00 +00:00
Lint
This commit is contained in:
parent
2a3da72239
commit
a72f290038
38 changed files with 209 additions and 207 deletions
|
@ -101,12 +101,12 @@ struct SideBarView<Content: View>: View {
|
||||||
} label: {
|
} label: {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
if userPreferences.isSidebarExpanded {
|
if userPreferences.isSidebarExpanded {
|
||||||
AppAccountView(viewModel: .init(appAccount: account,
|
AppAccountView(viewModel: .init(appAccount: account,
|
||||||
isCompact: false,
|
isCompact: false,
|
||||||
isInSettings: false),
|
isInSettings: false),
|
||||||
isParentPresented: .constant(false))
|
isParentPresented: .constant(false))
|
||||||
} else {
|
} else {
|
||||||
AppAccountView(viewModel: .init(appAccount: account,
|
AppAccountView(viewModel: .init(appAccount: account,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
isInSettings: false),
|
isInSettings: false),
|
||||||
isParentPresented: .constant(false))
|
isParentPresented: .constant(false))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AppAccount
|
import AppAccount
|
||||||
|
import AuthenticationServices
|
||||||
import Combine
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
@ -7,7 +8,6 @@ import Network
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AuthenticationServices
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct AddAccountView: View {
|
struct AddAccountView: View {
|
||||||
|
|
|
@ -31,9 +31,9 @@ struct SettingsTabs: View {
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
let isModal: Bool
|
let isModal: Bool
|
||||||
|
|
||||||
@State private var startingPoint: SettingsStartingPoint? = nil
|
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
Form {
|
Form {
|
||||||
|
@ -72,24 +72,24 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.navigationDestination(item: $startingPoint) { targetView in
|
.navigationDestination(item: $startingPoint) { targetView in
|
||||||
switch targetView {
|
switch targetView {
|
||||||
case .display:
|
case .display:
|
||||||
DisplaySettingsView()
|
DisplaySettingsView()
|
||||||
case .haptic:
|
case .haptic:
|
||||||
HapticSettingsView()
|
HapticSettingsView()
|
||||||
case .remoteTimelines:
|
case .remoteTimelines:
|
||||||
RemoteTimelinesSettingView()
|
RemoteTimelinesSettingView()
|
||||||
case .tagGroups:
|
case .tagGroups:
|
||||||
TagsGroupSettingView()
|
TagsGroupSettingView()
|
||||||
case .recentTags:
|
case .recentTags:
|
||||||
RecenTagsSettingView()
|
RecenTagsSettingView()
|
||||||
case .content:
|
case .content:
|
||||||
ContentSettingsView()
|
ContentSettingsView()
|
||||||
case .swipeActions:
|
case .swipeActions:
|
||||||
SwipeActionsSettingsView()
|
SwipeActionsSettingsView()
|
||||||
case .tabAndSidebarEntries:
|
case .tabAndSidebarEntries:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
case .translation:
|
case .translation:
|
||||||
TranslationSettingsView()
|
TranslationSettingsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,24 +22,24 @@ public struct ListEntity: Identifiable, AppEntity {
|
||||||
|
|
||||||
public struct DefaultListEntityQuery: EntityQuery {
|
public struct DefaultListEntityQuery: EntityQuery {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
@IntentParameterDependency<ListsWidgetConfiguration>(
|
@IntentParameterDependency<ListsWidgetConfiguration>(
|
||||||
\.$account
|
\.$account
|
||||||
)
|
)
|
||||||
var account
|
var account
|
||||||
|
|
||||||
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
|
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
|
||||||
await fetchLists().map{ .init(list: $0 )}
|
await fetchLists().map { .init(list: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func suggestedEntities() async throws -> [ListEntity] {
|
public func suggestedEntities() async throws -> [ListEntity] {
|
||||||
await fetchLists().map{ .init(list: $0 )}
|
await fetchLists().map { .init(list: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func defaultResult() async -> ListEntity? {
|
public func defaultResult() async -> ListEntity? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchLists() async -> [Models.List] {
|
private func fetchLists() async -> [Models.List] {
|
||||||
guard let account = account?.account.account else {
|
guard let account = account?.account.account else {
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -10,18 +10,18 @@ struct AccountWidgetProvider: AppIntentTimelineProvider {
|
||||||
.init(date: Date(), account: .placeholder(), avatar: nil)
|
.init(date: Date(), account: .placeholder(), avatar: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(for configuration: AccountWidgetConfiguration, in context: Context) async -> AccountWidgetEntry {
|
func snapshot(for configuration: AccountWidgetConfiguration, in _: Context) async -> AccountWidgetEntry {
|
||||||
let account = await fetchAccount(configuration: configuration)
|
let account = await fetchAccount(configuration: configuration)
|
||||||
return .init(date: Date(), account: account, avatar: nil)
|
return .init(date: Date(), account: account, avatar: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeline(for configuration: AccountWidgetConfiguration, in context: Context) async -> Timeline<AccountWidgetEntry> {
|
func timeline(for configuration: AccountWidgetConfiguration, in _: Context) async -> Timeline<AccountWidgetEntry> {
|
||||||
let account = await fetchAccount(configuration: configuration)
|
let account = await fetchAccount(configuration: configuration)
|
||||||
let images = try? await loadImages(urls: [account.avatar])
|
let images = try? await loadImages(urls: [account.avatar])
|
||||||
return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)],
|
return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)],
|
||||||
policy: .atEnd)
|
policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account {
|
private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account {
|
||||||
let client = Client(server: configuration.account.account.server,
|
let client = Client(server: configuration.account.account.server,
|
||||||
oauthToken: configuration.account.account.oauthToken)
|
oauthToken: configuration.account.account.oauthToken)
|
||||||
|
|
|
@ -11,7 +11,6 @@ struct AccountWidgetView: View {
|
||||||
@Environment(\.widgetFamily) var family
|
@Environment(\.widgetFamily) var family
|
||||||
@Environment(\.redactionReasons) var redacted
|
@Environment(\.redactionReasons) var redacted
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center, spacing: 4) {
|
VStack(alignment: .center, spacing: 4) {
|
||||||
if let avatar = entry.avatar {
|
if let avatar = entry.avatar {
|
||||||
|
|
|
@ -138,13 +138,13 @@ public struct AccountDetailContextMenu: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
if #available(iOS 17.4, *) {
|
if #available(iOS 17.4, *) {
|
||||||
Button {
|
Button {
|
||||||
showTranslateView = true
|
showTranslateView = true
|
||||||
} label: {
|
} label: {
|
||||||
Label("status.action.translate", systemImage: "captions.bubble")
|
Label("status.action.translate", systemImage: "captions.bubble")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if viewModel.relationship?.following == true {
|
if viewModel.relationship?.following == true {
|
||||||
|
|
|
@ -85,14 +85,14 @@ public struct AccountDetailView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
routerPath.navigate(to: .accountMediaGridView(account: account,
|
routerPath.navigate(to: .accountMediaGridView(account: account,
|
||||||
initialMediaStatuses: viewModel.statusesMedias))
|
initialMediaStatuses: viewModel.statusesMedias))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
StatusesListView(fetcher: viewModel,
|
StatusesListView(fetcher: viewModel,
|
||||||
|
@ -302,7 +302,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
||||||
showTranslateView: $showTranslateView,
|
showTranslateView: $showTranslateView,
|
||||||
viewModel: viewModel)
|
viewModel: viewModel)
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ import SwiftUI
|
||||||
|
|
||||||
private(set) var statuses: [Status] = []
|
private(set) var statuses: [Status] = []
|
||||||
var statusesMedias: [MediaStatus] {
|
var statusesMedias: [MediaStatus] {
|
||||||
statuses.filter{ !$0.mediaAttachments.isEmpty }.flatMap{ $0.asMediaStatus}
|
statuses.filter { !$0.mediaAttachments.isEmpty }.flatMap { $0.asMediaStatus }
|
||||||
}
|
}
|
||||||
|
|
||||||
var boosts: [Status] = []
|
var boosts: [Status] = []
|
||||||
|
|
|
@ -113,7 +113,7 @@ public struct AccountsListRow: View {
|
||||||
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
|
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
|
||||||
#endif
|
#endif
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
|
||||||
showTranslateView: $showTranslateView,
|
showTranslateView: $showTranslateView,
|
||||||
viewModel: .init(account: viewModel.account))
|
viewModel: .init(account: viewModel.account))
|
||||||
} preview: {
|
} preview: {
|
||||||
|
|
|
@ -128,7 +128,7 @@ public struct AccountsListView: View {
|
||||||
PlaceholderView(iconName: "person.icloud",
|
PlaceholderView(iconName: "person.icloud",
|
||||||
title: "No accounts found",
|
title: "No accounts found",
|
||||||
message: "This list of accounts is empty")
|
message: "This list of accounts is empty")
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
} else {
|
} else {
|
||||||
ForEach(accounts) { account in
|
ForEach(accounts) { account in
|
||||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||||
|
@ -139,7 +139,7 @@ public struct AccountsListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
|
|
|
@ -99,21 +99,21 @@ public struct EditAccountView: View {
|
||||||
viewModel.isPhotoPickerPresented = true
|
viewModel.isPhotoPickerPresented = true
|
||||||
}
|
}
|
||||||
if viewModel.avatar != nil || viewModel.header != nil {
|
if viewModel.avatar != nil || viewModel.header != nil {
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
if viewModel.avatar != nil {
|
if viewModel.avatar != nil {
|
||||||
Button("account.edit.avatar.delete", role: .destructive) {
|
Button("account.edit.avatar.delete", role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.deleteAvatar()
|
await viewModel.deleteAvatar()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if viewModel.header != nil {
|
if viewModel.header != nil {
|
||||||
Button("account.edit.header.delete", role: .destructive) {
|
Button("account.edit.header.delete", role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.deleteHeader()
|
await viewModel.deleteHeader()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "photo.badge.plus")
|
Image(systemName: "photo.badge.plus")
|
||||||
|
|
|
@ -51,19 +51,19 @@ import SwiftUI
|
||||||
didSet {
|
didSet {
|
||||||
if let item = mediaPickers.first {
|
if let item = mediaPickers.first {
|
||||||
Task {
|
Task {
|
||||||
if isChangingAvatar {
|
if isChangingAvatar {
|
||||||
if let data = await getItemImageData(item: item, for: .avatar) {
|
if let data = await getItemImageData(item: item, for: .avatar) {
|
||||||
_ = await uploadAvatar(data: data)
|
_ = await uploadAvatar(data: data)
|
||||||
}
|
|
||||||
isChangingAvatar = false
|
|
||||||
} else if isChangingHeader {
|
|
||||||
if let data = await getItemImageData(item: item, for: .header) {
|
|
||||||
_ = await uploadHeader(data: data)
|
|
||||||
}
|
|
||||||
isChangingHeader = false
|
|
||||||
}
|
}
|
||||||
await fetchAccount()
|
isChangingAvatar = false
|
||||||
mediaPickers = []
|
} else if isChangingHeader {
|
||||||
|
if let data = await getItemImageData(item: item, for: .header) {
|
||||||
|
_ = await uploadHeader(data: data)
|
||||||
|
}
|
||||||
|
isChangingHeader = false
|
||||||
|
}
|
||||||
|
await fetchAccount()
|
||||||
|
mediaPickers = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,26 +186,26 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EditAccountViewModel {
|
extension EditAccountViewModel {
|
||||||
private enum ItemType {
|
private enum ItemType {
|
||||||
case avatar
|
case avatar
|
||||||
case header
|
case header
|
||||||
|
|
||||||
var maxHeight: CGFloat {
|
var maxHeight: CGFloat {
|
||||||
switch self {
|
switch self {
|
||||||
case .avatar:
|
case .avatar:
|
||||||
400
|
400
|
||||||
case .header:
|
case .header:
|
||||||
500
|
500
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var maxWidth: CGFloat {
|
|
||||||
switch self {
|
|
||||||
case .avatar:
|
|
||||||
400
|
|
||||||
case .header:
|
|
||||||
1500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maxWidth: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .avatar:
|
||||||
|
400
|
||||||
|
case .header:
|
||||||
|
1500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import SwiftUI
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import NukeUI
|
|
||||||
import Env
|
import Env
|
||||||
import MediaUI
|
import MediaUI
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import NukeUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct AccountDetailMediaGridView: View {
|
public struct AccountDetailMediaGridView: View {
|
||||||
|
@ -12,21 +12,22 @@ public struct AccountDetailMediaGridView: View {
|
||||||
@Environment(RouterPath.self) private var routerPath
|
@Environment(RouterPath.self) private var routerPath
|
||||||
@Environment(Client.self) private var client
|
@Environment(Client.self) private var client
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
|
|
||||||
let account: Account
|
let account: Account
|
||||||
@State var mediaStatuses: [MediaStatus]
|
@State var mediaStatuses: [MediaStatus]
|
||||||
|
|
||||||
public init(account: Account, initialMediaStatuses: [MediaStatus]) {
|
public init(account: Account, initialMediaStatuses: [MediaStatus]) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.mediaStatuses = initialMediaStatuses
|
mediaStatuses = initialMediaStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4),
|
LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4),
|
||||||
.init(.flexible(minimum: 100), spacing: 4),
|
.init(.flexible(minimum: 100), spacing: 4),
|
||||||
.init(.flexible(minimum: 100), spacing: 4)],
|
.init(.flexible(minimum: 100), spacing: 4)],
|
||||||
spacing: 4) {
|
spacing: 4)
|
||||||
|
{
|
||||||
ForEach(mediaStatuses) { status in
|
ForEach(mediaStatuses) { status in
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
if let url = status.attachment.url {
|
if let url = status.attachment.url {
|
||||||
|
@ -35,7 +36,7 @@ public struct AccountDetailMediaGridView: View {
|
||||||
case .image:
|
case .image:
|
||||||
LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in
|
LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in
|
||||||
if let image = state.image {
|
if let image = state.image {
|
||||||
image
|
image
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: proxy.size.width, height: proxy.size.width)
|
.frame(width: proxy.size.width, height: proxy.size.width)
|
||||||
|
@ -84,7 +85,7 @@ public struct AccountDetailMediaGridView: View {
|
||||||
.clipped()
|
.clipped()
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
NextPageView {
|
NextPageView {
|
||||||
|
@ -96,21 +97,21 @@ public struct AccountDetailMediaGridView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle(account.displayName ?? "")
|
.navigationTitle(account.displayName ?? "")
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchNextPage() async throws {
|
private func fetchNextPage() async throws {
|
||||||
let client = client
|
let client = client
|
||||||
let newStatuses: [Status] =
|
let newStatuses: [Status] =
|
||||||
try await client.get(endpoint: Accounts.statuses(id: account.id,
|
try await client.get(endpoint: Accounts.statuses(id: account.id,
|
||||||
sinceId: mediaStatuses.last?.id,
|
sinceId: mediaStatuses.last?.id,
|
||||||
tag: nil,
|
tag: nil,
|
||||||
onlyMedia: true,
|
onlyMedia: true,
|
||||||
excludeReplies: true,
|
excludeReplies: true,
|
||||||
excludeReblogs: true,
|
excludeReblogs: true,
|
||||||
pinned: nil))
|
pinned: nil))
|
||||||
mediaStatuses.append(contentsOf: newStatuses.flatMap{ $0.asMediaStatus })
|
mediaStatuses.append(contentsOf: newStatuses.flatMap { $0.asMediaStatus })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,7 +284,7 @@ public final class Theme {
|
||||||
themeStorage.chosenFontData = chosenFontData
|
themeStorage.chosenFontData = chosenFontData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var showContentGradient: Bool {
|
public var showContentGradient: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
themeStorage.showContentGradient = showContentGradient
|
themeStorage.showContentGradient = showContentGradient
|
||||||
|
|
|
@ -38,7 +38,7 @@ import Observation
|
||||||
public var isNotificationsFilterSupported: Bool {
|
public var isNotificationsFilterSupported: Bool {
|
||||||
version >= 4.3
|
version >= 4.3
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isLinkTimelineSupported: Bool {
|
public var isLinkTimelineSupported: Bool {
|
||||||
version >= 4.3
|
version >= 4.3
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import SwiftUI
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
import Translation
|
import Translation
|
||||||
|
|
||||||
extension View {
|
public extension View {
|
||||||
public func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
|
func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
|
||||||
if #available(iOS 17.4, *) {
|
if #available(iOS 17.4, *) {
|
||||||
return self.translationPresentation(isPresented: isPresented, text: text)
|
return self.translationPresentation(isPresented: isPresented, text: text)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -137,7 +137,7 @@ public enum SettingsStartingPoint {
|
||||||
public var path: [RouterDestination] = []
|
public var path: [RouterDestination] = []
|
||||||
public var presentedSheet: SheetDestination?
|
public var presentedSheet: SheetDestination?
|
||||||
|
|
||||||
public static var settingsStartingPoint: SettingsStartingPoint? = nil
|
public static var settingsStartingPoint: SettingsStartingPoint?
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
|
|
|
@ -354,7 +354,7 @@ public struct ExploreView: View {
|
||||||
viewModel.scrollToTopVisible = false
|
viewModel.scrollToTopVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeNextPageView(for type: Search.EntityType) -> some View {
|
private func makeNextPageView(for type: Search.EntityType) -> some View {
|
||||||
NextPageView {
|
NextPageView {
|
||||||
await viewModel.fetchNextPage(of: type)
|
await viewModel.fetchNextPage(of: type)
|
||||||
|
|
|
@ -115,7 +115,7 @@ import SwiftUI
|
||||||
isSearching = false
|
isSearching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage(of type: Search.EntityType) async {
|
func fetchNextPage(of type: Search.EntityType) async {
|
||||||
guard let client, !searchQuery.isEmpty,
|
guard let client, !searchQuery.isEmpty,
|
||||||
let results = results[searchQuery] else { return }
|
let results = results[searchQuery] else { return }
|
||||||
|
@ -128,18 +128,18 @@ import SwiftUI
|
||||||
case .statuses:
|
case .statuses:
|
||||||
results.statuses.count
|
results.statuses.count
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
|
var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
|
||||||
type: type,
|
type: type,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
following: nil),
|
following: nil),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
if type == .accounts {
|
if type == .accounts {
|
||||||
let relationships: [Relationship] =
|
let relationships: [Relationship] =
|
||||||
try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id)))
|
try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id)))
|
||||||
newPageResults.relationships = relationships
|
newPageResults.relationships = relationships
|
||||||
}
|
}
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .accounts:
|
case .accounts:
|
||||||
self.results[searchQuery]?.accounts.append(contentsOf: newPageResults.accounts)
|
self.results[searchQuery]?.accounts.append(contentsOf: newPageResults.accounts)
|
||||||
|
@ -149,8 +149,6 @@ import SwiftUI
|
||||||
case .statuses:
|
case .statuses:
|
||||||
self.results[searchQuery]?.statuses.append(contentsOf: newPageResults.statuses)
|
self.results[searchQuery]?.statuses.append(contentsOf: newPageResults.statuses)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ public struct MediaUIShareLink: View, @unchecked Sendable {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.type = type
|
self.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if type == .image {
|
if type == .image {
|
||||||
let transferable = MediaUIImageTransferable(url: url)
|
let transferable = MediaUIImageTransferable(url: url)
|
||||||
|
|
|
@ -8,7 +8,7 @@ public struct MediaUIImageTransferable: Codable, Transferable {
|
||||||
public init(url: URL) {
|
public init(url: URL) {
|
||||||
self.url = url
|
self.url = url
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchData() async -> Data {
|
public func fetchData() async -> Data {
|
||||||
do {
|
do {
|
||||||
return try await URLSession.shared.data(from: url).0
|
return try await URLSession.shared.data(from: url).0
|
||||||
|
|
|
@ -175,9 +175,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
||||||
return
|
return
|
||||||
} else if node.nodeName() == "#text" {
|
} else if node.nodeName() == "#text" {
|
||||||
var txt = node.description
|
var txt = node.description
|
||||||
|
|
||||||
txt = (try? Entities.unescape(txt)) ?? txt
|
txt = (try? Entities.unescape(txt)) ?? txt
|
||||||
|
|
||||||
if let underscore_regex, let main_regex {
|
if let underscore_regex, let main_regex {
|
||||||
// This is the markdown escaper
|
// This is the markdown escaper
|
||||||
txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1")
|
txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1")
|
||||||
|
|
|
@ -4,11 +4,12 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct CardAuthor: Codable, Sendable, Identifiable, Equatable, Hashable {
|
public struct CardAuthor: Codable, Sendable, Identifiable, Equatable, Hashable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
public let name: String
|
public let name: String
|
||||||
public let url: String
|
public let url: String
|
||||||
public let account: Account?
|
public let account: Account?
|
||||||
|
|
|
@ -4,10 +4,10 @@ public struct MediaStatus: Sendable, Identifiable, Hashable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
attachment.id
|
attachment.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public let status: Status
|
public let status: Status
|
||||||
public let attachment: MediaAttachment
|
public let attachment: MediaAttachment
|
||||||
|
|
||||||
public init(status: Status, attachment: MediaAttachment) {
|
public init(status: Status, attachment: MediaAttachment) {
|
||||||
self.status = status
|
self.status = status
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
|
|
|
@ -77,9 +77,9 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
||||||
public var isHidden: Bool {
|
public var isHidden: Bool {
|
||||||
filtered?.first?.filter.filterAction == .hide
|
filtered?.first?.filter.filterAction == .hide
|
||||||
}
|
}
|
||||||
|
|
||||||
public var asMediaStatus: [MediaStatus] {
|
public var asMediaStatus: [MediaStatus] {
|
||||||
mediaAttachments.map{ .init(status: self, attachment: $0)}
|
mediaAttachments.map { .init(status: self, attachment: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
||||||
|
|
|
@ -4,7 +4,6 @@ public enum Profile: Endpoint {
|
||||||
case deleteAvatar
|
case deleteAvatar
|
||||||
case deleteHeader
|
case deleteHeader
|
||||||
|
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .deleteAvatar:
|
case .deleteAvatar:
|
||||||
|
|
|
@ -4,7 +4,7 @@ public enum Search: Endpoint {
|
||||||
public enum EntityType: String, Sendable {
|
public enum EntityType: String, Sendable {
|
||||||
case accounts, hashtags, statuses
|
case accounts, hashtags, statuses
|
||||||
}
|
}
|
||||||
|
|
||||||
case search(query: String, type: EntityType?, offset: Int?, following: Bool?)
|
case search(query: String, type: EntityType?, offset: Int?, following: Bool?)
|
||||||
case accountsSearch(query: String, type: EntityType?, offset: Int?, following: Bool?)
|
case accountsSearch(query: String, type: EntityType?, offset: Int?, following: Bool?)
|
||||||
|
|
||||||
|
|
|
@ -58,10 +58,10 @@ public extension StatusEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func compressImageForUpload(
|
public func compressImageForUpload(
|
||||||
_ image: UIImage,
|
_ image: UIImage,
|
||||||
maxSize: Int = 10 * 1024 * 1024,
|
maxSize: Int = 10 * 1024 * 1024,
|
||||||
maxHeight: Double = 5000,
|
maxHeight: Double = 5000,
|
||||||
maxWidth: Double = 5000
|
maxWidth: Double = 5000
|
||||||
) async throws -> Data {
|
) async throws -> Data {
|
||||||
var image = image
|
var image = image
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,9 @@ extension StatusEditor {
|
||||||
generateButton
|
generateButton
|
||||||
}
|
}
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
if #available(iOS 17.4, *), !imageDescription.isEmpty {
|
if #available(iOS 17.4, *), !imageDescription.isEmpty {
|
||||||
translateButton
|
translateButton
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
|
@ -194,7 +194,7 @@ extension StatusEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeErrorView(error: ServerError) -> some View {
|
private func makeErrorView(error _: ServerError) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
placeholderView
|
placeholderView
|
||||||
Text("status.editor.error.upload")
|
Text("status.editor.error.upload")
|
||||||
|
|
|
@ -3,12 +3,12 @@ import SwiftUI
|
||||||
public struct StatusRowExternalView: View {
|
public struct StatusRowExternalView: View {
|
||||||
@State private var viewModel: StatusRowViewModel
|
@State private var viewModel: StatusRowViewModel
|
||||||
private let context: StatusRowView.Context
|
private let context: StatusRowView.Context
|
||||||
|
|
||||||
public init(viewModel: StatusRowViewModel, context: StatusRowView.Context = .timeline) {
|
public init(viewModel: StatusRowViewModel, context: StatusRowView.Context = .timeline) {
|
||||||
_viewModel = .init(initialValue: viewModel)
|
_viewModel = .init(initialValue: viewModel)
|
||||||
self.context = context
|
self.context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
StatusRowView(viewModel: viewModel, context: context)
|
StatusRowView(viewModel: viewModel, context: context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ public struct StatusRowView: View {
|
||||||
@Environment(\.isStatusFocused) private var isFocused
|
@Environment(\.isStatusFocused) private var isFocused
|
||||||
@Environment(\.indentationLevel) private var indentationLevel
|
@Environment(\.indentationLevel) private var indentationLevel
|
||||||
@Environment(\.isHomeTimeline) private var isHomeTimeline
|
@Environment(\.isHomeTimeline) private var isHomeTimeline
|
||||||
|
|
||||||
@Environment(RouterPath.self) private var routerPath: RouterPath
|
@Environment(RouterPath.self) private var routerPath: RouterPath
|
||||||
|
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
|
@ -27,7 +27,7 @@ public struct StatusRowView: View {
|
||||||
@State private var isBlockConfirmationPresented = false
|
@State private var isBlockConfirmationPresented = false
|
||||||
|
|
||||||
public enum Context { case timeline, detail }
|
public enum Context { case timeline, detail }
|
||||||
|
|
||||||
@State public var viewModel: StatusRowViewModel
|
@State public var viewModel: StatusRowViewModel
|
||||||
public let context: Context
|
public let context: Context
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import SwiftUI
|
||||||
|
|
||||||
let client: Client
|
let client: Client
|
||||||
let routerPath: RouterPath
|
let routerPath: RouterPath
|
||||||
|
|
||||||
let userFollowedTag: HTMLString.Link?
|
let userFollowedTag: HTMLString.Link?
|
||||||
|
|
||||||
private let theme = Theme.shared
|
private let theme = Theme.shared
|
||||||
|
@ -118,7 +118,7 @@ import SwiftUI
|
||||||
backgroundColor
|
backgroundColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var homeBackgroundColor: some View {
|
var homeBackgroundColor: some View {
|
||||||
if status.visibility == .direct {
|
if status.visibility == .direct {
|
||||||
|
@ -146,7 +146,7 @@ import SwiftUI
|
||||||
theme.primaryBackgroundColor
|
theme.primaryBackgroundColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeDecorativeGradient(startColor: Color, endColor: Color) -> some View {
|
func makeDecorativeGradient(startColor: Color, endColor: Color) -> some View {
|
||||||
LinearGradient(stops: [
|
LinearGradient(stops: [
|
||||||
.init(color: startColor.opacity(0.2), location: 0.03),
|
.init(color: startColor.opacity(0.2), location: 0.03),
|
||||||
|
@ -155,8 +155,8 @@ import SwiftUI
|
||||||
.init(color: startColor.opacity(0.02), location: 0.15),
|
.init(color: startColor.opacity(0.02), location: 0.15),
|
||||||
.init(color: endColor, location: 0.25),
|
.init(color: endColor, location: 0.25),
|
||||||
],
|
],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing)
|
endPoint: .bottomTrailing)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(status: Status,
|
public init(status: Status,
|
||||||
|
@ -191,7 +191,7 @@ import SwiftUI
|
||||||
} else {
|
} else {
|
||||||
userMentionned = false
|
userMentionned = false
|
||||||
}
|
}
|
||||||
|
|
||||||
userFollowedTag = finalStatus.content.links.first(where: { link in
|
userFollowedTag = finalStatus.content.links.first(where: { link in
|
||||||
link.type == .hashtag && CurrentAccount.shared.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() })
|
link.type == .hashtag && CurrentAccount.shared.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() })
|
||||||
})
|
})
|
||||||
|
@ -366,29 +366,31 @@ import SwiftUI
|
||||||
|
|
||||||
if preferredTranslationType != .useDeepl {
|
if preferredTranslationType != .useDeepl {
|
||||||
await translateWithInstance(userLang: userLang)
|
await translateWithInstance(userLang: userLang)
|
||||||
|
|
||||||
if translation == nil {
|
if translation == nil {
|
||||||
await translateWithDeepL(userLang: userLang)
|
await translateWithDeepL(userLang: userLang)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await translateWithDeepL(userLang: userLang)
|
await translateWithDeepL(userLang: userLang)
|
||||||
|
|
||||||
if translation == nil {
|
if translation == nil {
|
||||||
await translateWithInstance(userLang: userLang)
|
await translateWithInstance(userLang: userLang)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasShown = false
|
var hasShown = false
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
if translation == nil,
|
if translation == nil,
|
||||||
#available(iOS 17.4, *) {
|
#available(iOS 17.4, *)
|
||||||
showAppleTranslation = true
|
{
|
||||||
hasShown = true
|
showAppleTranslation = true
|
||||||
}
|
hasShown = true
|
||||||
#endif
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if !hasShown,
|
if !hasShown,
|
||||||
translation == nil {
|
translation == nil
|
||||||
|
{
|
||||||
if preferredTranslationType == .useDeepl {
|
if preferredTranslationType == .useDeepl {
|
||||||
deeplTranslationError = true
|
deeplTranslationError = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -410,7 +412,7 @@ import SwiftUI
|
||||||
isLoadingTranslation = false
|
isLoadingTranslation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateWithInstance(userLang: String) async {
|
func translateWithInstance(userLang: String) async {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLoadingTranslation = true
|
isLoadingTranslation = true
|
||||||
|
|
|
@ -186,7 +186,7 @@ public struct StatusRowCardView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
|
@ -248,7 +248,7 @@ public struct StatusRowCardView: View {
|
||||||
}.padding(16)
|
}.padding(16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func moreFromAccountView(_ account: Account, divider: Bool = true) -> some View {
|
private func moreFromAccountView(_ account: Account, divider: Bool = true) -> some View {
|
||||||
if divider {
|
if divider {
|
||||||
|
|
|
@ -95,9 +95,9 @@ struct StatusRowHeaderView: View {
|
||||||
|
|
||||||
private var dateView: some View {
|
private var dateView: some View {
|
||||||
Text("\(Image(systemName: viewModel.finalStatus.visibility.iconName)) ⸱ \(viewModel.finalStatus.createdAt.relativeFormatted)")
|
Text("\(Image(systemName: viewModel.finalStatus.visibility.iconName)) ⸱ \(viewModel.finalStatus.createdAt.relativeFormatted)")
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,32 +280,32 @@ struct AltTextButton: View {
|
||||||
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
|
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
.addTranslateView(isPresented: $isDisplayingTranslation, text: text)
|
.addTranslateView(isPresented: $isDisplayingTranslation, text: text)
|
||||||
#endif
|
#endif
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
#endif
|
#endif
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
.padding(theme.statusDisplayStyle == .compact ? 0 : 10)
|
.padding(theme.statusDisplayStyle == .compact ? 0 : 10)
|
||||||
.alert(
|
.alert(
|
||||||
"status.editor.media.image-description",
|
"status.editor.media.image-description",
|
||||||
isPresented: $isDisplayingAlert
|
isPresented: $isDisplayingAlert
|
||||||
) {
|
) {
|
||||||
Button("alert.button.ok", action: {})
|
Button("alert.button.ok", action: {})
|
||||||
Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
|
Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
|
||||||
#if canImport(_Translation_SwiftUI)
|
#if canImport(_Translation_SwiftUI)
|
||||||
if #available(iOS 17.4, *) {
|
if #available(iOS 17.4, *) {
|
||||||
Button("status.action.translate", action: { isDisplayingTranslation = true })
|
Button("status.action.translate", action: { isDisplayingTranslation = true })
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
} message: {
|
} message: {
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
maxHeight: .infinity,
|
maxHeight: .infinity,
|
||||||
alignment: .bottomTrailing
|
alignment: .bottomTrailing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -307,25 +307,26 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
|
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
|
||||||
canStreamEvents = false
|
canStreamEvents = false
|
||||||
let initialTimeline = timeline
|
let initialTimeline = timeline
|
||||||
|
|
||||||
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
|
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
|
||||||
|
|
||||||
guard !newStatuses.isEmpty,
|
guard !newStatuses.isEmpty,
|
||||||
isTimelineVisible,
|
isTimelineVisible,
|
||||||
!Task.isCancelled,
|
!Task.isCancelled,
|
||||||
initialTimeline == timeline else {
|
initialTimeline == timeline
|
||||||
|
else {
|
||||||
canStreamEvents = true
|
canStreamEvents = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateTimelineWithNewStatuses(newStatuses)
|
await updateTimelineWithNewStatuses(newStatuses)
|
||||||
|
|
||||||
if !Task.isCancelled, let latest = await datasource.get().first {
|
if !Task.isCancelled, let latest = await datasource.get().first {
|
||||||
pendingStatusesObserver.isLoadingNewStatuses = true
|
pendingStatusesObserver.isLoadingNewStatuses = true
|
||||||
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
|
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
|
||||||
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
||||||
let ids = await datasource.get().map(\.id)
|
let ids = await datasource.get().map(\.id)
|
||||||
|
@ -335,38 +336,39 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||||
return newStatuses
|
return newStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
|
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
|
||||||
let topStatus = await datasource.getFiltered().first
|
let topStatus = await datasource.getFiltered().first
|
||||||
await datasource.insert(contentOf: newStatuses, at: 0)
|
await datasource.insert(contentOf: newStatuses, at: 0)
|
||||||
await cache()
|
await cache()
|
||||||
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
||||||
|
|
||||||
let statuses = await datasource.getFiltered()
|
let statuses = await datasource.getFiltered()
|
||||||
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
|
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
|
||||||
|
|
||||||
if let topStatus = topStatus,
|
if let topStatus = topStatus,
|
||||||
visibileStatuses.contains(where: { $0.id == topStatus.id }),
|
visibileStatuses.contains(where: { $0.id == topStatus.id }),
|
||||||
scrollToTopVisible {
|
scrollToTopVisible
|
||||||
|
{
|
||||||
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
|
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
|
||||||
} else {
|
} else {
|
||||||
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
|
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the timeline while keeping the scroll position to the top status.
|
// Refresh the timeline while keeping the scroll position to the top status.
|
||||||
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
|
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||||
pendingStatusesObserver.disableUpdate = true
|
pendingStatusesObserver.disableUpdate = true
|
||||||
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
||||||
scrollToIndexAnimated = false
|
scrollToIndexAnimated = false
|
||||||
scrollToIndex = newStatuses.count + 1
|
scrollToIndex = newStatuses.count + 1
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.pendingStatusesObserver.disableUpdate = false
|
self?.pendingStatusesObserver.disableUpdate = false
|
||||||
self?.canStreamEvents = true
|
self?.canStreamEvents = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the timeline while keeping the user current position.
|
// Refresh the timeline while keeping the user current position.
|
||||||
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
|
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
|
||||||
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
|
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||||
|
@ -381,18 +383,18 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
var allStatuses: [Status] = []
|
var allStatuses: [Status] = []
|
||||||
var latestMinId = minId
|
var latestMinId = minId
|
||||||
do {
|
do {
|
||||||
for _ in 1...maxPages {
|
for _ in 1 ... maxPages {
|
||||||
if Task.isCancelled { break }
|
if Task.isCancelled { break }
|
||||||
|
|
||||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
|
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
|
||||||
sinceId: nil,
|
sinceId: nil,
|
||||||
maxId: nil,
|
maxId: nil,
|
||||||
minId: latestMinId,
|
minId: latestMinId,
|
||||||
offset: nil
|
offset: nil
|
||||||
))
|
))
|
||||||
|
|
||||||
if newStatuses.isEmpty { break }
|
if newStatuses.isEmpty { break }
|
||||||
|
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||||
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
latestMinId = newStatuses.first?.id ?? latestMinId
|
latestMinId = newStatuses.first?.id ?? latestMinId
|
||||||
|
@ -400,7 +402,7 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
} catch {
|
} catch {
|
||||||
return allStatuses
|
return allStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
return allStatuses
|
return allStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue