Implement cache for Notifications (IOS-200)
This commit is contained in:
parent
63e45d65f9
commit
a1cd1690fa
@ -60,7 +60,7 @@
|
|||||||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
|
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
|
||||||
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
|
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
|
||||||
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; };
|
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; };
|
||||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */; };
|
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; };
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||||
@ -697,7 +697,7 @@
|
|||||||
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||||
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = "<group>"; };
|
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = "<group>"; };
|
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = "<group>"; };
|
||||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+HomeTimeline.swift"; sourceTree = "<group>"; };
|
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = "<group>"; };
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||||
@ -1900,7 +1900,7 @@
|
|||||||
children = (
|
children = (
|
||||||
D8AC98772B0F62230045EC2B /* Model */,
|
D8AC98772B0F62230045EC2B /* Model */,
|
||||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
|
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
|
||||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */,
|
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */,
|
||||||
);
|
);
|
||||||
path = Persistence;
|
path = Persistence;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -3981,7 +3981,7 @@
|
|||||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||||
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
|
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
|
||||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
||||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */,
|
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */,
|
||||||
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
||||||
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
|
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
|
||||||
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */,
|
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */,
|
||||||
|
@ -626,7 +626,10 @@ extension SceneCoordinator: SettingsCoordinatorDelegate {
|
|||||||
try await self.appContext.authenticationService.signOutMastodonUser(
|
try await self.appContext.authenticationService.signOutMastodonUser(
|
||||||
authenticationBox: authContext.mastodonAuthenticationBox
|
authenticationBox: authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
FileManager.default.invalidateHomeTimelineCache(for: authContext.mastodonAuthenticationBox.userID)
|
let userId = authContext.mastodonAuthenticationBox.userID
|
||||||
|
FileManager.default.invalidateHomeTimelineCache(for: userId)
|
||||||
|
FileManager.default.invalidateNotificationsAll(for: userId)
|
||||||
|
FileManager.default.invalidateNotificationsMentions(for: userId)
|
||||||
self.setup()
|
self.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import MastodonCore
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
extension FileManager {
|
|
||||||
private static let cacheHomeItemsLimit: Int = 100 // max number of items to cache
|
|
||||||
|
|
||||||
func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] {
|
|
||||||
guard let cachesDirectory else { return [] }
|
|
||||||
|
|
||||||
let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory)
|
|
||||||
|
|
||||||
guard let data = try? Data(contentsOf: filePath) else { return [] }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let items = try JSONDecoder().decode([Mastodon.Entity.Status].self, from: data)
|
|
||||||
|
|
||||||
return items.map(MastodonStatus.fromEntity)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cacheHomeTimeline(items: [MastodonStatus], for userId: String) {
|
|
||||||
guard let cachesDirectory else { return }
|
|
||||||
|
|
||||||
let processableItems: [MastodonStatus]
|
|
||||||
if items.count > Self.cacheHomeItemsLimit {
|
|
||||||
processableItems = items.dropLast(items.count - Self.cacheHomeItemsLimit)
|
|
||||||
} else {
|
|
||||||
processableItems = items
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try JSONEncoder().encode(processableItems.map { $0.entity })
|
|
||||||
|
|
||||||
let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory)
|
|
||||||
try data.write(to: filePath)
|
|
||||||
} catch {
|
|
||||||
debugPrint(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidateHomeTimelineCache(for userId: String) {
|
|
||||||
guard let cachesDirectory else { return }
|
|
||||||
|
|
||||||
let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory)
|
|
||||||
|
|
||||||
try? removeItem(at: filePath)
|
|
||||||
}
|
|
||||||
}
|
|
93
Mastodon/Persistence/FileManager+Timeline.swift
Normal file
93
Mastodon/Persistence/FileManager+Timeline.swift
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonCore
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
private static let cacheItemsLimit: Int = 100 // max number of items to cache
|
||||||
|
|
||||||
|
// Retrieve
|
||||||
|
func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] {
|
||||||
|
try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachedNotificationsAll(for userId: String) throws -> [Mastodon.Entity.Notification] {
|
||||||
|
try cached(timeline: .notificationsAll(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachedNotificationsMentions(for userId: String) throws -> [Mastodon.Entity.Notification] {
|
||||||
|
try cached(timeline: .notificationsMentions(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func cached<T: Decodable>(timeline: Persistence) throws -> [T] {
|
||||||
|
guard let cachesDirectory else { return [] }
|
||||||
|
|
||||||
|
let filePath = timeline.filepath(baseURL: cachesDirectory)
|
||||||
|
|
||||||
|
guard let data = try? Data(contentsOf: filePath) else { return [] }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let items = try JSONDecoder().decode([T].self, from: data)
|
||||||
|
|
||||||
|
return items
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
func cacheHomeTimeline(items: [MastodonStatus], for userId: String) {
|
||||||
|
cache(items.map { $0.entity }, timeline: .homeTimeline(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userId: String) {
|
||||||
|
cache(items, timeline: .notificationsAll(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userId: String) {
|
||||||
|
cache(items, timeline: .notificationsMentions(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cache<T: Encodable>(_ items: [T], timeline: Persistence) {
|
||||||
|
guard let cachesDirectory else { return }
|
||||||
|
|
||||||
|
let processableItems: [T]
|
||||||
|
if items.count > Self.cacheItemsLimit {
|
||||||
|
processableItems = items.dropLast(items.count - Self.cacheItemsLimit)
|
||||||
|
} else {
|
||||||
|
processableItems = items
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try JSONEncoder().encode(processableItems)
|
||||||
|
|
||||||
|
let filePath = timeline.filepath(baseURL: cachesDirectory)
|
||||||
|
try data.write(to: filePath)
|
||||||
|
} catch {
|
||||||
|
debugPrint(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func invalidateHomeTimelineCache(for userId: String) {
|
||||||
|
invalidate(timeline: .homeTimeline(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidateNotificationsAll(for userId: String) {
|
||||||
|
invalidate(timeline: .notificationsAll(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidateNotificationsMentions(for userId: String) {
|
||||||
|
invalidate(timeline: .notificationsMentions(userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func invalidate(timeline: Persistence) {
|
||||||
|
guard let cachesDirectory else { return }
|
||||||
|
|
||||||
|
let filePath = timeline.filepath(baseURL: cachesDirectory)
|
||||||
|
|
||||||
|
try? removeItem(at: filePath)
|
||||||
|
}
|
||||||
|
}
|
@ -386,7 +386,10 @@ extension HomeTimelineViewController {
|
|||||||
@objc func signOutAction(_ sender: UIAction) {
|
@objc func signOutAction(_ sender: UIAction) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||||
FileManager.default.invalidateHomeTimelineCache(for: viewModel.authContext.mastodonAuthenticationBox.userID)
|
let userId = viewModel.authContext.mastodonAuthenticationBox.userID
|
||||||
|
FileManager.default.invalidateHomeTimelineCache(for: userId)
|
||||||
|
FileManager.default.invalidateNotificationsAll(for: userId)
|
||||||
|
FileManager.default.invalidateNotificationsMentions(for: userId)
|
||||||
self.coordinator.setup()
|
self.coordinator.setup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,34 @@ final class NotificationTimelineViewModel {
|
|||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
self.feedFetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext)
|
self.feedFetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext)
|
||||||
|
|
||||||
|
switch scope {
|
||||||
|
case .everything:
|
||||||
|
self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox.userID))?.map({ notification in
|
||||||
|
MastodonFeed.fromNotification(notification, kind: .notificationAll)
|
||||||
|
}) ?? []
|
||||||
|
case .mentions:
|
||||||
|
self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox.userID))?.map({ notification in
|
||||||
|
MastodonFeed.fromNotification(notification, kind: .notificationMentions)
|
||||||
|
}) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
self.feedFetchedResultsController.$records
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink(receiveValue: { feeds in
|
||||||
|
let items: [Mastodon.Entity.Notification] = feeds.compactMap { feed -> Mastodon.Entity.Notification? in
|
||||||
|
guard let status = feed.notification else { return nil }
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
switch self.scope {
|
||||||
|
case .everything:
|
||||||
|
FileManager.default.cacheNotificationsAll(items: items, for: authContext.mastodonAuthenticationBox.userID)
|
||||||
|
case .mentions:
|
||||||
|
FileManager.default.cacheNotificationsMentions(items: items, for: authContext.mastodonAuthenticationBox.userID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import Foundation
|
|||||||
public enum Persistence {
|
public enum Persistence {
|
||||||
case searchHistory
|
case searchHistory
|
||||||
case homeTimeline(String)
|
case homeTimeline(String)
|
||||||
|
case notificationsMentions(String)
|
||||||
|
case notificationsAll(String)
|
||||||
|
|
||||||
private var filename: String {
|
private var filename: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -18,6 +20,10 @@ public enum Persistence {
|
|||||||
return "search_history"
|
return "search_history"
|
||||||
case let .homeTimeline(userId):
|
case let .homeTimeline(userId):
|
||||||
return "home_timeline_\(userId)"
|
return "home_timeline_\(userId)"
|
||||||
|
case let .notificationsMentions(userId):
|
||||||
|
return "notifications_mentions_\(userId)"
|
||||||
|
case let .notificationsAll(userId):
|
||||||
|
return "notifications_all_\(userId)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user