Add JSON-based account-persistence (IOS-192)

This is per user.

Also: Fetch authenticated accounts regularly
Also: Move Persistence-stuff to MastodonCore because.
This commit is contained in:
Nathan Mattes 2023-12-13 15:07:16 +01:00
parent d3c7ba2c7c
commit 60aafe6330
20 changed files with 172 additions and 153 deletions

View File

@ -60,7 +60,6 @@
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.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 */; };
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; };
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
@ -162,8 +161,6 @@
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */; };
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98782B0F622B0045EC2B /* SearchHistory.swift */; };
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
@ -694,7 +691,6 @@
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>"; };
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.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>"; };
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>"; };
@ -833,8 +829,6 @@
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = "<group>"; };
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = "<group>"; };
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = "<group>"; };
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
@ -1892,21 +1886,10 @@
D8AC98742B0F615E0045EC2B /* Persistence */ = {
isa = PBXGroup;
children = (
D8AC98772B0F62230045EC2B /* Model */,
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */,
);
path = Persistence;
sourceTree = "<group>";
};
D8AC98772B0F62230045EC2B /* Model */ = {
isa = PBXGroup;
children = (
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */,
);
path = Model;
sourceTree = "<group>";
};
D8E5C347296DB896007E76A7 /* Edit History */ = {
isa = PBXGroup;
children = (
@ -3856,7 +3839,6 @@
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */,
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */,
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
@ -3864,7 +3846,6 @@
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */,
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */,
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,
@ -3969,7 +3950,6 @@
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */,
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */,

View File

@ -5,8 +5,6 @@
// Created by Marcus Kida on 17.11.22.
//
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK

View File

@ -1,25 +0,0 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonCore
import MastodonSDK
extension Persistence.SearchHistory {
struct Item: Codable, Hashable, Equatable {
let updatedAt: Date
let userID: Mastodon.Entity.Account.ID
let account: Mastodon.Entity.Account?
let hashtag: Mastodon.Entity.Tag?
func hash(into hasher: inout Hasher) {
hasher.combine(userID)
hasher.combine(account)
hasher.combine(hashtag)
}
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
}
}
}

View File

@ -39,21 +39,19 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
break
case .reply:
guard let replyToAccountID = status.entity.inReplyToAccountID else { return }
#warning("TODO: Implement Domain")
await DataSourceFacade.coordinateToProfileScene(provider: self,
domain: "",
accountID: replyToAccountID)
await DataSourceFacade.coordinateToProfileScene(provider: self,
domain: domain,
accountID: replyToAccountID)
case .repost:
await DataSourceFacade.coordinateToProfileScene(
provider: self,
target: .reblog, // keep the wrapper for header author
status: status
)
case .repost:
await DataSourceFacade.coordinateToProfileScene(
provider: self,
target: .reblog, // keep the wrapper for header author
status: status
)
}
}
}
}
// MARK: - avatar button

View File

