From bc918ffdfc0a758728edb147da3d08c099afd447 Mon Sep 17 00:00:00 2001 From: shannon Date: Fri, 24 Jan 2025 15:08:11 -0500 Subject: [PATCH] Restore ability to load additional notifications beyond the first fetched batch --- .../NotificationListViewController.swift | 2 +- .../Notification/NotificationListItem.swift | 13 +--- .../Notification/NotificationSection.swift | 5 -- ...ineViewController+DataSourceProvider.swift | 2 +- .../NotificationTimelineViewController.swift | 2 +- .../NotificationTimelineViewModel.swift | 30 ++++---- .../DataController/MastodonFeedLoader.swift | 74 +++++++++++-------- 7 files changed, 68 insertions(+), 60 deletions(-) diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift index d2b08a804..ecbf06118 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift @@ -89,7 +89,7 @@ struct NotificationListView: View { @ViewBuilder func rowView(_ notificationListItem: NotificationListItem) -> some View { switch notificationListItem { - case .bottomLoader, .middleLoader: + case .bottomLoader: Text("loader not yet implemented") case .filteredNotificationsInfo: Text("filtered notifications not yet implemented") diff --git a/Mastodon/Scene/Notification/NotificationListItem.swift b/Mastodon/Scene/Notification/NotificationListItem.swift index db0559228..9d3263998 100644 --- a/Mastodon/Scene/Notification/NotificationListItem.swift +++ b/Mastodon/Scene/Notification/NotificationListItem.swift @@ -12,19 +12,16 @@ import MastodonSDK enum NotificationListItem: Hashable { case filteredNotificationsInfo(policy: Mastodon.Entity.NotificationPolicy) case notification(MastodonFeedItemIdentifier) - case middleLoader(after: MastodonFeedItemIdentifier, before: MastodonFeedItemIdentifier) case bottomLoader - var nextFetchAnchors: (MastodonFeedItemIdentifier?, MastodonFeedItemIdentifier?) { + var fetchAnchor: MastodonFeedItemIdentifier? { switch self { case .filteredNotificationsInfo: - return (nil, nil) + return nil case .notification(let identifier): - return (identifier, nil) - case .middleLoader(let after, let before): - return (after, before) + return identifier case .bottomLoader: - return (nil, nil) + return nil } } } @@ -38,8 +35,6 @@ extension NotificationListItem: Identifiable { return "filtered_notifications_info" case .notification(let identifier): return identifier.id - case let .middleLoader(afterID, beforeID): - return afterID.id+"-"+beforeID.id case .bottomLoader: return "bottom_loader" } diff --git a/Mastodon/Scene/Notification/NotificationSection.swift b/Mastodon/Scene/Notification/NotificationSection.swift index f4529bcf1..361fc7b1d 100644 --- a/Mastodon/Scene/Notification/NotificationSection.swift +++ b/Mastodon/Scene/Notification/NotificationSection.swift @@ -56,11 +56,6 @@ extension NotificationSection { ) return cell } - - case .middleLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell - cell.activityIndicatorView.startAnimating() - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index 1108c9fa2..db3f23432 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -58,7 +58,7 @@ extension NotificationTimelineViewController: DataSourceProvider { } case .filteredNotificationsInfo(let policy): return DataSourceItem.notificationBanner(policy: policy) - case .bottomLoader, .middleLoader: + case .bottomLoader: return nil } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index cb18698ec..5b56a34ce 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -187,7 +187,7 @@ extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateT return } Task { - await viewModel.loadMore(item: item) + await viewModel.loadMore(olderThan: item, newerThan: nil) } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index f5e5574d0..cdcb7c57a 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -123,22 +123,26 @@ extension NotificationTimelineViewModel { func loadLatest() async { isLoadingLatest = true defer { isLoadingLatest = false } - feedLoader.loadMore(olderThan: nil, newerThan: nil) + let currentFirst = diffableDataSource?.snapshot().itemIdentifiers.first + await loadMore(olderThan: nil, newerThan: currentFirst) didLoadLatest.send() } - // load timeline gap - func loadMore(item: NotificationListItem) async { - let olderThan: MastodonFeedItemIdentifier? - let newerThan: MastodonFeedItemIdentifier? - switch item { - case .notification, .middleLoader: - (olderThan, newerThan) = item.nextFetchAnchors - case .bottomLoader: - (olderThan, newerThan) = diffableDataSource?.snapshot().itemIdentifiers.last(where: { $0 != .bottomLoader })?.nextFetchAnchors ?? (nil, nil) - case .filteredNotificationsInfo: - return + func loadMore(olderThan: NotificationListItem?, newerThan: NotificationListItem?) async { + + func fetchAnchor(for item: NotificationListItem?) -> MastodonFeedItemIdentifier? { + switch item { + case .notification: + return item?.fetchAnchor + case .bottomLoader: + return diffableDataSource?.snapshot().itemIdentifiers.last(where: { $0.fetchAnchor != nil })?.fetchAnchor + case .filteredNotificationsInfo: + return diffableDataSource?.snapshot().itemIdentifiers.first(where: { $0.fetchAnchor != nil })?.fetchAnchor + case .none: + return nil + } } - feedLoader.loadMore(olderThan: olderThan, newerThan: newerThan) + + feedLoader.loadMore(olderThan: fetchAnchor(for: olderThan), newerThan: fetchAnchor(for: newerThan)) } } diff --git a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift index c4a8fb625..277ac85eb 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift @@ -14,6 +14,28 @@ import os.log @MainActor final public class MastodonFeedLoader { + struct FeedLoadRequest: Equatable { + let olderThan: MastodonFeedItemIdentifier? + let newerThan: MastodonFeedItemIdentifier? + + var maxID: String? { olderThan?.id } + + var resultsInsertionPoint: InsertLocation { + if olderThan != nil { + return .end + } else if newerThan != nil { + return .start + } else { + return .replace + } + } + enum InsertLocation { + case start + case end + case replace + } + } + private let logger = Logger(subsystem: "MastodonFeedLoader", 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)." @@ -37,43 +59,26 @@ final public class MastodonFeedLoader { } } + private var mostRecentLoad: FeedLoadRequest? + public func loadMore(olderThan: MastodonFeedItemIdentifier?, newerThan: MastodonFeedItemIdentifier?) { - if let olderThan { - Task { - let unfiltered = try await load(kind: kind, olderThan: olderThan.id) - await setRecordsAfterFiltering(unfiltered) - } - } else { - loadInitial(kind: kind) - } - } - - private func loadInitial(kind: MastodonFeedKind) { + let request = FeedLoadRequest(olderThan: olderThan, newerThan: newerThan) Task { - let unfilteredRecords = try await load(kind: kind) - await setRecordsAfterFiltering(unfilteredRecords) + let unfiltered = try await load(request) + await insertRecordsAfterFiltering(at: request.resultsInsertionPoint, additionalRecords:unfiltered) } } - private func loadNext(kind: MastodonFeedKind) { - Task { - guard let lastId = records.last?.id else { - return loadInitial(kind: kind) - } - - let unfiltered = try await load(kind: kind, olderThan: lastId) - await self.appendRecordsAfterFiltering(unfiltered) - } - } - - private func load(kind: MastodonFeedKind, olderThan maxID: String? = nil) async throws -> [MastodonFeedItemIdentifier] { + private func load(_ request: FeedLoadRequest) async throws -> [MastodonFeedItemIdentifier] { + guard request != mostRecentLoad else { throw AppError.badRequest } + mostRecentLoad = request switch kind { case .notificationsAll: - return try await loadNotifications(withScope: .everything, olderThan: maxID) + return try await loadNotifications(withScope: .everything, olderThan: request.maxID) case .notificationsMentionsOnly: - return try await loadNotifications(withScope: .mentions, olderThan: maxID) + return try await loadNotifications(withScope: .mentions, olderThan: request.maxID) case .notificationsWithAccount(let accountID): - return try await loadNotifications(withAccountID: accountID, olderThan: maxID) + return try await loadNotifications(withAccountID: accountID, olderThan: request.maxID) } } @@ -217,10 +222,19 @@ private extension MastodonFeedLoader { self.records = filtered.removingDuplicates() } - private func appendRecordsAfterFiltering(_ additionalRecords: [MastodonFeedItemIdentifier]) async { + private func insertRecordsAfterFiltering(at insertionPoint: FeedLoadRequest.InsertLocation, additionalRecords: [MastodonFeedItemIdentifier]) async { guard let filterBox = StatusFilterService.shared.activeFilterBox else { self.records += additionalRecords; return } let newRecords = await self.filter(additionalRecords, forFeed: kind, with: filterBox) - self.records = (self.records + newRecords).removingDuplicates() + var combinedRecords = self.records + switch insertionPoint { + case .start: + combinedRecords = newRecords + combinedRecords + case .end: + combinedRecords.append(contentsOf: newRecords) + case .replace: + combinedRecords = newRecords + } + self.records = combinedRecords.removingDuplicates() } private func filter(_ records: [MastodonFeedItemIdentifier], forFeed feedKind: MastodonFeedKind, with filterBox: Mastodon.Entity.FilterBox) async -> [MastodonFeedItemIdentifier] {