From c94f0126b7a712fb0f9171df6c3e3e06ae76d2b1 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 23 May 2023 10:30:35 +0200 Subject: [PATCH 01/22] Switch CoreData to In-Memory Store --- MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index e8ac87548..48e69e375 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -25,6 +25,7 @@ public final class CoreDataStack { public convenience init(databaseName: String = "shared") { let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName) let storeDescription = NSPersistentStoreDescription(url: storeURL) + storeDescription.url = URL(fileURLWithPath: "/dev/null") /// in-memory store with all features in favor of NSInMemoryStoreType self.init(persistentStoreDescriptions: [storeDescription]) } From d570d3ef0959f8c3158277314dd5b70ca738bc20 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 25 May 2023 16:26:17 +0200 Subject: [PATCH 02/22] Replace MastodonAuthentication to not be backed by CoreData --- Mastodon.xcodeproj/project.pbxproj | 4 - Mastodon/Coordinator/SceneCoordinator.swift | 9 +- .../Diffable/Discovery/DiscoverySection.swift | 2 +- .../Search/SearchHistorySection.swift | 2 +- .../Diffable/Search/SearchResultSection.swift | 2 +- Mastodon/Diffable/User/UserSection.swift | 2 +- .../Extension/AppContext+NextAccount.swift | 10 +- .../DataSourceFacade+SearchHistory.swift | 8 +- .../Scene/Account/AccountListViewModel.swift | 54 ++------- .../Scene/Account/AccountViewController.swift | 6 +- .../Share/AuthenticationViewModel.swift | 41 +++---- .../FollowedTags/FollowedTagsViewModel.swift | 2 +- .../Scene/Profile/MeProfileViewModel.swift | 2 +- .../Scene/Profile/ProfileViewController.swift | 7 +- Mastodon/Scene/Profile/ProfileViewModel.swift | 2 +- .../ReportResult/ReportResultViewModel.swift | 2 +- .../Root/MainTab/MainTabBarController.swift | 6 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 4 +- Mastodon/Supporting Files/SceneDelegate.swift | 7 +- .../Handler/SendPostIntentHandler.swift | 9 +- MastodonIntent/Model/Account+Fetch.swift | 10 +- .../Model/MastodonAuthentication+Fetch.swift | 20 ---- .../Entity/Mastodon/Instance.swift | 2 +- .../Mastodon/MastodonAuthentication.swift | 47 ++++---- .../Entity/Mastodon/MastodonUser.swift | 2 +- .../Sources/MastodonCore/AuthContext.swift | 26 ----- .../MastodonAuthenticationBox.swift | 10 +- .../AuthenticationServiceProvider.swift | 49 +++++++++ .../MastodonCore/MastodonAuthentication.swift | 103 ++++++++++++++++++ .../Service/API/APIService+Account.swift | 2 +- .../Service/API/APIService+Block.swift | 18 ++- .../Service/API/APIService+Bookmark.swift | 23 ++-- .../Service/API/APIService+Favorite.swift | 20 ++-- .../Service/API/APIService+Follow.swift | 9 +- .../API/APIService+FollowRequest.swift | 2 +- .../Service/API/APIService+Follower.swift | 2 +- .../Service/API/APIService+Following.swift | 4 +- .../API/APIService+HashtagTimeline.swift | 4 +- .../Service/API/APIService+HomeTimeline.swift | 2 +- .../Service/API/APIService+Mute.swift | 11 +- .../Service/API/APIService+Notification.swift | 4 +- .../Service/API/APIService+Poll.swift | 4 +- .../API/APIService+PublicTimeline.swift | 2 +- .../Service/API/APIService+Reblog.swift | 17 ++- .../Service/API/APIService+Relationship.swift | 2 +- .../Service/API/APIService+Search.swift | 2 +- .../API/APIService+Status+History.swift | 2 +- .../API/APIService+Status+Publish.swift | 2 +- .../Service/API/APIService+Status.swift | 2 +- .../Service/API/APIService+Tags.swift | 2 +- .../Service/API/APIService+Thread.swift | 2 +- .../Service/API/APIService+UserTimeline.swift | 2 +- ...vice+CoreData+MastodonAuthentication.swift | 76 ------------- .../Service/AuthenticationService.swift | 72 ++---------- .../Service/InstanceService.swift | 28 +---- .../Notification/NotificationService.swift | 40 +++---- .../ComposeContentViewModel.swift | 15 ++- .../Content/NotificationView+ViewModel.swift | 5 +- .../View/Content/StatusView+ViewModel.swift | 5 +- .../Scene/ShareViewController.swift | 4 +- .../FollowersCount/FollowersCountWidget.swift | 4 +- .../MultiFollowersCountWidget.swift | 4 +- 62 files changed, 369 insertions(+), 474 deletions(-) delete mode 100644 MastodonIntent/Model/MastodonAuthentication+Fetch.swift create mode 100644 MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift create mode 100644 MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift delete mode 100644 MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7d3c4f611..91a227d5e 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -309,7 +309,6 @@ DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */; }; DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */; }; DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */; }; - DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; }; DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; }; DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; }; DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; }; @@ -997,7 +996,6 @@ DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+Configuration.swift"; sourceTree = ""; }; DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Reblog.swift"; sourceTree = ""; }; DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Favorite.swift"; sourceTree = ""; }; - DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = ""; }; DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; }; DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = ""; }; DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = ""; }; @@ -2302,7 +2300,6 @@ DB64BA462851F23300ADF1B7 /* Model */ = { isa = PBXGroup; children = ( - DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */, DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */, ); path = Model; @@ -3949,7 +3946,6 @@ DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */, 2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */, - DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */, 2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 4908a533a..496808d1f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -54,12 +54,8 @@ final public class SceneCoordinator { return } else { // switch to notification's account - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) - request.returnsObjectsAsFaults = false - request.fetchLimit = 1 do { - guard let authentication = try appContext.managedObjectContext.fetch(request).first else { + guard let authentication = AuthenticationServiceProvider.shared.authentications.first(where: { $0.userAccessToken == accessToken }) else { return } let domain = authentication.domain @@ -221,8 +217,7 @@ extension SceneCoordinator { let rootViewController: UIViewController do { - let request = MastodonAuthentication.activeSortedFetchRequest // use active order - let _authentication = try appContext.managedObjectContext.fetch(request).first + let _authentication = AuthenticationServiceProvider.shared.authentications.first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } self.authContext = _authContext diff --git a/Mastodon/Diffable/Discovery/DiscoverySection.swift b/Mastodon/Diffable/Discovery/DiscoverySection.swift index 225b6f46a..53726292b 100644 --- a/Mastodon/Diffable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffable/Discovery/DiscoverySection.swift @@ -77,7 +77,7 @@ extension DiscoverySection { cell.profileCardView.viewModel.familiarFollowers = nil } // bind me - cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) } return cell case .bottomLoader: diff --git a/Mastodon/Diffable/Search/SearchHistorySection.swift b/Mastodon/Diffable/Search/SearchHistorySection.swift index 813c5b59a..1cd17da78 100644 --- a/Mastodon/Diffable/Search/SearchHistorySection.swift +++ b/Mastodon/Diffable/Search/SearchHistorySection.swift @@ -31,7 +31,7 @@ extension SearchHistorySection { context.managedObjectContext.performAndWait { guard let user = item.object(in: context.managedObjectContext) else { return } cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, + me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), viewModel: SearchHistoryUserCollectionViewCell.ViewModel( value: user, followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), diff --git a/Mastodon/Diffable/Search/SearchResultSection.swift b/Mastodon/Diffable/Search/SearchResultSection.swift index 90560150e..06ea1b7fe 100644 --- a/Mastodon/Diffable/Search/SearchResultSection.swift +++ b/Mastodon/Diffable/Search/SearchResultSection.swift @@ -129,7 +129,7 @@ extension SearchResultSection { configuration: Configuration ) { cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, + me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), tableView: tableView, viewModel: viewModel, delegate: configuration.userTableViewCellDelegate diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index cbad1ff72..8a0563db4 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -83,7 +83,7 @@ extension UserSection { configuration: Configuration ) { cell.configure( - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user, + me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), tableView: tableView, viewModel: viewModel, delegate: configuration.userTableViewCellDelegate diff --git a/Mastodon/Extension/AppContext+NextAccount.swift b/Mastodon/Extension/AppContext+NextAccount.swift index a8eae1e13..db3df4194 100644 --- a/Mastodon/Extension/AppContext+NextAccount.swift +++ b/Mastodon/Extension/AppContext+NextAccount.swift @@ -12,17 +12,13 @@ import MastodonSDK extension AppContext { func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? { - let request = MastodonAuthentication.sortedFetchRequest - guard - let accounts = try? managedObjectContext.fetch(request), - accounts.count > 1 - else { return nil } + let accounts = AuthenticationServiceProvider.shared.authentications + guard accounts.count > 1 else { return nil } let nextSelectedAccountIndex: Int? = { for (index, account) in accounts.enumerated() { guard account == authContext.mastodonAuthenticationBox - .authenticationRecord - .object(in: managedObjectContext) + .authentication else { continue } let nextAccountIndex = index + 1 diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 18d238c02..446b0a27d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -23,7 +23,7 @@ extension DataSourceFacade { let managedObjectContext = provider.context.backgroundManagedObjectContext try? await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } guard let user = record.object(in: managedObjectContext) else { return } _ = Persistence.SearchHistory.createOrMerge( in: managedObjectContext, @@ -41,7 +41,7 @@ extension DataSourceFacade { switch tag { case .entity(let entity): try? await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } let now = Date() @@ -67,7 +67,7 @@ extension DataSourceFacade { case .record(let record): try? await managedObjectContext.performChanges { let authenticationBox = provider.authContext.mastodonAuthenticationBox - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } guard let tag = record.object(in: managedObjectContext) else { return } let now = Date() @@ -98,7 +98,7 @@ extension DataSourceFacade { let managedObjectContext = provider.context.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return } let request = SearchHistory.sortedFetchRequest request.predicate = SearchHistory.predicate( domain: authenticationBox.domain, diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 2b8c75b8d..16a5055c6 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -22,10 +22,8 @@ final class AccountListViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController // output - @Published var authentications: [ManagedObjectRecord] = [] @Published var items: [Item] = [] let dataSourceDidUpdate = PassthroughSubject() @@ -34,30 +32,11 @@ final class AccountListViewModel: NSObject { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.mastodonAuthenticationFetchedResultsController = { - let fetchRequest = MastodonAuthentication.sortedFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: context.managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - return controller - }() + super.init() // end init - - mastodonAuthenticationFetchedResultsController.delegate = self - do { - try mastodonAuthenticationFetchedResultsController.performFetch() - authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? [] - } catch { - assertionFailure(error.localizedDescription) - } - $authentications + AuthenticationServiceProvider.shared.$authentications .receive(on: DispatchQueue.main) .sink { [weak self] authentications in guard let self = self else { return } @@ -86,7 +65,7 @@ extension AccountListViewModel { } enum Item: Hashable { - case authentication(record: ManagedObjectRecord) + case authentication(record: MastodonAuthentication) case addAccount } @@ -98,12 +77,12 @@ extension AccountListViewModel { switch item { case .authentication(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell - if let authentication = record.object(in: managedObjectContext), - let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext) + if let activeAuthentication = AuthenticationServiceProvider.shared.authentications.first { AccountListViewModel.configure( + in: managedObjectContext, cell: cell, - authentication: authentication, + authentication: record, activeAuthentication: activeAuthentication ) } @@ -120,11 +99,12 @@ extension AccountListViewModel { } static func configure( + in context: NSManagedObjectContext, cell: AccountListTableViewCell, authentication: MastodonAuthentication, activeAuthentication: MastodonAuthentication ) { - let user = authentication.user + guard let user = authentication.user(in: context) else { return } // avatar cell.avatarButton.avatarImageView.configure( @@ -169,21 +149,3 @@ extension AccountListViewModel { .joined(separator: ", ") } } - -// MARK: - NSFetchedResultsControllerDelegate -extension AccountListViewModel: NSFetchedResultsControllerDelegate { - - public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - - public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - guard controller === mastodonAuthenticationFetchedResultsController else { - assertionFailure() - return - } - - authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecord } ?? [] - } - -} diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 75ea91fd6..4eab3565c 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -69,8 +69,7 @@ extension AccountListViewController: PanModalPresentable { return .contentHeight(CGFloat(height)) } - let request = MastodonAuthentication.sortedFetchRequest - let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0 + let authenticationCount = AuthenticationServiceProvider.shared.authentications.count let count = authenticationCount + 1 let height = calculateHeight(of: count) @@ -177,9 +176,8 @@ extension AccountListViewController: UITableViewDelegate { switch item { case .authentication(let record): assert(Thread.isMainThread) - guard let authentication = record.object(in: context.managedObjectContext) else { return } Task { @MainActor in - let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) + let isActive = try await context.authenticationService.activeMastodonUser(domain: record.domain, userID: record.userID) guard isActive else { return } self.coordinator.setup() } // end Task diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 920164bce..868b14641 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -199,41 +199,26 @@ extension AuthenticationViewModel { domain: info.domain, authorization: authorization ) - .flatMap { response -> AnyPublisher, Error> in + .tryMap { response -> Mastodon.Response.Content in let account = response.value let mastodonUserRequest = MastodonUser.sortedFetchRequest mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) mastodonUserRequest.fetchLimit = 1 guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { - return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher() + throw AuthenticationError.badCredentials } + + AuthenticationServiceProvider.shared + .authentications + .insert(MastodonAuthentication.createFrom(domain: info.domain, + userID: mastodonUser.id, + username: mastodonUser.username, + appAccessToken: userToken.accessToken, // TODO: swap app token + userAccessToken: userToken.accessToken, + clientID: info.clientID, + clientSecret: info.clientSecret), at: 0) - let property = MastodonAuthentication.Property( - domain: info.domain, - userID: mastodonUser.id, - username: mastodonUser.username, - appAccessToken: userToken.accessToken, // TODO: swap app token - userAccessToken: userToken.accessToken, - clientID: info.clientID, - clientSecret: info.clientSecret - ) - return managedObjectContext.performChanges { - _ = APIService.CoreData.createOrMergeMastodonAuthentication( - into: managedObjectContext, - for: mastodonUser, - in: info.domain, - property: property, - networkDate: response.networkDate - ) - } - .setFailureType(to: Error.self) - .tryMap { result in - switch result { - case .failure(let error): throw error - case .success: return response - } - } - .eraseToAnyPublisher() + return response } .eraseToAnyPublisher() } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift index dbcf4d756..91e6c36ca 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -34,7 +34,7 @@ final class FollowedTagsViewModel: NSObject { self.fetchedResultsController = FollowedTagsFetchedResultController( managedObjectContext: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain, - user: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)!.user + user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme: ) super.init() diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index 995e32002..46424ab5e 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -16,7 +16,7 @@ import MastodonSDK final class MeProfileViewModel: ProfileViewModel { init(context: AppContext, authContext: AuthContext) { - let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) super.init( context: context, authContext: authContext, diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 238f076f0..8e0d61ee4 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -963,11 +963,6 @@ extension ProfileViewController: PagerTabStripNavigateable { private extension ProfileViewController { var currentInstance: Instance? { - guard let authenticationRecord = authContext.mastodonAuthenticationBox - .authenticationRecord - .object(in: context.managedObjectContext) - else { return nil } - - return authenticationRecord.instance + authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext) } } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 5c81c7920..de32b5607 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -85,7 +85,7 @@ class ProfileViewModel: NSObject { super.init() // bind me - self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) $me .assign(to: \.me, on: relationshipViewModel) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index 8123a8773..9caaeeb59 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -60,7 +60,7 @@ class ReportResultViewModel: ObservableObject { Task { @MainActor in guard let user = user.object(in: context.managedObjectContext) else { return } - guard let me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { return } + guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return } self.relationshipViewModel.user = user self.relationshipViewModel.me = me diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 13ae74103..3867e8cbe 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -292,7 +292,7 @@ extension MainTabBarController { } .store(in: &disposeBag) - if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { + if let user = authContext?.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) { self.avatarURLObserver = user.publisher(for: \.avatar) .sink { [weak self, weak user] _ in guard let self = self else { return } @@ -486,9 +486,9 @@ extension MainTabBarController { authenticationBox: authContext.mastodonAuthenticationBox ) - if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object( + if let user = authContext.mastodonAuthenticationBox.authentication.user( in: context.managedObjectContext - )?.user { + ) { user.update( property: .init( entity: profileResponse.value, diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 6362cfe95..fd3e3e45e 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -75,7 +75,7 @@ extension SidebarViewModel { let imageURL: URL? = { switch item { case .me: - let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user + let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) return user?.avatarImageURL() default: return nil @@ -132,7 +132,7 @@ extension SidebarViewModel { } .store(in: &cell.disposeBag) case .me: - guard let user = self.authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } + guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } let currentUserDisplayName = user.displayNameWithFallback cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) default: diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index a5c3a9caa..1e023227c 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -214,11 +214,8 @@ extension SceneDelegate { assertionFailure() return false } - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) - request.fetchLimit = 1 - - guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else { + + guard let authentication = AuthenticationServiceProvider.shared.getAuthentication(matching: accessToken) else { assertionFailure() return false } diff --git a/MastodonIntent/Handler/SendPostIntentHandler.swift b/MastodonIntent/Handler/SendPostIntentHandler.swift index 54fcefe39..b457b1c91 100644 --- a/MastodonIntent/Handler/SendPostIntentHandler.swift +++ b/MastodonIntent/Handler/SendPostIntentHandler.swift @@ -50,9 +50,8 @@ extension SendPostIntentHandler: SendPostIntentHandling { let mastodonAuthentications: [MastodonAuthentication] let accounts = intent.accounts ?? [] if accounts.isEmpty { - let request = MastodonAuthentication.sortedFetchRequest - let authentications = try managedObjectContext.fetch(request) - let _authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first + // fixme: refactor this and implemented method on AuthenticationServiceProvider + let _authentication = AuthenticationServiceProvider.shared.authentications.sorted(by: { $0.activedAt > $1.activedAt }).first guard let authentication = _authentication else { let failureReason = APIService.APIError.implicit(.authenticationMissing).errorDescription ?? "Fail to Send Post" @@ -65,12 +64,12 @@ extension SendPostIntentHandler: SendPostIntentHandling { let authenticationBoxes = mastodonAuthentications.map { authentication in MastodonAuthenticationBox( - authenticationRecord: .init(objectID: authentication.objectID), + authentication: authentication, domain: authentication.domain, userID: authentication.userID, appAuthorization: .init(accessToken: authentication.appAccessToken), userAuthorization: .init(accessToken: authentication.userAccessToken), - inMemoryCache: .sharedCache(for: authentication.objectID.description) + inMemoryCache: .sharedCache(for: authentication.userAccessToken) ) } diff --git a/MastodonIntent/Model/Account+Fetch.swift b/MastodonIntent/Model/Account+Fetch.swift index fd8c81769..f3d8ee344 100644 --- a/MastodonIntent/Model/Account+Fetch.swift +++ b/MastodonIntent/Model/Account+Fetch.swift @@ -17,9 +17,11 @@ extension Account { static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] { // get accounts let accounts: [Account] = try await managedObjectContext.perform { - let results = try MastodonAuthentication.fetch(in: managedObjectContext) + let results = AuthenticationServiceProvider.shared.authentications let accounts = results.compactMap { mastodonAuthentication -> Account? in - let user = mastodonAuthentication.user + guard let user = mastodonAuthentication.user(in: managedObjectContext) else { + return nil + } let account = Account( identifier: mastodonAuthentication.identifier.uuidString, display: user.displayNameWithFallback, @@ -43,9 +45,7 @@ extension Array where Element == Account { let identifiers = self .compactMap { $0.identifier } .compactMap { UUID(uuidString: $0) } - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(identifiers: identifiers) - let results = try managedObjectContext.fetch(request) + let results = AuthenticationServiceProvider.shared.authentications.filter({ identifiers.contains($0.identifier) }) return results } diff --git a/MastodonIntent/Model/MastodonAuthentication+Fetch.swift b/MastodonIntent/Model/MastodonAuthentication+Fetch.swift deleted file mode 100644 index 9d1201000..000000000 --- a/MastodonIntent/Model/MastodonAuthentication+Fetch.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MastodonAuthentication.swift -// MastodonIntent -// -// Created by MainasuK on 2022-6-9. -// - -import Foundation -import CoreData -import CoreDataStack - -extension MastodonAuthentication { - - static func fetch(in managedObjectContext: NSManagedObjectContext) throws -> [MastodonAuthentication] { - let request = MastodonAuthentication.sortedFetchRequest - let results = try managedObjectContext.fetch(request) - return results - } - -} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift index c11a92b76..d7b32d5d2 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift @@ -19,7 +19,7 @@ public final class Instance: NSManagedObject { @NSManaged public private(set) var configurationV2Raw: Data? // MARK: one-to-many relationships - @NSManaged public var authentications: Set + @NSManaged public var authentications: Set } extension Instance { diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift index 2d8a97fad..dfdc0f045 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonAuthentication.swift @@ -8,7 +8,8 @@ import Foundation import CoreData -final public class MastodonAuthentication: NSManagedObject { +@objc(MastodonAuthentication) +final public class MastodonAuthenticationLegacy: NSManagedObject { public typealias ID = UUID @@ -35,16 +36,16 @@ final public class MastodonAuthentication: NSManagedObject { } -extension MastodonAuthentication { +extension MastodonAuthenticationLegacy { public override func awakeFromInsert() { super.awakeFromInsert() - setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthentication.identifier)) + setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthenticationLegacy.identifier)) let now = Date() - setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.createdAt)) - setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.updatedAt)) - setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.activedAt)) + setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.createdAt)) + setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.updatedAt)) + setPrimitiveValue(now, forKey: #keyPath(MastodonAuthenticationLegacy.activedAt)) } @discardableResult @@ -52,8 +53,8 @@ extension MastodonAuthentication { into context: NSManagedObjectContext, property: Property, user: MastodonUser - ) -> MastodonAuthentication { - let authentication: MastodonAuthentication = context.insertObject() + ) -> MastodonAuthenticationLegacy { + let authentication: MastodonAuthenticationLegacy = context.insertObject() authentication.domain = property.domain authentication.userID = property.userID @@ -112,7 +113,7 @@ extension MastodonAuthentication { } -extension MastodonAuthentication { +extension MastodonAuthenticationLegacy { public struct Property { public let domain: String @@ -144,51 +145,51 @@ extension MastodonAuthentication { } } -extension MastodonAuthentication: Managed { +extension MastodonAuthenticationLegacy: Managed { public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)] + return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.createdAt, ascending: false)] } public static var activeSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \MastodonAuthentication.activedAt, ascending: false)] + return [NSSortDescriptor(keyPath: \MastodonAuthenticationLegacy.activedAt, ascending: false)] } } -extension MastodonAuthentication { - public static var activeSortedFetchRequest: NSFetchRequest { - let request = NSFetchRequest(entityName: entityName) +extension MastodonAuthenticationLegacy { + public static var activeSortedFetchRequest: NSFetchRequest { + let request = NSFetchRequest(entityName: entityName) request.sortDescriptors = activeSortDescriptors return request } } -extension MastodonAuthentication { +extension MastodonAuthenticationLegacy { public static func predicate(domain: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain) + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.domain), domain) } static func predicate(userID: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userID), userID) + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userID), userID) } public static func predicate(domain: String, userID: String) -> NSPredicate { return NSCompoundPredicate(andPredicateWithSubpredicates: [ - MastodonAuthentication.predicate(domain: domain), - MastodonAuthentication.predicate(userID: userID) + MastodonAuthenticationLegacy.predicate(domain: domain), + MastodonAuthenticationLegacy.predicate(userID: userID) ]) } public static func predicate(userAccessToken: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userAccessToken), userAccessToken) + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.userAccessToken), userAccessToken) } public static func predicate(identifier: UUID) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.identifier), identifier as NSUUID) + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifier as NSUUID) } public static func predicate(identifiers: [UUID]) -> NSPredicate { - return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthentication.identifier), identifiers as [NSUUID]) + return NSPredicate(format: "%K IN %@", #keyPath(MastodonAuthenticationLegacy.identifier), identifiers as [NSUUID]) } } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index fe668ccb9..cb2548b6a 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -62,7 +62,7 @@ final public class MastodonUser: NSManagedObject { // one-to-one relationship @NSManaged public private(set) var pinnedStatus: Status? - @NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication? + @NSManaged public private(set) var mastodonAuthentication: MastodonAuthenticationLegacy? // one-to-many relationship @NSManaged public private(set) var statuses: Set diff --git a/MastodonSDK/Sources/MastodonCore/AuthContext.swift b/MastodonSDK/Sources/MastodonCore/AuthContext.swift index b93a2e03a..ff936c3c0 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthContext.swift @@ -27,38 +27,12 @@ public class AuthContext { private init(mastodonAuthenticationBox: MastodonAuthenticationBox) { self.mastodonAuthenticationBox = mastodonAuthenticationBox } - } extension AuthContext { public convenience init?(authentication: MastodonAuthentication) { self.init(mastodonAuthenticationBox: MastodonAuthenticationBox(authentication: authentication)) - - ManagedObjectObserver.observe(object: authentication) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") - case .finished: - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): observer finished") - } - } receiveValue: { [weak self] change in - guard let self = self else { return } - switch change.changeType { - case .update(let object): - guard let authentication = object as? MastodonAuthentication else { - assertionFailure() - return - } - self.mastodonAuthenticationBox = .init(authentication: authentication) - default: - break - } - } - .store(in: &disposeBag) } } diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift index e35264ed1..8db326dfe 100644 --- a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -10,7 +10,7 @@ import CoreDataStack import MastodonSDK public struct MastodonAuthenticationBox: UserIdentifier { - public let authenticationRecord: ManagedObjectRecord + public let authentication: MastodonAuthentication public let domain: String public let userID: MastodonUser.ID public let appAuthorization: Mastodon.API.OAuth.Authorization @@ -18,14 +18,14 @@ public struct MastodonAuthenticationBox: UserIdentifier { public let inMemoryCache: MastodonAccountInMemoryCache public init( - authenticationRecord: ManagedObjectRecord, + authentication: MastodonAuthentication, domain: String, userID: MastodonUser.ID, appAuthorization: Mastodon.API.OAuth.Authorization, userAuthorization: Mastodon.API.OAuth.Authorization, inMemoryCache: MastodonAccountInMemoryCache ) { - self.authenticationRecord = authenticationRecord + self.authentication = authentication self.domain = domain self.userID = userID self.appAuthorization = appAuthorization @@ -38,12 +38,12 @@ extension MastodonAuthenticationBox { init(authentication: MastodonAuthentication) { self = MastodonAuthenticationBox( - authenticationRecord: .init(objectID: authentication.objectID), + authentication: authentication, domain: authentication.domain, userID: authentication.userID, appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken), - inMemoryCache: .sharedCache(for: authentication.objectID.description) + inMemoryCache: .sharedCache(for: authentication.userID) // todo: make sure this is really unique ) } diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift new file mode 100644 index 000000000..a7c3decc1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -0,0 +1,49 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import Combine +import CoreDataStack +import MastodonSDK + +public class AuthenticationServiceProvider: ObservableObject { + public static let shared = AuthenticationServiceProvider() + + private init() {} + + @Published public var authentications: [MastodonAuthentication] = [] + + func update(instance: Instance, where domain: String) { + authentications = authentications.map { authentication in + guard authentication.domain == domain else { return authentication } + return authentication.updating(instance: instance) + } + } + + func delete(authentication: MastodonAuthentication) { + authentications.removeAll(where: { $0 == authentication }) + } + + func activateAuthentication(in domain: String, for userID: String) { + authentications = authentications.map { authentication in + guard authentication.domain == domain, authentication.userID == userID else { + return authentication + } + return authentication.updating(activatedAt: Date()) + } + } + + func getAuthentication(in domain: String, for userID: String) -> MastodonAuthentication? { + authentications.first(where: { $0.domain == domain && $0.userID == userID }) + } +} + +// MARK: - Public +public extension AuthenticationServiceProvider { + func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? { + authentications.first(where: { $0.userAccessToken == userAccessToken }) + } + + func sortByActivation() { // fixme: why do we need this? + authentications = authentications.sorted(by: { $0.activedAt > $1.activedAt }) + } +} diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift new file mode 100644 index 000000000..7b0660502 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -0,0 +1,103 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import CoreDataStack +import MastodonSDK + +public struct MastodonAuthentication: Codable, Hashable { + public typealias ID = UUID + + public private(set) var identifier: ID + public private(set) var domain: String + public private(set) var username: String + + public private(set) var appAccessToken: String + public private(set) var userAccessToken: String + public private(set) var clientID: String + public private(set) var clientSecret: String + + public private(set) var createdAt: Date + public private(set) var updatedAt: Date + public private(set) var activedAt: Date + + public private(set) var userID: String + public private(set) var instanceObjectIdURI: URL? + + public static func createFrom( + domain: String, + userID: String, + username: String, + appAccessToken: String, + userAccessToken: String, + clientID: String, + clientSecret: String + ) -> Self { + let now = Date() + return MastodonAuthentication( + identifier: .init(), // fixme + domain: domain, + username: username, + appAccessToken: appAccessToken, + userAccessToken: userAccessToken, + clientID: clientID, + clientSecret: clientSecret, + createdAt: now, + updatedAt: now, + activedAt: now, + userID: userID, + instanceObjectIdURI: nil + ) + } + + func copy( + identifier: ID? = nil, + domain: String? = nil, + username: String? = nil, + appAccessToken: String? = nil, + userAccessToken: String? = nil, + clientID: String? = nil, + clientSecret: String? = nil, + createdAt: Date? = nil, + updatedAt: Date? = nil, + activedAt: Date? = nil, + userID: String? = nil, + instanceObjectIdURI: URL? = nil + ) -> Self { + MastodonAuthentication( + identifier: identifier ?? self.identifier, + domain: domain ?? self.domain, + username: username ?? self.username, + appAccessToken: appAccessToken ?? self.appAccessToken, + userAccessToken: userAccessToken ?? self.userAccessToken, + clientID: clientID ?? self.clientID, + clientSecret: clientSecret ?? self.clientSecret, + createdAt: createdAt ?? self.createdAt, + updatedAt: updatedAt ?? self.updatedAt, + activedAt: activedAt ?? self.activedAt, + userID: userID ?? self.userID, + instanceObjectIdURI: instanceObjectIdURI ?? self.instanceObjectIdURI + ) + } + + public func instance(in context: NSManagedObjectContext) -> Instance? { + guard + let instanceObjectIdURI = instanceObjectIdURI, + let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI) + else { return nil } + + return try? context.existingObject(with: objectID) as? Instance + } + + public func user(in context: NSManagedObjectContext) -> MastodonUser? { + let userPredicate = MastodonUser.predicate(domain: domain, id: userID) + return MastodonUser.findOrFetch(in: context, matching: userPredicate) + } + + func updating(instance: Instance) -> Self { + copy(instanceObjectIdURI: instance.objectID.uriRepresentation()) + } + + func updating(activatedAt: Date) -> Self { + copy(activedAt: activatedAt) + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index d68984587..ad8320fe9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -182,7 +182,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) for entity in response.value { _ = Persistence.Tag.createOrMerge( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 19c5ff437..5580245ce 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -69,12 +69,15 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { - guard let user = user.object(in: managedObjectContext), - let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let user = user.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let me = authentication.user + let isBlocking = user.blockingBy.contains(me) let isFollowing = user.followingBy.contains(me) // toggle block state @@ -119,10 +122,13 @@ extension APIService { } try await managedObjectContext.performChanges { - guard let user = user.object(in: managedObjectContext), - let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let user = user.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { return } - let me = authentication.user + switch result { case .success(let response): diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 0d8c243f8..9020524ea 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -29,12 +29,15 @@ extension APIService { // update bookmark state and retrieve bookmark context let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let _status = record.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let me = authentication.user + let status = _status.reblog ?? _status let isBookmarked = status.bookmarkedBy.contains(me) status.update(bookmarked: !isBookmarked, by: me) @@ -64,10 +67,13 @@ extension APIService { // update bookmark state try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let _status = record.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { return } - let me = authentication.user + let status = _status.reblog ?? _status switch result { @@ -114,7 +120,10 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + + guard + let me = authenticationBox.authentication.user(in: managedObjectContext) + else { assertionFailure() return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index 4abe9ba5f..166118c8a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -31,12 +31,15 @@ extension APIService { // update like state and retrieve like context let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let _status = record.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let me = authentication.user + let status = _status.reblog ?? _status let isFavorited = status.favouritedBy.contains(me) let favoritedCount = status.favouritesCount @@ -70,10 +73,13 @@ extension APIService { // update like state try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let _status = record.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { return } - let me = authentication.user + let status = _status.reblog ?? _status switch result { @@ -124,7 +130,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { assertionFailure() return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 442d293ce..0cd71cb2e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -38,7 +38,7 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return nil } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } guard let user = user.object(in: managedObjectContext) else { return nil } let isFollowing = user.followingBy.contains(me) @@ -94,7 +94,7 @@ extension APIService { // update friendship state try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, + guard let me = authenticationBox.authentication.user(in: managedObjectContext), let user = user.object(in: managedObjectContext) else { return } @@ -129,10 +129,9 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext guard let user = user.object(in: managedObjectContext), - let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + let me = authenticationBox.authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let me = authentication.user let result: Result, Error> let oldShowReblogs = me.showingReblogsBy.contains(user) @@ -153,7 +152,7 @@ extension APIService { } try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } switch result { case .success(let response): diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index 74650131d..fb0e52ddb 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -36,7 +36,7 @@ extension APIService { ) request.fetchLimit = 1 guard let user = managedObjectContext.safeFetch(request).first else { return } - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } Persistence.MastodonUser.update( mastodonUser: user, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift index 0f0ea1a59..9c5801449 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift @@ -36,7 +36,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) for entity in response.value { let result = Persistence.MastodonUser.createOrMerge( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index 313d715ed..e2a2c6142 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -37,8 +37,8 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user - + let me = authenticationBox.authentication.user(in: managedObjectContext) + for entity in response.value { let result = Persistence.MastodonUser.createOrMerge( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index d2fbe844a..86ee7faed 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -45,8 +45,8 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user - + let me = authenticationBox.authentication.user(in: managedObjectContext) + for entity in response.value { _ = Persistence.Status.createOrMerge( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index dad844d37..7fddc8344 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -40,7 +40,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { assertionFailure() return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index cc46872f4..15cd009e5 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -68,13 +68,15 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges { - guard let user = user.object(in: managedObjectContext), - let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let user = user.object(in: managedObjectContext), + let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let me = authentication.user let isMuting = user.mutingBy.contains(me) // toggle mute state @@ -116,9 +118,8 @@ extension APIService { try await managedObjectContext.performChanges { guard let user = user.object(in: managedObjectContext), - let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) + let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - let me = authentication.user switch result { case .success(let response): diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index c4d82df41..296a43d2b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -88,7 +88,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { assertionFailure() return } @@ -176,7 +176,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } _ = Persistence.Notification.createOrMerge( in: managedObjectContext, context: Persistence.Notification.PersistContext( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift index 5a4b38b29..dba4527a4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -36,7 +36,7 @@ extension APIService { ).singleOutput() try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) _ = Persistence.Poll.createOrMerge( in: managedObjectContext, context: Persistence.Poll.PersistContext( @@ -79,7 +79,7 @@ extension APIService { ).singleOutput() try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) _ = Persistence.Poll.createOrMerge( in: managedObjectContext, context: Persistence.Poll.PersistContext( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index 21f198299..6d6726db9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -30,7 +30,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) for entity in response.value { _ = Persistence.Status.createOrMerge( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index 0dc1a40e5..717f2715d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -29,11 +29,13 @@ extension APIService { // update repost state and retrieve repost context let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let me = authentication.user(in: managedObjectContext), + let _status = record.object(in: managedObjectContext) else { return nil } - let me = authentication.user let status = _status.reblog ?? _status let isReblogged = status.rebloggedBy.contains(me) let rebloggedCount = status.reblogsCount @@ -70,10 +72,13 @@ extension APIService { // update repost state try await managedObjectContext.performChanges { - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let authentication = authenticationBox.authentication + + guard + let me = authentication.user(in: managedObjectContext), + let _status = record.object(in: managedObjectContext) else { return } - let me = authentication.user + let status = _status.reblog ?? _status switch result { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index f5c108725..c2c88cfb1 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -42,7 +42,7 @@ extension APIService { ).singleOutput() try await managedObjectContext.performChanges { - guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { + guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { // assertionFailure() return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift index 5e6217bcf..7f63b2419 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -28,7 +28,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) // user for entity in response.value.accounts { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift index 88cf91f9e..482dd0019 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift @@ -76,7 +76,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) let status = Persistence.Status.createOrMerge( in: managedObjectContext, context: Persistence.Status.PersistContext( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift index d2cbd3f5c..ee70db964 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift @@ -34,7 +34,7 @@ extension APIService { #if !APP_EXTENSION let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) _ = Persistence.Status.createOrMerge( in: managedObjectContext, context: Persistence.Status.PersistContext( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index 52b169ee7..d652f0ad4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -30,7 +30,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) _ = Persistence.Status.createOrMerge( in: managedObjectContext, context: Persistence.Status.PersistContext( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift index 0b0320078..b5b526b8a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift @@ -74,7 +74,7 @@ fileprivate extension APIService { ) async throws -> Mastodon.Response.Content { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) _ = Persistence.Tag.createOrMerge( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index f1b94376e..ecc7a0bc3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -30,7 +30,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) let value = response.value.ancestors + response.value.descendants for entity in value { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift index fdf90a2aa..402bd9f61 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift @@ -46,7 +46,7 @@ extension APIService { let managedObjectContext = self.backgroundManagedObjectContext try await managedObjectContext.performChanges { - let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user + let me = authenticationBox.authentication.user(in: managedObjectContext) for entity in response.value { _ = Persistence.Status.createOrMerge( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift deleted file mode 100644 index 1acb52a77..000000000 --- a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// APIService+CoreData+MastodonAuthentication.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/2/3. -// - -import os.log -import Foundation -import CoreData -import CoreDataStack -import MastodonSDK - -extension APIService.CoreData { - - public static func createOrMergeMastodonAuthentication( - into managedObjectContext: NSManagedObjectContext, - for authenticateMastodonUser: MastodonUser, - in domain: String, - property: MastodonAuthentication.Property, - networkDate: Date - ) -> (mastodonAuthentication: MastodonAuthentication, isCreated: Bool) { - // fetch old mastodon authentication - let oldMastodonAuthentication: MastodonAuthentication? = { - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(domain: domain, userID: property.userID) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - - if let oldMastodonAuthentication = oldMastodonAuthentication { - // merge old mastodon authentication - APIService.CoreData.mergeMastodonAuthentication( - for: authenticateMastodonUser, - old: oldMastodonAuthentication, - in: domain, - property: property, - networkDate: networkDate - ) - return (oldMastodonAuthentication, false) - } else { - let mastodonAuthentication = MastodonAuthentication.insert( - into: managedObjectContext, - property: property, - user: authenticateMastodonUser - ) - return (mastodonAuthentication, true) - } - } - - static func mergeMastodonAuthentication( - for authenticateMastodonUser: MastodonUser, - old authentication: MastodonAuthentication, - in domain: String, - property: MastodonAuthentication.Property, - networkDate: Date - ) { - guard networkDate > authentication.updatedAt else { return } - - - authentication.update(username: property.username) - authentication.update(appAccessToken: property.appAccessToken) - authentication.update(userAccessToken: property.userAccessToken) - authentication.update(clientID: property.clientID) - authentication.update(clientSecret: property.clientSecret) - - authentication.didUpdate(at: networkDate) - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 8faac74d9..b3ad468cc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -22,10 +22,9 @@ public final class AuthenticationService: NSObject { weak var apiService: APIService? let managedObjectContext: NSManagedObjectContext // read-only let backgroundManagedObjectContext: NSManagedObjectContext - let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController + let authenticationServiceProvider = AuthenticationServiceProvider.shared // output - @Published public var mastodonAuthentications: [ManagedObjectRecord] = [] @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] private func fetchFollowedBlockedUserIds( @@ -93,21 +92,8 @@ public final class AuthenticationService: NSObject { self.managedObjectContext = managedObjectContext self.backgroundManagedObjectContext = backgroundManagedObjectContext self.apiService = apiService - self.mastodonAuthenticationFetchedResultsController = { - let fetchRequest = MastodonAuthentication.sortedFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - return controller - }() - super.init() - mastodonAuthenticationFetchedResultsController.delegate = self + super.init() $mastodonAuthenticationBoxes .sink { [weak self] boxes in @@ -123,10 +109,9 @@ public final class AuthenticationService: NSObject { // TODO: verify credentials for active authentication - $mastodonAuthentications + authenticationServiceProvider.$authentications .map { authentications -> [MastodonAuthenticationBox] in return authentications - .compactMap { $0.object(in: managedObjectContext) } .sorted(by: { $0.activedAt > $1.activedAt }) .compactMap { authentication -> MastodonAuthenticationBox? in return MastodonAuthenticationBox(authentication: authentication) @@ -134,14 +119,7 @@ public final class AuthenticationService: NSObject { } .assign(to: &$mastodonAuthenticationBoxes) - do { - try mastodonAuthenticationFetchedResultsController.performFetch() - mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects? - .sorted(by: { $0.activedAt > $1.activedAt }) - .compactMap { $0.asRecord } ?? [] - } catch { - assertionFailure(error.localizedDescription) - } + AuthenticationServiceProvider.shared.sortByActivation() } } @@ -151,18 +129,9 @@ extension AuthenticationService { public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { var isActive = false - let managedObjectContext = backgroundManagedObjectContext - - try await managedObjectContext.performChanges { - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) - request.fetchLimit = 1 - guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else { - return - } - mastodonAuthentication.update(activedAt: Date()) - isActive = true - } + AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID) + + isActive = true return isActive } @@ -183,12 +152,7 @@ extension AuthenticationService { managedObjectContext.delete(feed) } - guard let authentication = authenticationBox.authenticationRecord.object(in: managedObjectContext) else { - assertionFailure() - throw APIService.APIError.implicit(.authenticationMissing) - } - - managedObjectContext.delete(authentication) + AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication) } // cancel push notification subscription @@ -203,23 +167,3 @@ extension AuthenticationService { } } - -// MARK: - NSFetchedResultsControllerDelegate -extension AuthenticationService: NSFetchedResultsControllerDelegate { - - public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - - public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - guard controller === mastodonAuthenticationFetchedResultsController else { - assertionFailure() - return - } - - mastodonAuthentications = mastodonAuthenticationFetchedResultsController.fetchedObjects? - .sorted(by: { $0.activedAt > $1.activedAt }) - .compactMap { $0.asRecord } ?? [] - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 0745e2f37..1c3c05d4b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -89,18 +89,8 @@ extension InstanceService { log: Logger(subsystem: "Update", category: "InstanceService") ) - // update relationship - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(domain: domain) - request.returnsObjectsAsFaults = false - do { - let authentications = try managedObjectContext.fetch(request) - for authentication in authentications { - authentication.update(instance: instance) - } - } catch { - assertionFailure(error.localizedDescription) - } + // update instance + AuthenticationServiceProvider.shared.update(instance: instance, where: domain) } .setFailureType(to: Error.self) .tryMap { result in @@ -128,18 +118,8 @@ extension InstanceService { ) ) - // update relationship - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(domain: domain) - request.returnsObjectsAsFaults = false - do { - let authentications = try managedObjectContext.fetch(request) - for authentication in authentications { - authentication.update(instance: instance) - } - } catch { - assertionFailure(error.localizedDescription) - } + // update instance + AuthenticationServiceProvider.shared.update(instance: instance, where: domain) } .setFailureType(to: Error.self) .tryMap { result in diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index adabd8962..229def922 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -42,7 +42,7 @@ public final class NotificationService { self.apiService = apiService self.authenticationService = authenticationService - authenticationService.$mastodonAuthentications + AuthenticationServiceProvider.shared.$authentications .sink(receiveValue: { [weak self] mastodonAuthentications in guard let self = self else { return } @@ -113,13 +113,13 @@ extension NotificationService { let managedObjectContext = authenticationService.managedObjectContext return try await managedObjectContext.perform { var items: [UIApplicationShortcutItem] = [] - for object in authenticationService.mastodonAuthentications { - guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue } + for authentication in AuthenticationServiceProvider.shared.authentications { + guard let user = authentication.user(in: managedObjectContext) else { continue } let accessToken = authentication.userAccessToken let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) guard count > 0 else { continue } - let title = "@\(authentication.user.acctWithDomain)" + let title = "@\(user.acctWithDomain)" let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) let item = UIApplicationShortcutItem( @@ -214,9 +214,8 @@ extension NotificationService { let needsCancelSubscription: Bool = try await managedObjectContext.perform { // check authentication exists - let authenticationRequest = MastodonAuthentication.sortedFetchRequest - authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken) - return managedObjectContext.safeFetch(authenticationRequest).first == nil + let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == userAccessToken } + return results.first == nil } guard needsCancelSubscription else { @@ -255,22 +254,17 @@ extension NotificationService { private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? { guard let authenticationService = self.authenticationService else { return nil } - let managedObjectContext = authenticationService.managedObjectContext - return try await managedObjectContext.perform { - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken) - request.fetchLimit = 1 - guard let authentication = managedObjectContext.safeFetch(request).first else { return nil } - - return MastodonAuthenticationBox( - authenticationRecord: .init(objectID: authentication.objectID), - domain: authentication.domain, - userID: authentication.userID, - appAuthorization: .init(accessToken: authentication.appAccessToken), - userAuthorization: .init(accessToken: authentication.userAccessToken), - inMemoryCache: .sharedCache(for: authentication.objectID.description) - ) - } + let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken } + guard let authentication = results.first else { return nil } + + return MastodonAuthenticationBox( + authentication: authentication, + domain: authentication.domain, + userID: authentication.userID, + appAuthorization: .init(accessToken: authentication.appAccessToken), + userAuthorization: .init(accessToken: authentication.userAccessToken), + inMemoryCache: .sharedCache(for: authentication.userAccessToken) + ) } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 4e5c217ba..dac67b607 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -159,7 +159,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.visibility = { // default private when user locked var visibility: Mastodon.Entity.Status.Visibility = { - guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user else { + guard let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return .public } return author.locked ? .private : .public @@ -227,7 +227,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { assertionFailure() return } - let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user + let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) var mentionAccts: [String] = [] if author?.id != status.author.id { @@ -262,9 +262,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { let _configuration: Mastodon.Entity.Instance.Configuration? = { var configuration: Mastodon.Entity.Instance.Configuration? = nil context.managedObjectContext.performAndWait { - guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) - else { return } - configuration = authentication.instance?.configuration + let authentication = authContext.mastodonAuthenticationBox.authentication + configuration = authentication.instance(in: context.managedObjectContext)?.configuration } return configuration }() @@ -325,7 +324,7 @@ extension ComposeContentViewModel { $authContext .sink { [weak self] authContext in guard let self = self else { return } - guard let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } + guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } self.avatarURL = user.avatarImageURL() self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) self.username = user.acctWithDomain @@ -564,7 +563,7 @@ extension ComposeContentViewModel { let managedObjectContext = self.context.managedObjectContext var _author: ManagedObjectRecord? managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord + _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord } guard let author = _author else { throw AppError.badAuthentication @@ -620,7 +619,7 @@ extension ComposeContentViewModel { let managedObjectContext = self.context.managedObjectContext var _author: ManagedObjectRecord? managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord + _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord } guard let author = _author else { throw AppError.badAuthentication diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index ed038f47f..e887e540f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -230,9 +230,8 @@ extension NotificationView.ViewModel { var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil context.managedObjectContext.performAndWait { - guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) - else { return } - configuration = authentication.instance?.configurationV2 + let authentication = authContext.mastodonAuthenticationBox.authentication + configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2 } return configuration }() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index f466fd819..c3b0caf7a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -691,9 +691,8 @@ extension StatusView.ViewModel { var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil context.managedObjectContext.performAndWait { - guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) - else { return } - configuration = authentication.instance?.configurationV2 + let authentication = authContext.mastodonAuthenticationBox.authentication + configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2 } return configuration }() diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 18383a053..8ed018c99 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -179,8 +179,8 @@ extension ShareViewController { extension ShareViewController { private func setupAuthContext() throws -> AuthContext? { - let request = MastodonAuthentication.activeSortedFetchRequest // use active order - let _authentication = try context.managedObjectContext.fetch(request).first + AuthenticationServiceProvider.shared.sortByActivation() + let _authentication = AuthenticationServiceProvider.shared.authentications.first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } return _authContext } diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index c8e53fb1c..bef484e39 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -86,9 +86,9 @@ private extension FollowersCountWidgetProvider { } guard - let desiredAccount = configuration.account ?? authBox.authenticationRecord.object( + let desiredAccount = configuration.account ?? authBox.authentication.user( in: WidgetExtension.appContext.managedObjectContext - )?.user.acctWithDomain + )?.acctWithDomain else { return completion(.unconfigured) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 9049a5a79..15c1c4d14 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -86,9 +86,9 @@ private extension MultiFollowersCountWidgetProvider { if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) { desiredAccounts = configuredAccounts - } else if let currentlyLoggedInAccount = authBox.authenticationRecord.object( + } else if let currentlyLoggedInAccount = authBox.authentication.user( in: WidgetExtension.appContext.managedObjectContext - )?.user.acctWithDomain { + )?.acctWithDomain { desiredAccounts = [currentlyLoggedInAccount] } else { return completion(.unconfigured) From 06c72a022a8c411807f6d030c3e1a2b99bf20473 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 26 May 2023 11:36:08 +0200 Subject: [PATCH 03/22] Persist Authentication in Keychain --- Mastodon/Supporting Files/AppDelegate.swift | 1 + .../AuthenticationServiceProvider.swift | 30 +++++++++++++++++-- .../MastodonCore/MastodonAuthentication.swift | 6 +++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 85fed2b28..79eaeb5cd 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let appContext = AppContext() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + AuthenticationServiceProvider.shared.restore() AppSecret.default.register() diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index a7c3decc1..877f1211c 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -4,13 +4,20 @@ import Foundation import Combine import CoreDataStack import MastodonSDK +import KeychainAccess +import MastodonCommon public class AuthenticationServiceProvider: ObservableObject { public static let shared = AuthenticationServiceProvider() - + private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID) + private init() {} - @Published public var authentications: [MastodonAuthentication] = [] + @Published public var authentications: [MastodonAuthentication] = [] { + didSet { + persist() // todo: Is this too heavy and too often here??? + } + } func update(instance: Instance, where domain: String) { authentications = authentications.map { authentication in @@ -46,4 +53,23 @@ public extension AuthenticationServiceProvider { func sortByActivation() { // fixme: why do we need this? authentications = 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) + else { return nil } + return try? JSONDecoder().decode(MastodonAuthentication.self, from: data) + } + } +} + +// MARK: - Private +private extension AuthenticationServiceProvider { + func persist() { + for authentication in authentications { + Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString() + } + } } diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 7b0660502..a9880472a 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -23,6 +23,10 @@ public struct MastodonAuthentication: Codable, Hashable { public private(set) var userID: String public private(set) var instanceObjectIdURI: URL? + internal var persistenceIdentifier: String { + "\(username)@\(domain)" + } + public static func createFrom( domain: String, userID: String, @@ -34,7 +38,7 @@ public struct MastodonAuthentication: Codable, Hashable { ) -> Self { let now = Date() return MastodonAuthentication( - identifier: .init(), // fixme + identifier: .init(), domain: domain, username: username, appAccessToken: appAccessToken, From ca61d29ae20674426974a2c5b6bdec32391061be Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 1 Jun 2023 16:07:45 +0200 Subject: [PATCH 04/22] Download user when we need them --- .../MastodonCore/Service/API/APIService+HomeTimeline.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 7fddc8344..91d053d5b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -39,6 +39,12 @@ extension APIService { ).singleOutput() let managedObjectContext = self.backgroundManagedObjectContext + + // 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. + _ = try await authenticatedUserInfo(authenticationBox: authenticationBox).value + try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { assertionFailure() From 5513174696bfd207bc7c1ff6b28c358451e2222b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 1 Jun 2023 16:19:06 +0200 Subject: [PATCH 05/22] Apply differences on main-queue This fixes a lot of warnings in Console when searching for a server --- .../Scene/Onboarding/Login/MastodonLoginViewController.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 8f1cbecf2..c8f614e59 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -298,10 +298,9 @@ extension MastodonLoginViewController: MastodonLoginViewModelDelegate { snapshot.appendSections([MastodonLoginViewSection.servers]) snapshot.appendItems(viewModel.filteredServers) - - dataSource?.apply(snapshot, animatingDifferences: false) - + DispatchQueue.main.async { + self.dataSource?.apply(snapshot, animatingDifferences: false) let numberOfResults = viewModel.filteredServers.count self.contentView.updateCorners(numberOfResults: numberOfResults) } From 8df4729cdd7ad26b29705cd3fa1576e96de0ef9b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 1 Jun 2023 16:34:45 +0200 Subject: [PATCH 06/22] Load profile --- .../Scene/Profile/MeProfileViewModel.swift | 24 ++++++++++++++++++- .../Scene/Profile/ProfileViewController.swift | 2 ++ Mastodon/Scene/Profile/ProfileViewModel.swift | 5 ++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index 46424ab5e..35da62eb7 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -32,5 +32,27 @@ final class MeProfileViewModel: ProfileViewModel { } .store(in: &disposeBag) } - + + override func viewDidLoad() { + + super.viewDidLoad() + + Task { + do { + + _ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value + + try await context.managedObjectContext.performChanges { + guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { + assertionFailure() + return + } + + self.me = me + } + } catch { + // do nothing? + } + } + } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 8e0d61ee4..8c99ffc50 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -287,6 +287,8 @@ extension ProfileViewController { bindTitleView() bindMoreBarButtonItem() bindPager() + + viewModel.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index de32b5607..063e20c31 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -176,9 +176,10 @@ class ProfileViewModel: NSObject { .assign(to: &$isPagingEnabled) } -} -extension ProfileViewModel { + func viewDidLoad() { + + } // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { From 58ddb2bd06b18997e83122e90515e75eee150147 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 1 Jun 2023 17:22:53 +0200 Subject: [PATCH 07/22] Fix build (#1053) --- Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 0426e2384..049683836 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -243,7 +243,8 @@ extension HomeTimelineViewController { let userDoesntFollowPeople: Bool if let managedObjectContext = self?.context.managedObjectContext, - let me = self?.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user { + let authContext = self?.authContext, + let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){ userDoesntFollowPeople = me.followersCount == 0 } else { userDoesntFollowPeople = true From 1bb8e64574e5ef9bc1ac2708a70a59b4c2ff1b6f Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 2 Jun 2023 10:49:00 +0200 Subject: [PATCH 08/22] Fix TabBar avatar loading --- .../Root/MainTab/MainTabBarController.swift | 44 +++++++++++-------- .../Service/API/APIService+HomeTimeline.swift | 7 ++- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 3867e8cbe..4f637c68d 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -292,28 +292,34 @@ extension MainTabBarController { } .store(in: &disposeBag) - if let user = authContext?.mastodonAuthenticationBox.authentication.user(in: 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() - } + 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() + } - // 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) + // 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) - context.authenticationService.updateActiveUserAccountPublisher - .sink { [weak self] in - self?.updateUserAccount() + self.context.authenticationService.updateActiveUserAccountPublisher + .sink { [weak self] in + self?.updateUserAccount() + } + .store(in: &self.disposeBag) + } else { + self.avatarURLObserver = nil } - .store(in: &disposeBag) - } else { - self.avatarURLObserver = nil - } + } + .store(in: &disposeBag) let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 91d053d5b..a3b4b4b08 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -12,6 +12,10 @@ import CoreDataStack import CommonOSLog import MastodonSDK +public extension Foundation.Notification.Name { + static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched") +} + extension APIService { public func homeTimeline( @@ -44,7 +48,8 @@ extension APIService { // 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. _ = try await authenticatedUserInfo(authenticationBox: authenticationBox).value - + NotificationCenter.default.post(name: .userFetched, object: nil) + try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { assertionFailure() From aa9a6ee7013d350b3ead5a9f296e3487d01716e5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 2 Jun 2023 11:10:29 +0200 Subject: [PATCH 09/22] Make account switching work again --- Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../Sources/MastodonCore/AuthenticationServiceProvider.swift | 4 ++-- .../Sources/MastodonCore/Service/AuthenticationService.swift | 2 +- ShareActionExtension/Scene/ShareViewController.swift | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 496808d1f..bd7ae4894 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -217,7 +217,7 @@ extension SceneCoordinator { let rootViewController: UIViewController do { - let _authentication = AuthenticationServiceProvider.shared.authentications.first + let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } self.authContext = _authContext diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 877f1211c..5a9c77228 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -50,8 +50,8 @@ public extension AuthenticationServiceProvider { authentications.first(where: { $0.userAccessToken == userAccessToken }) } - func sortByActivation() { // fixme: why do we need this? - authentications = authentications.sorted(by: { $0.activedAt > $1.activedAt }) + func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this? + return authentications.sorted(by: { $0.activedAt > $1.activedAt }) } func restore() { diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index b3ad468cc..d6024faad 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -119,7 +119,7 @@ public final class AuthenticationService: NSObject { } .assign(to: &$mastodonAuthenticationBoxes) - AuthenticationServiceProvider.shared.sortByActivation() + AuthenticationServiceProvider.shared.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation() } } diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 8ed018c99..6b2ed1323 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -179,8 +179,7 @@ extension ShareViewController { extension ShareViewController { private func setupAuthContext() throws -> AuthContext? { - AuthenticationServiceProvider.shared.sortByActivation() - let _authentication = AuthenticationServiceProvider.shared.authentications.first + let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } return _authContext } From 081727865f229504d197d41d2a428bce65ff748e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 2 Jun 2023 11:12:30 +0200 Subject: [PATCH 10/22] Show correct checkmark --- Mastodon/Scene/Account/AccountListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 16a5055c6..be09d376e 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -77,7 +77,7 @@ extension AccountListViewModel { switch item { case .authentication(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell - if let activeAuthentication = AuthenticationServiceProvider.shared.authentications.first + if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first { AccountListViewModel.configure( in: managedObjectContext, From c5cfca93ea062ed0fa1225520c14c6b34dd95e90 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 2 Jun 2023 11:21:54 +0200 Subject: [PATCH 11/22] Download not one but all authenticated users at start --- .../Service/API/APIService+HomeTimeline.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index a3b4b4b08..ac3c6402a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -47,12 +47,17 @@ 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. - _ = try await authenticatedUserInfo(authenticationBox: authenticationBox).value + 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) try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { - assertionFailure() + assertionFailure() return } From 4c9c34b6ce8db28e3eb0c48fb3d93684df28e17e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 5 Jun 2023 14:02:44 +0200 Subject: [PATCH 12/22] Migrate legacy authentications --- Mastodon/Supporting Files/AppDelegate.swift | 1 + .../AuthenticationServiceProvider.swift | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 79eaeb5cd..2778baad6 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let appContext = AppContext() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + AuthenticationServiceProvider.shared.migrateLegacyAuthenticationsIfRequired(in: appContext.managedObjectContext) AuthenticationServiceProvider.shared.restore() AppSecret.default.register() diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 5a9c77228..f7ec62718 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -6,10 +6,14 @@ import CoreDataStack import MastodonSDK import KeychainAccess import MastodonCommon +import os.log public class AuthenticationServiceProvider: ObservableObject { + private let logger = Logger(subsystem: "AuthenticationServiceProvider", category: "Authentication") + public static let shared = AuthenticationServiceProvider() private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID) + private let userDefaults: UserDefaults = .shared private init() {} @@ -63,6 +67,35 @@ public extension AuthenticationServiceProvider { return try? JSONDecoder().decode(MastodonAuthentication.self, from: data) } } + + func migrateLegacyAuthenticationsIfRequired(in context: NSManagedObjectContext) { + guard !userDefaults.didMigrateAuthentications else { return } + + defer { userDefaults.didMigrateAuthentications = true } + + do { + let request = NSFetchRequest(entityName: "MastodonAuthentication") + let legacyAuthentications = try context.fetch(request) + + self.authentications = legacyAuthentications.map { auth in + MastodonAuthentication( + identifier: auth.identifier, + domain: auth.domain, + username: auth.username, + appAccessToken: auth.appAccessToken, + userAccessToken: auth.userAccessToken, + clientID: auth.clientID, + clientSecret: auth.clientSecret, + createdAt: auth.createdAt, + updatedAt: auth.updatedAt, + activedAt: auth.activedAt, + userID: auth.userID + ) + } + } catch { + logger.log(level: .error, "Could not migrate legacy authentications") + } + } } // MARK: - Private @@ -73,3 +106,13 @@ private extension AuthenticationServiceProvider { } } } + +private extension UserDefaults { + @objc dynamic var didMigrateAuthentications: Bool { + get { + register(defaults: [#function: false]) + return bool(forKey: #function) + } + set { self[#function] = newValue } + } +} From 55afa02b529cb7716dae9238519fab41d102f2dd Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 5 Jun 2023 15:53:27 +0200 Subject: [PATCH 13/22] Try migrating old auth to keychain --- Mastodon/Supporting Files/AppDelegate.swift | 1 - .../Handler/SendPostIntentHandler.swift | 2 +- .../Sources/CoreDataStack/CoreDataStack.swift | 21 ++++++--- .../Sources/MastodonCore/AppContext.swift | 9 +++- .../AuthenticationServiceProvider.swift | 46 +++++++++++++------ 5 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 2778baad6..79eaeb5cd 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -18,7 +18,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let appContext = AppContext() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - AuthenticationServiceProvider.shared.migrateLegacyAuthenticationsIfRequired(in: appContext.managedObjectContext) AuthenticationServiceProvider.shared.restore() AppSecret.default.register() diff --git a/MastodonIntent/Handler/SendPostIntentHandler.swift b/MastodonIntent/Handler/SendPostIntentHandler.swift index b457b1c91..af56ad523 100644 --- a/MastodonIntent/Handler/SendPostIntentHandler.swift +++ b/MastodonIntent/Handler/SendPostIntentHandler.swift @@ -17,7 +17,7 @@ final class SendPostIntentHandler: NSObject { var disposeBag = Set() - let coreDataStack = CoreDataStack() + let coreDataStack = CoreDataStack(isInMemory: true) lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext lazy var api: APIService = { let backgroundManagedObjectContext = coreDataStack.newTaskContext() diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index 48e69e375..ce512f303 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -22,10 +22,15 @@ public final class CoreDataStack { self.storeDescriptions = storeDescriptions } - public convenience init(databaseName: String = "shared") { + public convenience init(databaseName: String = "shared", isInMemory: Bool) { let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName) - let storeDescription = NSPersistentStoreDescription(url: storeURL) - storeDescription.url = URL(fileURLWithPath: "/dev/null") /// in-memory store with all features in favor of NSInMemoryStoreType + let storeDescription: NSPersistentStoreDescription + if isInMemory { + storeDescription = NSPersistentStoreDescription(url: URL(string: "file:///dev/null")!) /// in-memory store with all features in favor of NSInMemoryStoreType + } else { + storeDescription = NSPersistentStoreDescription(url: storeURL) + storeDescription.isReadOnly = true + } self.init(persistentStoreDescriptions: [storeDescription]) } @@ -123,16 +128,18 @@ extension CoreDataStack { } } -extension CoreDataStack { - - public func rebuild() { +public extension CoreDataStack { + func tearDown() { let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!) try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil) + } + + func rebuild() { + tearDown() CoreDataStack.load(persistentContainer: persistentContainer) { [weak self] in guard let self = self else { return } self.didFinishLoad.value = true } } - } diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index d8aa06fae..12eea2519 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -47,7 +47,14 @@ public class AppContext: ObservableObject { .eraseToAnyPublisher() public init() { - let _coreDataStack = CoreDataStack() + + /// Migrate existing Authentications to new Keychain-Based format +// var _legacyCoreDataStack: CoreDataStack? = CoreDataStack(isInMemory: false) +// AuthenticationServiceProvider.shared.migrateLegacyAuthenticationsIfRequired(in: _legacyCoreDataStack!.persistentContainer.viewContext) +// _legacyCoreDataStack!.tearDown() +// _legacyCoreDataStack = nil + + let _coreDataStack = CoreDataStack(isInMemory: true) let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() coreDataStack = _coreDataStack diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index f7ec62718..81d0c2da7 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -69,27 +69,43 @@ public extension AuthenticationServiceProvider { } func migrateLegacyAuthenticationsIfRequired(in context: NSManagedObjectContext) { - guard !userDefaults.didMigrateAuthentications else { return } +// guard !userDefaults.didMigrateAuthentications else { return } defer { userDefaults.didMigrateAuthentications = true } do { - let request = NSFetchRequest(entityName: "MastodonAuthentication") + let request = NSFetchRequest(entityName: "MastodonAuthentication") let legacyAuthentications = try context.fetch(request) - self.authentications = legacyAuthentications.map { auth in - MastodonAuthentication( - identifier: auth.identifier, - domain: auth.domain, - username: auth.username, - appAccessToken: auth.appAccessToken, - userAccessToken: auth.userAccessToken, - clientID: auth.clientID, - clientSecret: auth.clientSecret, - createdAt: auth.createdAt, - updatedAt: auth.updatedAt, - activedAt: auth.activedAt, - userID: auth.userID + self.authentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in + guard + let identifier = auth.value(forKey: "identifier") as? UUID, + let domain = auth.value(forKey: "domain") as? String, + let username = auth.value(forKey: "username") as? String, + let appAccessToken = auth.value(forKey: "appAccessToken") as? String, + let userAccessToken = auth.value(forKey: "userAccessToken") as? String, + let clientID = auth.value(forKey: "clientID") as? String, + let clientSecret = auth.value(forKey: "clientSecret") as? String, + let createdAt = auth.value(forKey: "createdAt") as? Date, + let updatedAt = auth.value(forKey: "updatedAt") as? Date, + let activedAt = auth.value(forKey: "activedAt") as? Date, + let userID = auth.value(forKey: "userID") as? String + + else { + return nil + } + return MastodonAuthentication( + identifier: identifier, + domain: domain, + username: username, + appAccessToken: appAccessToken, + userAccessToken: userAccessToken, + clientID: clientID, + clientSecret: clientSecret, + createdAt: createdAt, + updatedAt: updatedAt, + activedAt: activedAt, + userID: userID ) } } catch { From 73909005de777ab22e0e26ad40baabc375f11b2c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 5 Jun 2023 17:21:32 +0200 Subject: [PATCH 14/22] Fix migration issues with duplicate persistent store --- .../Sources/CoreDataStack/CoreDataStack.swift | 1 - .../Sources/MastodonCore/AppContext.swift | 21 ++++++++++++------- .../AuthenticationServiceProvider.swift | 8 ++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index ce512f303..f7d8f1de7 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -29,7 +29,6 @@ public final class CoreDataStack { storeDescription = NSPersistentStoreDescription(url: URL(string: "file:///dev/null")!) /// in-memory store with all features in favor of NSInMemoryStoreType } else { storeDescription = NSPersistentStoreDescription(url: storeURL) - storeDescription.isReadOnly = true } self.init(persistentStoreDescriptions: [storeDescription]) } diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index 12eea2519..02966e2f8 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -47,15 +47,22 @@ public class AppContext: ObservableObject { .eraseToAnyPublisher() public init() { + + let authProvider = AuthenticationServiceProvider.shared + let _coreDataStack: CoreDataStack + if authProvider.authenticationMigrationRequired { + _coreDataStack = CoreDataStack(isInMemory: false) + authProvider.migrateLegacyAuthentications( + in: _coreDataStack.persistentContainer.viewContext + ) + } else { + _coreDataStack = CoreDataStack(isInMemory: true) + } - /// Migrate existing Authentications to new Keychain-Based format -// var _legacyCoreDataStack: CoreDataStack? = CoreDataStack(isInMemory: false) -// AuthenticationServiceProvider.shared.migrateLegacyAuthenticationsIfRequired(in: _legacyCoreDataStack!.persistentContainer.viewContext) -// _legacyCoreDataStack!.tearDown() -// _legacyCoreDataStack = nil - - let _coreDataStack = CoreDataStack(isInMemory: true) let _managedObjectContext = _coreDataStack.persistentContainer.viewContext + _coreDataStack.persistentContainer.persistentStoreDescriptions.forEach { + $0.url = URL(fileURLWithPath: "/dev/null") + } let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() coreDataStack = _coreDataStack managedObjectContext = _managedObjectContext diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 81d0c2da7..01b7bc6f3 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -68,9 +68,7 @@ public extension AuthenticationServiceProvider { } } - func migrateLegacyAuthenticationsIfRequired(in context: NSManagedObjectContext) { -// guard !userDefaults.didMigrateAuthentications else { return } - + func migrateLegacyAuthentications(in context: NSManagedObjectContext) { defer { userDefaults.didMigrateAuthentications = true } do { @@ -112,6 +110,10 @@ public extension AuthenticationServiceProvider { logger.log(level: .error, "Could not migrate legacy authentications") } } + + var authenticationMigrationRequired: Bool { + !userDefaults.didMigrateAuthentications + } } // MARK: - Private From 183f30306580a3fbc2c742a98bcc36a5043a4131 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 13 Jun 2023 12:36:48 +0200 Subject: [PATCH 15/22] Move extension to its own file --- .../AuthenticationServiceProvider.swift | 10 ---------- .../UserDefaults+Authentication.swift | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonCore/UserDefaults+Authentication.swift diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 01b7bc6f3..b8f743072 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -124,13 +124,3 @@ private extension AuthenticationServiceProvider { } } } - -private extension UserDefaults { - @objc dynamic var didMigrateAuthentications: Bool { - get { - register(defaults: [#function: false]) - return bool(forKey: #function) - } - set { self[#function] = newValue } - } -} diff --git a/MastodonSDK/Sources/MastodonCore/UserDefaults+Authentication.swift b/MastodonSDK/Sources/MastodonCore/UserDefaults+Authentication.swift new file mode 100644 index 000000000..d5262027c --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/UserDefaults+Authentication.swift @@ -0,0 +1,20 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public extension UserDefaults { + + enum Keys { + static let didMigrateAuthenticationsKey = "didMigrateAuthentications" + } + + @objc dynamic var didMigrateAuthentications: Bool { + get { + return bool(forKey: Keys.didMigrateAuthenticationsKey) + } + + set { + set(newValue, forKey: Keys.didMigrateAuthenticationsKey) + } + } +} From aede20f2c84adb812ef5289539004e182a119734 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 13 Jun 2023 13:05:05 +0200 Subject: [PATCH 16/22] Mark migration as succesful only in case of success --- .../AuthenticationServiceProvider.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index b8f743072..6e8e80971 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -68,14 +68,12 @@ public extension AuthenticationServiceProvider { } } - func migrateLegacyAuthentications(in context: NSManagedObjectContext) { - defer { userDefaults.didMigrateAuthentications = true } - + func migrateLegacyAuthentications(in context: NSManagedObjectContext) { do { let request = NSFetchRequest(entityName: "MastodonAuthentication") let legacyAuthentications = try context.fetch(request) - self.authentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in + let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in guard let identifier = auth.value(forKey: "identifier") as? UUID, let domain = auth.value(forKey: "domain") as? String, @@ -106,13 +104,21 @@ public extension AuthenticationServiceProvider { userID: userID ) } + + if migratedAuthentications.count != legacyAuthentications.count { + logger.log(level: .default, "Not all mitgrations could be migrated.") + } + + self.authentications = migratedAuthentications + userDefaults.didMigrateAuthentications = true } catch { + userDefaults.didMigrateAuthentications = false logger.log(level: .error, "Could not migrate legacy authentications") } } var authenticationMigrationRequired: Bool { - !userDefaults.didMigrateAuthentications + userDefaults.didMigrateAuthentications == false } } From 001404b1ae7f269ee3aa0728f4722cda56943564 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 13 Jun 2023 14:01:32 +0200 Subject: [PATCH 17/22] Slightly refactor authentication migration --- .../CoreData 8.xcdatamodel/contents | 2 +- .../AuthenticationServiceProvider.swift | 44 ++++++------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 8.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 8.xcdatamodel/contents index fe1fcd98a..b875fe28d 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 8.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 8.xcdatamodel/contents @@ -65,7 +65,7 @@ - + diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 6e8e80971..20ea9b906 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -70,38 +70,20 @@ public extension AuthenticationServiceProvider { func migrateLegacyAuthentications(in context: NSManagedObjectContext) { do { - let request = NSFetchRequest(entityName: "MastodonAuthentication") - let legacyAuthentications = try context.fetch(request) - - let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in - guard - let identifier = auth.value(forKey: "identifier") as? UUID, - let domain = auth.value(forKey: "domain") as? String, - let username = auth.value(forKey: "username") as? String, - let appAccessToken = auth.value(forKey: "appAccessToken") as? String, - let userAccessToken = auth.value(forKey: "userAccessToken") as? String, - let clientID = auth.value(forKey: "clientID") as? String, - let clientSecret = auth.value(forKey: "clientSecret") as? String, - let createdAt = auth.value(forKey: "createdAt") as? Date, - let updatedAt = auth.value(forKey: "updatedAt") as? Date, - let activedAt = auth.value(forKey: "activedAt") as? Date, - let userID = auth.value(forKey: "userID") as? String - - else { - return nil - } + let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest) + let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in return MastodonAuthentication( - identifier: identifier, - domain: domain, - username: username, - appAccessToken: appAccessToken, - userAccessToken: userAccessToken, - clientID: clientID, - clientSecret: clientSecret, - createdAt: createdAt, - updatedAt: updatedAt, - activedAt: activedAt, - userID: userID + identifier: auth.identifier, + domain: auth.domain, + username: auth.username, + appAccessToken: auth.appAccessToken, + userAccessToken: auth.userAccessToken, + clientID: auth.clientID, + clientSecret: auth.clientSecret, + createdAt: auth.createdAt, + updatedAt: auth.updatedAt, + activedAt: auth.activedAt, + userID: auth.userID ) } From c5bba298ac3fa871a34fe82335277b03418ac679 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 13 Jun 2023 14:33:45 +0200 Subject: [PATCH 18/22] Improve log statement :facepalm: Co-authored-by: Marcus Kida --- .../Sources/MastodonCore/AuthenticationServiceProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 20ea9b906..f3cd504a0 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -88,7 +88,7 @@ public extension AuthenticationServiceProvider { } if migratedAuthentications.count != legacyAuthentications.count { - logger.log(level: .default, "Not all mitgrations could be migrated.") + logger.log(level: .default, "Not all account authentications could be migrated.") } self.authentications = migratedAuthentications From c80a590306655cd56b5d416f1d59fc977c3b9f1d Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 29 Sep 2023 12:52:22 +0200 Subject: [PATCH 19/22] Fix compilation issue --- Mastodon/Coordinator/SceneCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index fa3ff1de8..331a0b0a7 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -533,7 +533,7 @@ private extension SceneCoordinator { viewController = activityViewController case .settings(let setting): guard let presentedOn = sender, - let accountName = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: appContext.managedObjectContext)?.username, + let accountName = authContext?.mastodonAuthenticationBox.authentication.username, let authContext else { return nil } From 544869c9f90130f63fa40fa4d35b1e3deeabf7a2 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 11 Oct 2023 15:37:32 +0200 Subject: [PATCH 20/22] Fix crash Class couldn't be found, so CoreData complained --- .../CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents index a87062959..ee57231f2 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents @@ -65,7 +65,7 @@ - + From 6df7cf8a80073b37e06ea301ea83058559c52156 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 11 Oct 2023 15:38:31 +0200 Subject: [PATCH 21/22] Don't use core data in memory (for now) --- MastodonIntent/Handler/SendPostIntentHandler.swift | 2 +- MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift | 2 +- MastodonSDK/Sources/MastodonCore/AppContext.swift | 11 +++-------- .../MastodonCore/AuthenticationServiceProvider.swift | 2 ++ 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/MastodonIntent/Handler/SendPostIntentHandler.swift b/MastodonIntent/Handler/SendPostIntentHandler.swift index af56ad523..b457b1c91 100644 --- a/MastodonIntent/Handler/SendPostIntentHandler.swift +++ b/MastodonIntent/Handler/SendPostIntentHandler.swift @@ -17,7 +17,7 @@ final class SendPostIntentHandler: NSObject { var disposeBag = Set() - let coreDataStack = CoreDataStack(isInMemory: true) + let coreDataStack = CoreDataStack() lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext lazy var api: APIService = { let backgroundManagedObjectContext = coreDataStack.newTaskContext() diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index 720e5df85..3a30e9443 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -20,7 +20,7 @@ public final class CoreDataStack { self.storeDescriptions = storeDescriptions } - public convenience init(databaseName: String = "shared", isInMemory: Bool) { + public convenience init(databaseName: String = "shared", isInMemory: Bool = false) { let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName) let storeDescription: NSPersistentStoreDescription if isInMemory { diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index b8cf3b259..c1e8933ee 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -48,21 +48,16 @@ public class AppContext: ObservableObject { public init() { let authProvider = AuthenticationServiceProvider.shared - let _coreDataStack: CoreDataStack + let _coreDataStack = CoreDataStack() if authProvider.authenticationMigrationRequired { - _coreDataStack = CoreDataStack(isInMemory: false) authProvider.migrateLegacyAuthentications( in: _coreDataStack.persistentContainer.viewContext ) - } else { - _coreDataStack = CoreDataStack(isInMemory: true) } - + let _managedObjectContext = _coreDataStack.persistentContainer.viewContext - _coreDataStack.persistentContainer.persistentStoreDescriptions.forEach { - $0.url = URL(fileURLWithPath: "/dev/null") - } let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() + coreDataStack = _coreDataStack managedObjectContext = _managedObjectContext backgroundManagedObjectContext = _backgroundManagedObjectContext diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index f3cd504a0..1d8b2d6cc 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -89,6 +89,8 @@ public extension AuthenticationServiceProvider { if migratedAuthentications.count != legacyAuthentications.count { logger.log(level: .default, "Not all account authentications could be migrated.") + } else { + logger.log(level: .default, "All account authentications were successful.") } self.authentications = migratedAuthentications From af9cce34ebef20f5ea6176eb5a95a3a87e182f27 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 11 Oct 2023 16:33:04 +0200 Subject: [PATCH 22/22] Receive some actions on main-queue --- .../Profile/Header/View/ProfileHeaderView+ViewModel.swift | 5 +++++ .../Sources/MastodonUI/ViewModel/RelationshipViewModel.swift | 1 + 2 files changed, 6 insertions(+) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 270f86b64..be0dce929 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -98,6 +98,7 @@ extension ProfileHeaderView.ViewModel { // follows you $relationshipActionOptionSet .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } + .receive(on: DispatchQueue.main) .sink { isFollowingBy in view.followsYouBlurEffectView.isHidden = !isFollowingBy } @@ -182,16 +183,19 @@ extension ProfileHeaderView.ViewModel { } .store(in: &disposeBag) $relationshipActionOptionSet + .receive(on: DispatchQueue.main) .sink { optionSet in let isBlocking = optionSet.contains(.blocking) let isBlockedBy = optionSet.contains(.blockingBy) let isSuspended = optionSet.contains(.suspended) let isNeedsHidden = isBlocking || isBlockedBy || isSuspended + view.bioMetaText.textView.isHidden = isNeedsHidden } .store(in: &disposeBag) // dashboard $isMyself + .receive(on: DispatchQueue.main) .sink { isMyself in if isMyself { view.statusDashboardView.postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.myPosts @@ -246,6 +250,7 @@ extension ProfileHeaderView.ViewModel { $isEditing, $isUpdating ) + .receive(on: DispatchQueue.main) .sink { relationshipActionOptionSet, isEditing, isUpdating in if relationshipActionOptionSet.contains(.edit) { // check .edit state and set .editing when isEditing diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 99cff19be..20f720d20 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -126,6 +126,7 @@ public final class RelationshipViewModel { $me, relationshipUpdatePublisher ) + .receive(on: DispatchQueue.main) .sink { [weak self] user, me, _ in guard let self = self else { return } self.update(user: user, me: me)