@ -92,6 +92,7 @@ extension HomeTimelineViewModel.LoadLatestState {
}
do {
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
let response = try await viewModel.context.apiService.homeTimeline(
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)

View File

@ -8,6 +8,7 @@
import Foundation
import GameplayKit
import MastodonSDK
import MastodonCore
extension HomeTimelineViewModel {
class LoadOldestState: GKState {
@ -60,6 +61,8 @@ extension HomeTimelineViewModel.LoadOldestState {
}
do {
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
let response = try await viewModel.context.apiService.homeTimeline(
maxID: maxID,
authenticationBox: viewModel.authContext.mastodonAuthenticationBox

View File

@ -147,7 +147,9 @@ extension HomeTimelineViewModel {
// reconfigure item
snapshot.reconfigureItems([item])
await updateSnapshotUsingReloadData(snapshot: snapshot)
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
// fetch data
let maxID = status.id
_ = try? await context.apiService.homeTimeline(

View File

@ -89,7 +89,7 @@ class MainTabBarController: UITabBarController {
@MainActor
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController {
guard let authContext = authContext else {
guard let authContext, let me = authContext.mastodonAuthenticationBox.authentication.account() else {
return UITableViewController()
}
@ -116,7 +116,6 @@ class MainTabBarController: UITabBarController {
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .me:
let me = authContext.mastodonAuthenticationBox.authentication.account()
let _viewController = ProfileViewController()
_viewController.context = context
_viewController.coordinator = coordinator
@ -133,7 +132,6 @@ class MainTabBarController: UITabBarController {
private(set) var isReadyForWizardAvatarButton = false
// output
var avatarURLObserver: AnyCancellable?
@Published var avatarURL: URL?
// haptic feedback
@ -268,28 +266,20 @@ extension MainTabBarController {
NotificationCenter.default.publisher(for: .userFetched)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
self.avatarURLObserver = user.publisher(for: \.avatar)
.sink { [weak self, weak user] _ in
guard let self = self else { return }
guard let user = user else { return }
guard user.managedObjectContext != nil else { return }
self.avatarURL = user.avatarImageURL()
}
guard let self else { return }
if let account = self.authContext?.mastodonAuthenticationBox.authentication.account() {
self.avatarURL = account.avatarImageURL()
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback)
self.context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in
self?.updateUserAccount()
}
.store(in: &self.disposeBag)
} else {
self.avatarURLObserver = nil
}
}
.store(in: &disposeBag)

View File

@ -122,8 +122,6 @@ public class AppContext: ObservableObject {
}
.store(in: &disposeBag)
}
}
extension AppContext {

View File

@ -54,21 +54,21 @@ public extension AuthenticationServiceProvider {
func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? {
authentications.first(where: { $0.userAccessToken == userAccessToken })
}
func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
}
func restore() {
authentications = Self.keychain.allKeys().compactMap {
guard
let encoded = Self.keychain[$0],
let data = Data(base64Encoded: encoded)
let data = Data(base64Encoded: encoded)
else { return nil }
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
}
}
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
do {
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
@ -101,10 +101,29 @@ public extension AuthenticationServiceProvider {
logger.log(level: .error, "Could not migrate legacy authentications")
}
}
var authenticationMigrationRequired: Bool {
userDefaults.didMigrateAuthentications == false
}
func fetchAccounts(apiService: APIService, completion: (() -> Void)? = nil) async {
// FIXME: This is a dirty hack to make the performance-stuff work.
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
// it when we need it to display on the home timeline.
// We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate
for authentication in authentications {
guard let account = try? await apiService.accountInfo(domain: authentication.domain,
userID: authentication.userID,
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value else { continue }
FileManager.default.store(account: account, forUserID: authentication.userID)
}
NotificationCenter.default.post(name: .userFetched, object: nil)
completion?()
}
}
// MARK: - Private

View File

@ -10,6 +10,7 @@ import Foundation
import UIKit
import Combine
import MastodonSDK
import MastodonCore
final public class FeedFetchedResultsController {
@ -80,15 +81,16 @@ final public class FeedFetchedResultsController {
private extension FeedFetchedResultsController {
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
switch kind {
case .home:
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationAll) }
case .notificationMentions:
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationMentions) }
case .home:
await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService)
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationAll) }
case .notificationMentions:
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationMentions) }
}
}
}

View File

@ -99,10 +99,10 @@ public struct MastodonAuthentication: Codable, Hashable {
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
public func account() -> Mastodon.Entity.Account {
// store accounts
#warning("TODO: Implement")
return Mastodon.Entity.Account.placeholder()
public func account() -> Mastodon.Entity.Account? {
let account = FileManager.default.accounts(forUserID: userID).first(where: { $0.id == userID })
return account
}
func updating(instance: Instance) -> Self {

View File

@ -0,0 +1,55 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonSDK
extension FileManager {
public func store(account: Mastodon.Entity.Account, forUserID userID: String) {
// store accounts for each loged in user
var accounts = accounts(forUserID: userID)
if let index = accounts.firstIndex(of: account) {
accounts.remove(at: index)
}
accounts.append(account)
storeJSON(accounts, userID: userID)
}
public func accounts(forUserID userID: String) -> [Mastodon.Entity.Account] {
guard let documentsDirectory else { return [] }
let accountPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory)
guard let data = try? Data(contentsOf: accountPath) else { return [] }
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601
do {
let accounts = try jsonDecoder.decode([Mastodon.Entity.Account].self, from: data)
return accounts
} catch {
return []
}
}
private func storeJSON(_ encodable: Encodable, userID: String) {
guard let documentsDirectory else { return }
let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .iso8601
do {
let data = try jsonEncoder.encode(encodable)
let accountsPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory)
try data.write(to: accountsPath)
} catch {
debugPrint(error.localizedDescription)
}
}
}

View File

@ -4,11 +4,11 @@ import Foundation
import MastodonCore
extension FileManager {
func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
public func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
return try searchItems().filter { $0.userID == userID }
}
func searchItems() throws -> [Persistence.SearchHistory.Item] {
public func searchItems() throws -> [Persistence.SearchHistory.Item] {
guard let documentsDirectory else { return [] }
let searchHistoryPath = Persistence.searchHistory.filepath(baseURL: documentsDirectory)
@ -28,9 +28,7 @@ extension FileManager {
}
}
func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
guard let documentsDirectory else { return }
public func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
var searchItems = (try? searchItems()) ?? []
if let index = searchItems.firstIndex(of: newSearchItem) {
@ -58,10 +56,8 @@ extension FileManager {
}
func removeSearchHistory(forUser userID: String) {
guard let documentsDirectory else { return }
var searchItems = (try? searchItems()) ?? []
public func removeSearchHistory(forUser userID: String) {
let searchItems = (try? searchItems()) ?? []
let newSearchItems = searchItems.filter { $0.userID != userID }
storeJSON(newSearchItems, .searchHistory)

View File

@ -1,22 +1,21 @@
// 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: UserIdentifier) throws -> [MastodonStatus] {
public func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] {
try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity)
}
func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
public func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
try cached(timeline: .notificationsAll(userId))
}
func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
public func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
try cached(timeline: .notificationsMentions(userId))
}
@ -38,15 +37,15 @@ extension FileManager {
}
// Create
func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) {
public func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) {
cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier))
}
func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
public func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
cache(items, timeline: .notificationsAll(userIdentifier))
}
func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
public func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
cache(items, timeline: .notificationsMentions(userIdentifier))
}
@ -71,15 +70,15 @@ extension FileManager {
}
// Delete
func invalidateHomeTimelineCache(for userId: UserIdentifier) {
public func invalidateHomeTimelineCache(for userId: UserIdentifier) {
invalidate(timeline: .homeTimeline(userId))
}
func invalidateNotificationsAll(for userId: UserIdentifier) {
public func invalidateNotificationsAll(for userId: UserIdentifier) {
invalidate(timeline: .notificationsAll(userId))
}
func invalidateNotificationsMentions(for userId: UserIdentifier) {
public func invalidateNotificationsMentions(for userId: UserIdentifier) {
invalidate(timeline: .notificationsMentions(userId))
}

View File

@ -0,0 +1,31 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonSDK
extension Persistence.SearchHistory {
public struct Item: Codable, Hashable, Equatable {
public let updatedAt: Date
public let userID: Mastodon.Entity.Account.ID
public let account: Mastodon.Entity.Account?
public let hashtag: Mastodon.Entity.Tag?
public init(updatedAt: Date, userID: Mastodon.Entity.Account.ID, account: Mastodon.Entity.Account?, hashtag: Mastodon.Entity.Tag?) {
self.updatedAt = updatedAt
self.userID = userID
self.account = account
self.hashtag = hashtag
}
public func hash(into hasher: inout Hasher) {
hasher.combine(userID)
hasher.combine(account)
hasher.combine(hashtag)
}
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
}
}
}

View File

@ -13,7 +13,12 @@ public enum Persistence {
case homeTimeline(UserIdentifier)
case notificationsMentions(UserIdentifier)
case notificationsAll(UserIdentifier)
case accounts(userID: String)
private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String {
"\(userIdentifier.userID)@\(userIdentifier.domain)"
}
private var filename: String {
switch self {
case .searchHistory:
@ -23,7 +28,9 @@ public enum Persistence {
case let .notificationsMentions(userIdentifier):
return "notifications_mentions_\(userIdentifier.uniqueUserDomainIdentifier)"
case let .notificationsAll(userIdentifier):
return "notifications_all_\(userIdentifier.uniqueUserDomainIdentifier)"
return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))"
case .accounts(let userID):
return "account_\(userID)"
}
}

View File

@ -34,29 +34,6 @@ extension APIService {
authorization: authorization
).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
for entity in response.value {
let result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: entity,
cache: nil,
networkDate: response.networkDate
)
)
if let me = me {
let user = result.user
user.update(isFollowing: true, by: me)
}
}
}
return response
}

View File

@ -33,7 +33,7 @@ extension APIService {
limit: limit,
local: local
)
let response = try await Mastodon.API.Timeline.home(
session: session,
domain: domain,
@ -54,18 +54,6 @@ extension APIService {
)
}
}
// FIXME: This is a dirty hack to make the performance-stuff work.
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
// it when we need it to display on the home timeline.
// We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate
for authentication in AuthenticationServiceProvider.shared.authentications {
_ = try? await accountInfo(domain: authentication.domain,
userID: authentication.userID,
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value
}
NotificationCenter.default.post(name: .userFetched, object: nil)
return response
}

View File

@ -25,13 +25,13 @@ public final class AuthenticationService: NSObject {
// output
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
private func fetchFollowedBlockedUserIds(
_ authBox: MastodonAuthenticationBox,
_ previousFollowingIDs: [String]? = nil,
_ maxID: String? = nil
) async throws {
guard let apiService = apiService else { return }
guard let apiService else { return }
let followingResponse = try await fetchFollowing(maxID, apiService, authBox)
let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids