diff --git a/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift b/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift index e240c59be..75dc04b97 100644 --- a/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift +++ b/Mastodon/In Progress New Layout and Datamodel/GroupedNotificationFeedLoader.swift @@ -206,13 +206,13 @@ final public class GroupedNotificationFeedLoader { switch kind { case .notificationsAll: return try await loadNotifications( - withScope: .everything, olderThan: request.olderThan) + withScope: .everything, olderThan: request.olderThan, newerThan: request.newerThan) case .notificationsMentionsOnly: return try await loadNotifications( - withScope: .mentions, olderThan: request.olderThan) + withScope: .mentions, olderThan: request.olderThan, newerThan: request.newerThan) case .notificationsWithAccount(let accountID): return try await loadNotifications( - withAccountID: accountID, olderThan: request.olderThan) + withAccountID: accountID, olderThan: request.olderThan, newerThan: request.newerThan) } } } @@ -257,27 +257,28 @@ extension GroupedNotificationFeedLoader { extension GroupedNotificationFeedLoader { private func loadNotifications( withScope scope: APIService.MastodonNotificationScope, - olderThan maxID: String? = nil + olderThan maxID: String? = nil, + newerThan minID: String? ) async throws -> NotificationsResultType { if useGroupedNotificationsApi { do { return try await getGroupedNotifications( - withScope: scope, olderThan: maxID) + withScope: scope, olderThan: maxID, newerThan: minID) } catch { } } - return try await getUngroupedNotifications(withScope: scope, olderThan: maxID) + return try await getUngroupedNotifications(withScope: scope, olderThan: maxID, newerThan: minID) } private func loadNotifications( - withAccountID accountID: String, olderThan maxID: String? = nil + withAccountID accountID: String, olderThan maxID: String? = nil, newerThan minID: String? ) async throws -> [Mastodon.Entity.Notification] { return try await getUngroupedNotifications( - accountID: accountID, olderThan: maxID) + accountID: accountID, olderThan: maxID, newerThan: minID) } private func getGroupedNotifications( - withScope scope: APIService.MastodonNotificationScope, olderThan maxID: String? = nil + withScope scope: APIService.MastodonNotificationScope, olderThan maxID: String? = nil, newerThan minID: String? ) async throws -> Mastodon.Entity.GroupedNotificationsResults { guard let authenticationBox = AuthenticationServiceProvider.shared @@ -285,7 +286,7 @@ extension GroupedNotificationFeedLoader { else { throw APIService.APIError.implicit(.authenticationMissing) } let results = try await APIService.shared.groupedNotifications( - olderThan: maxID, fromAccount: nil, scope: scope, + olderThan: maxID, newerThan: minID, fromAccount: nil, scope: scope, authenticationBox: authenticationBox ) @@ -294,7 +295,7 @@ extension GroupedNotificationFeedLoader { private func getUngroupedNotifications( withScope scope: APIService.MastodonNotificationScope? = nil, - accountID: String? = nil, olderThan maxID: String? = nil + accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String? ) async throws -> [Mastodon.Entity.Notification] { assert(scope != nil || accountID != nil, "need a scope or an accountID") diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift index 25315a734..88be80376 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift @@ -97,7 +97,7 @@ class UngroupedNotificationCacheManager: NotificationsCacheManager { updatedMostRecentChunk = newlyFetched } if let staleResults { - let (dedupedNewer, stale) = dedupeAndCombine(newer: updatedMostRecentChunk, older: staleResults) + let (dedupedNewer, stale) = merge(newer: updatedMostRecentChunk, older: staleResults) mostRecentlyFetchedResults = Array(dedupedNewer) if stale == nil { self.staleResults = nil @@ -214,7 +214,7 @@ class GroupedNotificationCacheManager: NotificationsCacheManager { let allStatuses: [Mastodon.Entity.Status] if let staleResults { - let (dedupedNewer, dedupedStale) = dedupeAndCombine(newer: updatedNewerChunk, older: staleResults.notificationGroups) + let (dedupedNewer, dedupedStale) = merge(newer: updatedNewerChunk, older: staleResults.notificationGroups) truncatedGroups = truncate(notificationGroups: dedupedNewer) if dedupedStale == nil { // the lists were combined, so we don't have to keep track of the stale one anymore @@ -346,14 +346,14 @@ class GroupedNotificationCacheManager: NotificationsCacheManager { } } -fileprivate func dedupeAndCombine(newer: [T], older: [T]) -> ([T], [T]?) { +fileprivate func merge(newer: [T], older: [T], assumeOverlap: Bool = true) -> ([T], [T]?) { // There can be multiple matches between the older and newer feeds, with no guarantee of order. The newer version of a duplicate is always the one that should be used. - // Note that the check here is not fully sufficient to test for a gap between freshly fetched notifications and cached notifications (this check could miss a gap that was skipped over by a group that got promoted far enough up the list). + // Note that the check here is not fully sufficient to test for a gap between freshly fetched notifications and cached notifications (this check could miss a gap that was skipped over by a group that got promoted far enough up the list), which is why for now we fetch with a minID to avoid gaps and always assume there is an overlap. var dedupedNewer = [T]() var dedupedOlder = [T]() var alreadyAdded = Set() - var canCombine = false + var hasOverlap = false for element in newer { guard !alreadyAdded.contains(element.id) else { continue } @@ -362,12 +362,12 @@ fileprivate func dedupeAndCombine(newer: [T], older: [T]) -> ([ } for element in older { - guard !alreadyAdded.contains(element.id) else { canCombine = true; continue } + guard !alreadyAdded.contains(element.id) else { hasOverlap = true; continue } dedupedOlder.append(element) alreadyAdded.insert(element.id) } - if canCombine { + if hasOverlap || assumeOverlap { return (dedupedNewer + dedupedOlder, nil) } else { return (dedupedNewer, dedupedOlder) diff --git a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift index e7fbc1957..222832eac 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/MastodonFeedLoader.swift @@ -290,13 +290,13 @@ private extension MastodonFeedLoader { } } - private func _getGroupedNotifications(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil) async throws -> [MastodonFeedItemIdentifier] { + private func _getGroupedNotifications(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> [MastodonFeedItemIdentifier] { assert(scope != nil || accountID != nil, "need a scope or an accountID") guard let authenticationBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { throw APIService.APIError.implicit(.authenticationMissing) } - let results = try await APIService.shared.groupedNotifications(olderThan: maxID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) + let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) for account in results.accounts { MastodonFeedItemCacheManager.shared.addToCache(account) @@ -319,13 +319,13 @@ private extension MastodonFeedLoader { } } - private func _getGroupedNotificationResults(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil) async throws -> Mastodon.Entity.GroupedNotificationsResults { + private func _getGroupedNotificationResults(withScope scope: APIService.MastodonNotificationScope? = nil, accountID: String? = nil, olderThan maxID: String? = nil, newerThan minID: String?) async throws -> Mastodon.Entity.GroupedNotificationsResults { assert(scope != nil || accountID != nil, "need a scope or an accountID") guard let authenticationBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { throw APIService.APIError.implicit(.authenticationMissing) } - let results = try await APIService.shared.groupedNotifications(olderThan: maxID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) + let results = try await APIService.shared.groupedNotifications(olderThan: maxID, newerThan: minID, fromAccount: accountID, scope: scope, authenticationBox: authenticationBox) return results } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 432eb5bbe..4878fdc55 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -59,7 +59,8 @@ extension APIService { } public func groupedNotifications( - olderThan maxID: Mastodon.Entity.Status.ID?, + olderThan maxID: Mastodon.Entity.Notification.ID?, + newerThan minID: Mastodon.Entity.Notification.ID?, fromAccount accountID: String? = nil, scope: MastodonNotificationScope?, authenticationBox: MastodonAuthenticationBox @@ -83,6 +84,7 @@ extension APIService { let query = Mastodon.API.Notifications.GroupedQuery( maxID: maxID, + minID: minID, types: types, excludeTypes: excludedTypes, accountID: accountID