diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 07af1d2c0..fedbb71a6 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -112,18 +112,21 @@ final class HomeTimelineViewController: UIViewController, MediaPreviewableViewCo let showFollowingAction = UIAction(title: L10n.Scene.HomeTimeline.TimelineMenu.following, image: .init(systemName: "house")) { [weak self] _ in guard let self, let viewModel = self.viewModel else { return } - viewModel.timelineContext = .home - viewModel.dataController.records = [] - - viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.ContextSwitch.self) - timelineSelectorButton.setAttributedTitle( - .init(string: L10n.Scene.HomeTimeline.TimelineMenu.following, attributes: [ - .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) - ]), - for: .normal) - - timelineSelectorButton.sizeToFit() - timelineSelectorButton.menu = generateTimelineSelectorMenu() + Task { [weak self] in + guard let self else { return } + viewModel.timelineContext = .home + await viewModel.dataController.setRecordsAfterFiltering([]) + + viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.ContextSwitch.self) + self.timelineSelectorButton.setAttributedTitle( + .init(string: L10n.Scene.HomeTimeline.TimelineMenu.following, attributes: [ + .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + ]), + for: .normal) + + self.timelineSelectorButton.sizeToFit() + self.timelineSelectorButton.menu = self.generateTimelineSelectorMenu() + } } let showLocalTimelineAction = UIAction(title: L10n.Scene.HomeTimeline.TimelineMenu.localCommunity, image: .init(systemName: "building.2")) { [weak self] action in diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 3c8f15969..72980f15a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -87,7 +87,7 @@ extension HomeTimelineViewModel.LoadLatestState { return } - viewModel.dataController.records = [] + await viewModel.dataController.setRecordsAfterFiltering([]) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems([.topLoader], toSection: .main) @@ -153,7 +153,7 @@ extension HomeTimelineViewModel.LoadLatestState { if statuses.isEmpty { // stop refresher if no new statuses - viewModel.dataController.records = [] + await viewModel.dataController.setRecordsAfterFiltering([]) viewModel.didLoadLatest.send() } else { var toAdd = [MastodonFeed]() @@ -169,7 +169,8 @@ extension HomeTimelineViewModel.LoadLatestState { toAdd.last?.hasMore = latestFeedRecords.isNotEmpty } - viewModel.dataController.records = (toAdd + latestFeedRecords).removingDuplicates() + let newRecords = (toAdd + latestFeedRecords).removingDuplicates() + await viewModel.dataController.setRecordsAfterFiltering(newRecords) } viewModel.timelineIsEmpty.value = (latestStatusIDs.isEmpty && statuses.isEmpty) ? { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 69d674b6c..7134bce9a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -94,9 +94,12 @@ final class HomeTimelineViewModel: NSObject { self.authenticationBox = authenticationBox self.dataController = FeedDataController(authenticationBox: authenticationBox, kind: .home(timeline: timelineContext)) super.init() - self.dataController.records = (try? PersistenceManager.shared.cachedTimeline(.homeTimeline(authenticationBox)).map { + let initialRecords = (try? PersistenceManager.shared.cachedTimeline(.homeTimeline(authenticationBox)).map { MastodonFeed.fromStatus($0, kind: .home) }) ?? [] + Task { + await self.dataController.setRecordsAfterFiltering(initialRecords) + } authenticationBox.inMemoryCache.$followingUserIds.sink { [weak self] _ in self?.homeTimelineNeedRefresh.send() @@ -231,10 +234,13 @@ extension HomeTimelineViewModel { } let combinedRecords = Array(head + feedItems + tail) - dataController.records = combinedRecords - record.isLoadingMore = false - record.hasMore = false + Task { + await dataController.setRecordsAfterFiltering(combinedRecords) + + record.isLoadingMore = false + record.hasMore = false + } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index ba3c87497..c003ba9d8 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -54,18 +54,22 @@ final class NotificationTimelineViewModel { self.scope = scope self.dataController = FeedDataController(authenticationBox: authenticationBox, kind: scope.feedKind) self.notificationPolicy = notificationPolicy - - switch scope { - case .everything: - self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authenticationBox))?.map({ notification in - MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll) - }) ?? [] - case .mentions: - self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authenticationBox))?.map({ notification in - MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions) - }) ?? [] - case .fromAccount(_): - self.dataController.records = [] + + Task { + switch scope { + case .everything: + let initialRecords = (try? FileManager.default.cachedNotificationsAll(for: authenticationBox))?.map({ notification in + MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll) + }) ?? [] + await self.dataController.setRecordsAfterFiltering(initialRecords) + case .mentions: + let initialRecords = (try? FileManager.default.cachedNotificationsMentions(for: authenticationBox))?.map({ notification in + MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions) + }) ?? [] + await self.dataController.setRecordsAfterFiltering(initialRecords) + case .fromAccount(_): + await self.dataController.setRecordsAfterFiltering([]) + } } self.dataController.$records diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index 501a60bbc..a92eb8a9d 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -9,7 +9,7 @@ final public class FeedDataController { private let logger = Logger(subsystem: "FeedDataController", category: "Data") private static let entryNotFoundMessage = "Failed to find suitable record. Depending on the context this might result in errors (data not being updated) or can be discarded (e.g. when there are mixed data sources where an entry might or might not exist)." - @Published public var records: [MastodonFeed] = [] + @Published public private(set) var records: [MastodonFeed] = [] private let authenticationBox: MastodonAuthenticationBox private let kind: MastodonFeed.Kind @@ -25,21 +25,27 @@ final public class FeedDataController { if let filterBox { Task { [weak self] in guard let self else { return } - self.records = await self.filter(self.records, forFeed: kind, with: filterBox) + await self.setRecordsAfterFiltering(self.records) } } } .store(in: &subscriptions) } + public func setRecordsAfterFiltering(_ newRecords: [MastodonFeed]) async { + guard let filterBox = StatusFilterService.shared.activeFilterBox else { self.records = newRecords; return } + self.records = await self.filter(self.records, forFeed: kind, with: filterBox) + } + + public func appendRecordsAfterFiltering(_ additionalRecords: [MastodonFeed]) async { + guard let filterBox = StatusFilterService.shared.activeFilterBox else { self.records += additionalRecords; return } + self.records += await self.filter(additionalRecords, forFeed: kind, with: filterBox) + } + public func loadInitial(kind: MastodonFeed.Kind) { Task { let unfilteredRecords = try await load(kind: kind, maxID: nil) - if let filterBox = StatusFilterService.shared.activeFilterBox { - records = await filter(unfilteredRecords, forFeed: kind, with: filterBox) - } else { - records = unfilteredRecords - } + await setRecordsAfterFiltering(unfilteredRecords) } } @@ -50,11 +56,7 @@ final public class FeedDataController { } let unfiltered = try await load(kind: kind, maxID: lastId) - if let filterBox = StatusFilterService.shared.activeFilterBox { - records += await filter(unfiltered, forFeed: kind, with: filterBox) - } else { - records += unfiltered - } + await self.appendRecordsAfterFiltering(unfiltered) } }