diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 1ea0d714..360689ae 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -246,8 +246,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } } - - func statusDidAppear(status: Models.Status) { } - - func statusDidDisappear(status: Status) { } + + func statusDidAppear(status _: Models.Status) {} + + func statusDidDisappear(status _: Status) {} } diff --git a/Packages/Models/Sources/Models/Alias/HTMLString.swift b/Packages/Models/Sources/Models/Alias/HTMLString.swift index d4b46697..37254f96 100644 --- a/Packages/Models/Sources/Models/Alias/HTMLString.swift +++ b/Packages/Models/Sources/Models/Alias/HTMLString.swift @@ -36,7 +36,7 @@ public struct HTMLString: Decodable, Equatable, Hashable { if let regex = try? NSRegularExpression(pattern: "((.*?))", options: .caseInsensitive) { htmlValue = regex.stringByReplacingMatches(in: htmlValue, options: [], range: NSRange(location: 0, length: htmlValue.count), withTemplate: "$2…") } - + do { asMarkdown = try HTMLParser().parse(html: htmlValue) .toMarkdown() diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 39ebcb8a..2a5b7754 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -17,11 +17,11 @@ public class Client: ObservableObject, Equatable, Identifiable, Hashable { case missingApp case invalidRedirectURL } - + public var id: String { "\(isAuth)\(server)\(oauthToken?.accessToken ?? "")" } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index b68bf039..2804ee63 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -201,7 +201,7 @@ public class StatusEditorViewModel: ObservableObject { mentionString += "@\(mention.acct)" } if !mentionString.isEmpty { - mentionString += " " + mentionString += " " } replyToStatus = status visibility = status.visibility diff --git a/Packages/Status/Sources/Status/List/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift index 133e7d9c..c566557e 100644 --- a/Packages/Status/Sources/Status/List/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -5,7 +5,7 @@ import SwiftUI public struct StatusesListView: View where Fetcher: StatusesFetcher { @EnvironmentObject private var theme: Theme - + @ObservedObject private var fetcher: Fetcher private let isRemote: Bool private let isEmbdedInList: Bool diff --git a/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift b/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift index 4b3faeff..759ae48d 100644 --- a/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift +++ b/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift @@ -1,29 +1,29 @@ import Foundation -import SwiftUI import Models +import SwiftUI @MainActor class PendingStatusesObserver: ObservableObject { let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) - + @Published var pendingStatusesCount: Int = 0 - + var disableUpdate: Bool = false - + var pendingStatuses: [String] = [] { didSet { pendingStatusesCount = pendingStatuses.count } } - + func removeStatus(status: Status) { if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) { - pendingStatuses.removeSubrange(index...(pendingStatuses.count - 1)) + pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1)) feedbackGenerator.impactOccurred() } } - - init() { } + + init() {} } struct PendingStatusesObserverView: View { @@ -32,7 +32,7 @@ struct PendingStatusesObserverView: View { if observer.pendingStatusesCount > 0 { HStack(spacing: 6) { Spacer() - Button { } label: { + Button {} label: { Text("\(observer.pendingStatusesCount)") } .buttonStyle(.bordered) diff --git a/Packages/Timeline/Sources/Timeline/TimelineCache.swift b/Packages/Timeline/Sources/Timeline/TimelineCache.swift index aafc1c28..5a5f5ff2 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineCache.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineCache.swift @@ -1,18 +1,18 @@ -import SwiftUI import Models import Network +import SwiftUI actor TimelineCache { static let shared: TimelineCache = .init() - + private var memoryCache: [Client: [Status]] = [:] - + private init() {} - + func set(statuses: [Status], client: Client) { - memoryCache[client] = statuses.prefix(upTo: min(100, (statuses.count - 1))).map{ $0 } + memoryCache[client] = statuses.prefix(upTo: min(100, statuses.count - 1)).map { $0 } } - + func getStatuses(for client: Client) -> [Status]? { memoryCache[client] } diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 26088b7d..ae3419df 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -19,9 +19,9 @@ public struct TimelineView: View { @EnvironmentObject private var routerPath: RouterPath @StateObject private var viewModel = TimelineViewModel() - + @State private var wasBackgrounded: Bool = false - + @Binding var timeline: TimelineFilter @Binding var scrollToTopSignal: Int @@ -111,7 +111,7 @@ public struct TimelineView: View { } case .background: wasBackgrounded = true - + default: break } @@ -154,9 +154,9 @@ public struct TimelineView: View { trailing: .layoutPadding)) } } - + private var scrollToTopView: some View { - HStack{ EmptyView() } + HStack { EmptyView() } .listRowBackground(theme.primaryBackgroundColor) .listRowSeparator(.hidden) .listRowInsets(.init()) diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index f4b2bca5..78b3209e 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -18,14 +18,14 @@ class TimelineViewModel: ObservableObject { private var statuses: [Status] = [] private var visibileStatusesIds = Set() var scrollToTopVisible: Bool = false - + private var canStreamEvents: Bool = true - + let pendingStatusesObserver: PendingStatusesObserver = .init() let cache: TimelineCache = .shared @Published var scrollToStatus: String? - + @Published var statusesState: StatusesState = .loading @Published var timeline: TimelineFilter = .federated { didSet { @@ -57,7 +57,6 @@ class TimelineViewModel: ObservableObject { client?.server ?? "Error" } - func fetchTag(id: String) async { guard let client else { return } do { @@ -65,7 +64,7 @@ class TimelineViewModel: ObservableObject { } catch {} } - func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) { + func handleEvent(event: any StreamEvent, currentAccount _: CurrentAccount) { if let event = event as? StreamEventUpdate, canStreamEvents, pendingStatusesEnabled, @@ -91,13 +90,14 @@ class TimelineViewModel: ObservableObject { } // MARK: - Cache + extension TimelineViewModel { private func cache(statuses: [Status]) async { if let client { await cache.set(statuses: statuses, client: client) } } - + private func getCachedStatuses() async -> [Status]? { if let client { return await cache.getStatuses(for: client) @@ -107,6 +107,7 @@ extension TimelineViewModel { } // MARK: - StatusesFetcher + extension TimelineViewModel: StatusesFetcher { func fetchStatuses() async { guard let client else { return } @@ -122,12 +123,12 @@ extension TimelineViewModel: StatusesFetcher { print("timeline parse error: \(error)") } } - + // Hydrate statuses in the Timeline when statuses are empty. private func fetchFirstPage(client: Client) async throws { pendingStatusesObserver.pendingStatuses = [] statusesState = .loading - + // If we get statuses from the cache for the home timeline, we displays those. // Else we fetch top most page from the API. if let cachedStatuses = await getCachedStatuses(), timeline == .home { @@ -150,34 +151,34 @@ extension TimelineViewModel: StatusesFetcher { } } } - + // Fetch pages from the top most status of the tomeline. - private func fetchNewPagesFrom(latestStatus: Status, client: Client) async throws { + private func fetchNewPagesFrom(latestStatus: Status, client _: Client) async throws { canStreamEvents = false var newStatuses: [Status] = await fetchNewPages(minId: latestStatus.id, maxPages: 10) - + // Dedup statuses, a status with the same id could have been streamed in. newStatuses = newStatuses.filter { status in !statuses.contains(where: { $0.id == status.id }) } - + // If no new statuses, resume streaming and exit. guard !newStatuses.isEmpty else { canStreamEvents = true return } - + // Keep track of the top most status, so we can scroll back to it after view update. let topStatusId = statuses.first?.id - + // Insert new statuses in internal datasource. statuses.insert(contentsOf: newStatuses, at: 0) - + // Cache statuses for home timeline. if timeline == .home { await cache(statuses: statuses) } - + // If pending statuses are not enabled, we simply load status on the top regardless of the current position. if !pendingStatusesEnabled { pendingStatusesObserver.pendingStatuses = [] @@ -187,9 +188,9 @@ extension TimelineViewModel: StatusesFetcher { } } else { // Append new statuses in the timeline indicator. - pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0) + pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map { $0.id }, at: 0) pendingStatusesObserver.feedbackGenerator.impactOccurred() - + // High chance the user is scrolled to the top. // We need to update the statuses state, and then scroll to the previous top most status. if let topStatusId, visibileStatusesIds.contains(topStatusId), scrollToTopVisible { @@ -209,7 +210,7 @@ extension TimelineViewModel: StatusesFetcher { } } } - + private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] { guard let client else { return [] } var pagesLoaded = 0 @@ -232,7 +233,7 @@ extension TimelineViewModel: StatusesFetcher { } return allStatuses } - + func fetchNextPage() async { guard let client else { return } do { @@ -248,12 +249,12 @@ extension TimelineViewModel: StatusesFetcher { statusesState = .error(error: error) } } - + func statusDidAppear(status: Status) { pendingStatusesObserver.removeStatus(status: status) visibileStatusesIds.insert(status.id) } - + func statusDidDisappear(status: Status) { visibileStatusesIds.remove(status.id) }