diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 82880d0e1..2abbab917 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; - 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -833,8 +829,6 @@ D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = ""; }; D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = ""; }; D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; - D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = ""; }; - D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = ""; }; D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = ""; }; D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; @@ -1892,21 +1886,10 @@ D8AC98742B0F615E0045EC2B /* Persistence */ = { isa = PBXGroup; children = ( - D8AC98772B0F62230045EC2B /* Model */, - D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */, - 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */, ); path = Persistence; sourceTree = ""; }; - D8AC98772B0F62230045EC2B /* Model */ = { - isa = PBXGroup; - children = ( - D8AC98782B0F622B0045EC2B /* SearchHistory.swift */, - ); - path = Model; - sourceTree = ""; - }; 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 */, diff --git a/Mastodon/Extension/AppContext+NextAccount.swift b/Mastodon/Extension/AppContext+NextAccount.swift index db3df4194..a2b0b34b3 100644 --- a/Mastodon/Extension/AppContext+NextAccount.swift +++ b/Mastodon/Extension/AppContext+NextAccount.swift @@ -5,8 +5,6 @@ // Created by Marcus Kida on 17.11.22. // -import CoreData -import CoreDataStack import MastodonCore import MastodonSDK diff --git a/Mastodon/Persistence/Model/SearchHistory.swift b/Mastodon/Persistence/Model/SearchHistory.swift deleted file mode 100644 index 057536747..000000000 --- a/Mastodon/Persistence/Model/SearchHistory.swift +++ /dev/null @@ -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 - } - } -} diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 115f077c4..0edb9e0cd 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -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 diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 19a58c80b..c24b2dbe3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -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 ) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 5f306ea20..fa419635f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -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 diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 70aafb730..898f091f8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -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( diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 9e1d082c5..258585c1d 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -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) diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index c1e8933ee..1f83b73d8 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -122,8 +122,6 @@ public class AppContext: ObservableObject { } .store(in: &disposeBag) } - - } extension AppContext { diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index ec706cc98..1f120e522 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -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 diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 77bf8098a..ee2a1ac9b 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -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) } } } } diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 6ebfe0d15..317e9ad52 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -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 { diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift new file mode 100644 index 000000000..1df3aef1e --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift @@ -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) + } + + } + +} diff --git a/Mastodon/Persistence/FileManager+SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift similarity index 81% rename from Mastodon/Persistence/FileManager+SearchHistory.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift index 95dd29a52..e289044d3 100644 --- a/Mastodon/Persistence/FileManager+SearchHistory.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift @@ -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) diff --git a/Mastodon/Persistence/FileManager+Timeline.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift similarity index 72% rename from Mastodon/Persistence/FileManager+Timeline.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift index 0a7046eaa..695686d6f 100644 --- a/Mastodon/Persistence/FileManager+Timeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift @@ -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)) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift new file mode 100644 index 000000000..9ad5b30be --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift @@ -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 + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index f11ff61e1..ffb45a26f 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -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)" } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index 683a98166..749d09110 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -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 } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 272d81fa2..1b32b88aa 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -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 } diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 8c7963a19..e43b812c9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -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