diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index edf420e70..49419d97f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -626,9 +626,7 @@ extension SceneCoordinator: SettingsCoordinatorDelegate { authentication: authenticationBox.authentication ) let userIdentifier = authenticationBox - FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) - FileManager.default.invalidateNotificationsAll(for: userIdentifier) - FileManager.default.invalidateNotificationsMentions(for: userIdentifier) + PersistenceManager.shared.removeAllCaches(forUser: userIdentifier) self.setup() } diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift index c79ee79a8..03df27f66 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationListViewController.swift @@ -422,7 +422,7 @@ private class NotificationListViewModel: ObservableObject { var updatedItems = records.allRecords.map { NotificationListItem.groupedNotification($0) } - if records.canLoadOlder { + if !records.allRecords.isEmpty && records.canLoadOlder { updatedItems.append(.bottomLoader) } updatedItems = self.notificationPolicyBannerRow + updatedItems diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift index 3f5f30dc2..e589992c5 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationsCacheManager.swift @@ -26,8 +26,6 @@ class UngroupedNotificationCacheManager: NotificationsCacheManager { typealias T = [Mastodon.Entity.Notification] private let userIdentifier: MastodonUserIdentifier private let feedKind: MastodonFeedKind - private let lastReadMarkerStore: Store - private let cachedNotifications: Store private var staleResults: T? private var staleMarkers: LastReadMarkers? @@ -38,8 +36,6 @@ class UngroupedNotificationCacheManager: NotificationsCacheManager { init(feedKind: MastodonFeedKind, userIdentifier: MastodonUserIdentifier) { self.feedKind = feedKind self.userIdentifier = userIdentifier - lastReadMarkerStore = Store.lastReadMarkersStore() - self.cachedNotifications = Store.ungroupedNotificationStore(forKind: feedKind, forUser: userIdentifier) staleResults = nil staleMarkers = nil self.mostRecentlyFetchedResults = nil @@ -49,17 +45,25 @@ class UngroupedNotificationCacheManager: NotificationsCacheManager { func currentResults() async -> T? { if let mostRecentlyFetchedResults { return mostRecentlyFetchedResults + } else if let staleResults { + return staleResults } else { do { switch feedKind { case .notificationsAll, .notificationsMentionsOnly: - try await lastReadMarkerStore.itemsHaveLoaded() - self.staleMarkers = lastReadMarkerStore.items.first(where: { $0.userGUID == userIdentifier.globallyUniqueUserIdentifier }) + let cachedMarkers: [LastReadMarkers] = try PersistenceManager.shared.cached(.lastReadMarkers(userIdentifier)) + self.staleMarkers = cachedMarkers.first(where: { $0.userGUID == userIdentifier.globallyUniqueUserIdentifier }) case .notificationsWithAccount: self.staleMarkers = nil } - try await cachedNotifications.itemsHaveLoaded() - staleResults = cachedNotifications.items + switch feedKind { + case .notificationsAll: + staleResults = try PersistenceManager.shared.cached(.notificationsAll(userIdentifier)) + case .notificationsMentionsOnly: + staleResults = try PersistenceManager.shared.cached(.notificationsMentions(userIdentifier)) + case .notificationsWithAccount(let string): + staleResults = nil + } } catch { assertionFailure("error reading notifications cache: \(error)") } @@ -116,15 +120,17 @@ class UngroupedNotificationCacheManager: NotificationsCacheManager { func commitToCache() async { if let mostRecentMarkers { - try? await lastReadMarkerStore.insert(mostRecentMarkers) + PersistenceManager.shared.cache([mostRecentMarkers], for: .lastReadMarkers(userIdentifier)) } if let mostRecentlyFetchedResults { - try? await cachedNotifications - .removeAll() - .insert(mostRecentlyFetchedResults) - .run() - } else { - try? await cachedNotifications.removeAll() + switch feedKind { + case .notificationsAll: + PersistenceManager.shared.cache(mostRecentlyFetchedResults, for: .notificationsAll(userIdentifier)) + case .notificationsMentionsOnly: + PersistenceManager.shared.cache(mostRecentlyFetchedResults, for: .notificationsMentions(userIdentifier)) + case .notificationsWithAccount: + break + } } } } @@ -144,22 +150,10 @@ class GroupedNotificationCacheManager: NotificationsCacheManager { internal var mostRecentlyFetchedResults: T? private var mostRecentMarkers: LastReadMarkers? - private let lastReadMarkerStore: Store - private let notificationGroupStore: Store - private let fullAccountStore: Store - private let partialAccountStore: Store - private let statusStore: Store - init(feedKind: MastodonFeedKind, userIdentifier: MastodonUserIdentifier) { self.feedKind = feedKind self.userIdentifier = userIdentifier - - lastReadMarkerStore = Store.lastReadMarkersStore() - notificationGroupStore = Store.notificationGroupStore(forKind: feedKind, forUser: userIdentifier) - fullAccountStore = Store.notificationRelevantFullAccountStore(forKind: feedKind, forUser: userIdentifier) - partialAccountStore = Store.notificationRelevantPartialAccountStore(forKind: feedKind, forUser: userIdentifier) - statusStore = Store.notificationRelevantStatusStore(forKind: feedKind, forUser: userIdentifier) staleMarkers = nil staleResults = nil } @@ -271,18 +265,44 @@ class GroupedNotificationCacheManager: NotificationsCacheManager { } func currentResults() async -> T? { - do { - try await lastReadMarkerStore.itemsHaveLoaded() - try await notificationGroupStore.itemsHaveLoaded() - try await fullAccountStore.itemsHaveLoaded() - try await partialAccountStore.itemsHaveLoaded() - try await statusStore.itemsHaveLoaded() - staleMarkers = lastReadMarkerStore.items.first(where: { $0.userGUID == userIdentifier.globallyUniqueUserIdentifier }) - staleResults = Mastodon.Entity.GroupedNotificationsResults(notificationGroups: notificationGroupStore.items, fullAccounts: fullAccountStore.items, partialAccounts: partialAccountStore.items, statuses: statusStore.items) - } catch { - assertionFailure("error loading notifications caches: \(error)") + if let mostRecentlyFetchedResults { + return mostRecentlyFetchedResults + } else if let staleResults { + return staleResults + } else { + do { + switch feedKind { + case .notificationsAll, .notificationsMentionsOnly: + let cachedMarkers: [LastReadMarkers] = try PersistenceManager.shared.cached(.lastReadMarkers(userIdentifier)) + self.staleMarkers = cachedMarkers.first(where: { $0.userGUID == userIdentifier.globallyUniqueUserIdentifier }) + case .notificationsWithAccount: + self.staleMarkers = nil + } + + let notificationGroups: [Mastodon.Entity.NotificationGroup] + let accounts: [Mastodon.Entity.Account] + let partialAccounts: [Mastodon.Entity.PartialAccountWithAvatar] + let statuses: [Mastodon.Entity.Status] + switch feedKind { + case .notificationsAll: + notificationGroups = (try? PersistenceManager.shared.cached(.groupedNotificationsAll(userIdentifier))) ?? [] + accounts = (try? PersistenceManager.shared.cached(.groupedNotificationsAllAccounts(userIdentifier))) ?? [] + partialAccounts = (try? PersistenceManager.shared.cached(.groupedNotificationsAllPartialAccounts(userIdentifier))) ?? [] + statuses = (try? PersistenceManager.shared.cached(.groupedNotificationsAllStatuses(userIdentifier))) ?? [] + case .notificationsMentionsOnly: + notificationGroups = (try? PersistenceManager.shared.cached(.groupedNotificationsMentions(userIdentifier))) ?? [] + accounts = (try? PersistenceManager.shared.cached(.groupedNotificationsMentionsAccounts(userIdentifier))) ?? [] + partialAccounts = (try? PersistenceManager.shared.cached(.groupedNotificationsMentionsPartialAccounts(userIdentifier))) ?? [] + statuses = (try? PersistenceManager.shared.cached(.groupedNotificationsMentionsStatuses(userIdentifier))) ?? [] + case .notificationsWithAccount(let string): + return mostRecentlyFetchedResults + } + staleResults = Mastodon.Entity.GroupedNotificationsResults(notificationGroups: notificationGroups, fullAccounts: accounts, partialAccounts: partialAccounts, statuses: statuses) + } catch { + assertionFailure("error reading notifications cache: \(error)") + } + return mostRecentlyFetchedResults ?? staleResults } - return mostRecentlyFetchedResults ?? staleResults } var currentLastReadMarker: LastReadMarkers.MarkerPosition? { @@ -296,31 +316,22 @@ class GroupedNotificationCacheManager: NotificationsCacheManager { func commitToCache() async { if let mostRecentMarkers { - do { - try await lastReadMarkerStore.insert(mostRecentMarkers) - } catch { - } + PersistenceManager.shared.cache([mostRecentMarkers], for: .lastReadMarkers(userIdentifier)) } if let mostRecentlyFetchedResults { - do { - try await notificationGroupStore - .removeAll() - .insert(mostRecentlyFetchedResults.notificationGroups) - .run() - try await fullAccountStore - .removeAll() - .insert(mostRecentlyFetchedResults.accounts) - .run() - try await partialAccountStore - .removeAll() - .insert(mostRecentlyFetchedResults.partialAccounts ?? []) - .run() - try await statusStore - .removeAll() - .insert(mostRecentlyFetchedResults.statuses) - .run() - } catch { - assertionFailure("error comitting to store \(error)") + switch feedKind { + case .notificationsAll: + PersistenceManager.shared.cache(mostRecentlyFetchedResults.notificationGroups, for: .groupedNotificationsAll(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.accounts, for: .groupedNotificationsAllAccounts(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.partialAccounts ?? [], for: .groupedNotificationsAllPartialAccounts(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.statuses, for: .groupedNotificationsAllStatuses(userIdentifier)) + case .notificationsMentionsOnly: + PersistenceManager.shared.cache(mostRecentlyFetchedResults.notificationGroups, for: .groupedNotificationsMentions(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.accounts, for: .groupedNotificationsMentionsAccounts(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.partialAccounts ?? [], for: .groupedNotificationsMentionsPartialAccounts(userIdentifier)) + PersistenceManager.shared.cache(mostRecentlyFetchedResults.statuses, for: .groupedNotificationsMentionsStatuses(userIdentifier)) + case .notificationsWithAccount: + break } } } diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 13b033ef1..f9ff8198d 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -106,12 +106,10 @@ extension AccountListViewController: UITableViewDelegate { Task { @MainActor in do { - try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: record) - let userIdentifier = record - FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) - FileManager.default.invalidateNotificationsAll(for: userIdentifier) - FileManager.default.invalidateNotificationsMentions(for: userIdentifier) + try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: record) + PersistenceManager.shared.removeAllCaches(forUser: userIdentifier) + self.sceneCoordinator?.setup() } catch { @@ -156,6 +154,9 @@ extension AccountListViewController: UITableViewDelegate { self.sceneCoordinator?.showLoading() for authenticationBox in AuthenticationServiceProvider.shared.mastodonAuthenticationBoxes { try? await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: authenticationBox.authentication) + let userIdentifier = authenticationBox.authentication.userIdentifier() + PersistenceManager.shared.removeAllCaches(forUser: userIdentifier) + self.sceneCoordinator?.setup() } self.sceneCoordinator?.hideLoading() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index d6ab6904c..0f7c27d38 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -651,9 +651,8 @@ extension HomeTimelineViewController { Task { @MainActor in try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: authenticationBox.authentication) let userIdentifier = authenticationBox - FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) - FileManager.default.invalidateNotificationsAll(for: userIdentifier) - FileManager.default.invalidateNotificationsMentions(for: userIdentifier) + PersistenceManager.shared.removeAllCaches(forUser: userIdentifier) + self.sceneCoordinator?.setup() self.sceneCoordinator?.setup() } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index aebfff6ed..d6389c0f3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -92,8 +92,8 @@ final class HomeTimelineViewModel: NSObject { self.authenticationBox = authenticationBox self.dataController = FeedDataController(authenticationBox: authenticationBox, kind: .home(timeline: timelineContext)) super.init() - let initialRecords = (try? PersistenceManager.shared.cachedTimeline(.homeTimeline(authenticationBox)).map { - MastodonFeed.fromStatus($0, kind: .home) + let initialRecords = (try? PersistenceManager.shared.cached(.homeTimeline(authenticationBox)).map { + MastodonFeed.fromStatus(MastodonStatus.fromEntity($0), kind: .home) }) ?? [] Task { await self.dataController.setRecordsAfterFiltering(initialRecords) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift index 0dee5bf0c..af531257e 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift @@ -30,19 +30,6 @@ public extension FileManager { func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { cache(items, timeline: .notificationsMentions(userIdentifier)) } - - // Delete - func invalidateHomeTimelineCache(for userId: UserIdentifier) { - invalidate(timeline: .homeTimeline(userId)) - } - - func invalidateNotificationsAll(for userId: UserIdentifier) { - invalidate(timeline: .notificationsAll(userId)) - } - - func invalidateNotificationsMentions(for userId: UserIdentifier) { - invalidate(timeline: .notificationsMentions(userId)) - } } private extension FileManager { diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index e53b66a3a..c88b1940c 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -13,20 +13,47 @@ public enum Persistence { case homeTimeline(UserIdentifier) case notificationsMentions(UserIdentifier) case notificationsAll(UserIdentifier) + case groupedNotificationsMentions(UserIdentifier) + case groupedNotificationsMentionsAccounts(UserIdentifier) + case groupedNotificationsMentionsPartialAccounts(UserIdentifier) + case groupedNotificationsMentionsStatuses(UserIdentifier) + case groupedNotificationsAll(UserIdentifier) + case groupedNotificationsAllAccounts(UserIdentifier) + case groupedNotificationsAllPartialAccounts(UserIdentifier) + case groupedNotificationsAllStatuses(UserIdentifier) + case lastReadMarkers(UserIdentifier) case accounts(UserIdentifier) private var filename: String { switch self { - case .searchHistory(let userIdentifier): - return "search_history_\(userIdentifier.globallyUniqueUserIdentifier))" - case let .homeTimeline(userIdentifier): - return "home_timeline_\(userIdentifier.globallyUniqueUserIdentifier)" - case let .notificationsMentions(userIdentifier): - return "notifications_mentions_\(userIdentifier.globallyUniqueUserIdentifier)" - case let .notificationsAll(userIdentifier): - return "notifications_all_\(userIdentifier.globallyUniqueUserIdentifier)" - case .accounts(let userIdentifier): - return "account_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .searchHistory(userIdentifier): + return "search_history_\(userIdentifier.globallyUniqueUserIdentifier))" + case let .homeTimeline(userIdentifier): + return "home_timeline_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .notificationsMentions(userIdentifier): + return "notifications_mentions_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .notificationsAll(userIdentifier): + return "notifications_all_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsMentions(userIdentifier): + return "grouped_notifications_mentions_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsMentionsStatuses(userIdentifier): + return "grouped_notifications_mentions_relevant_statuses_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsMentionsAccounts(userIdentifier): + return "grouped_notifications_mentions_relevant_accounts_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsMentionsPartialAccounts(userIdentifier): + return "grouped_notifications_mentions_relevant_partialAccounts_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsAll(userIdentifier): + return "grouped_notifications_all_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsAllStatuses(userIdentifier): + return "grouped_notifications_all_relevant_statuses_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsAllAccounts(userIdentifier): + return "grouped_notifications_all_relevant_accounts_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .groupedNotificationsAllPartialAccounts(userIdentifier): + return "grouped_notifications_all_relevant_partialAccounts_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .lastReadMarkers(userIdentifier): + return "last_read_markers_\(userIdentifier.globallyUniqueUserIdentifier)" + case let .accounts(userIdentifier): + return "account_\(userIdentifier.globallyUniqueUserIdentifier)" } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/PersistenceManager.swift b/MastodonSDK/Sources/MastodonCore/Persistence/PersistenceManager.swift index 55bb5c08d..a6faf74d1 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/PersistenceManager.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/PersistenceManager.swift @@ -42,10 +42,6 @@ public class PersistenceManager { return coreDataStack.newTaskContext() } - public func cachedTimeline(_ timeline: Persistence) throws -> [MastodonStatus] { - return try FileManager.default.cached(timeline: timeline).map(MastodonStatus.fromEntity) - } - public func cachedAccount(for authentication: MastodonAuthentication) -> Mastodon.Entity.Account? { let account = FileManager .default @@ -57,15 +53,38 @@ public class PersistenceManager { public func cacheAccount(_ account: Mastodon.Entity.Account, forUserID userID: MastodonUserIdentifier) { FileManager.default.store(account: account, forUserID: userID) } + + public func cached(_ cacheType: Persistence) throws -> [T] { + return try FileManager.default.cached(cacheType) + } + + public func cache(_ items: [T], for cacheType: Persistence) { + FileManager.default.cache(items, for: cacheType) + } + + public func removeAllCaches(forUser user: UserIdentifier) { + FileManager.default.invalidate(cache: .accounts(user)) + FileManager.default.invalidate(cache: .groupedNotificationsAll(user)) + FileManager.default.invalidate(cache: .groupedNotificationsAllAccounts(user)) + FileManager.default.invalidate(cache: .groupedNotificationsAllPartialAccounts(user)) + FileManager.default.invalidate(cache: .groupedNotificationsAllStatuses(user)) + FileManager.default.invalidate(cache: .groupedNotificationsMentions(user)) + FileManager.default.invalidate(cache: .groupedNotificationsMentionsAccounts(user)) + FileManager.default.invalidate(cache: .groupedNotificationsMentionsPartialAccounts(user)) + FileManager.default.invalidate(cache: .groupedNotificationsMentionsStatuses(user)) + FileManager.default.invalidate(cache: .homeTimeline(user)) + FileManager.default.invalidate(cache: .searchHistory(user)) + FileManager.default.invalidate(cache: .notificationsAll(user)) + } } private extension FileManager { static let cacheItemsLimit: Int = 100 // max number of items to cache - func cached(timeline: Persistence) throws -> [T] { + func cached(_ cacheType: Persistence) throws -> [T] { guard let cachesDirectory else { return [] } - let filePath = timeline.filepath(baseURL: cachesDirectory) + let filePath = cacheType.filepath(baseURL: cachesDirectory) guard let data = try? Data(contentsOf: filePath) else { return [] } @@ -78,8 +97,7 @@ private extension FileManager { } } - - func cache(_ items: [T], timeline: Persistence) { + func cache(_ items: [T], for cacheType: Persistence) { guard let cachesDirectory else { return } let processableItems: [T] @@ -92,17 +110,17 @@ private extension FileManager { do { let data = try JSONEncoder().encode(processableItems) - let filePath = timeline.filepath(baseURL: cachesDirectory) + let filePath = cacheType.filepath(baseURL: cachesDirectory) try data.write(to: filePath) } catch { debugPrint(error.localizedDescription) } } - func invalidate(timeline: Persistence) { + func invalidate(cache: Persistence) { guard let cachesDirectory else { return } - let filePath = timeline.filepath(baseURL: cachesDirectory) + let filePath = cache.filepath(baseURL: cachesDirectory) try? removeItem(at: filePath) }