diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 63e5fdc37..307d3a540 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -331,7 +331,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 */; }; @@ -1037,7 +1036,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 = ""; }; @@ -2404,7 +2402,6 @@ DB64BA462851F23300ADF1B7 /* Model */ = { isa = PBXGroup; children = ( - DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */, DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */, ); path = Model; @@ -4057,7 +4054,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 6b12506d3..223ac4588 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -57,12 +57,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 @@ -226,8 +222,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.authenticationSortedByActivation().first let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } self.authContext = _authContext @@ -538,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 } diff --git a/Mastodon/Diffable/Discovery/DiscoverySection.swift b/Mastodon/Diffable/Discovery/DiscoverySection.swift index d15d60f86..bb93ffc28 100644 --- a/Mastodon/Diffable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffable/Discovery/DiscoverySection.swift @@ -74,7 +74,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/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index 0c4a47b46..e6632c337 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -80,7 +80,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 48d6b7f03..edc4fbe2f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -24,7 +24,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, @@ -42,7 +42,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() @@ -68,7 +68,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() @@ -99,7 +99,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 1aa50f2ea..5919596bf 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -21,10 +21,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() @@ -33,30 +31,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 } @@ -85,7 +64,7 @@ extension AccountListViewModel { } enum Item: Hashable { - case authentication(record: ManagedObjectRecord) + case authentication(record: MastodonAuthentication) case addAccount } @@ -97,12 +76,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.authenticationSortedByActivation().first { AccountListViewModel.configure( + in: managedObjectContext, cell: cell, - authentication: authentication, + authentication: record, activeAuthentication: activeAuthentication ) } @@ -119,11 +98,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( @@ -168,16 +148,3 @@ extension AccountListViewModel { .joined(separator: ", ") } } - -// MARK: - NSFetchedResultsControllerDelegate -extension AccountListViewModel: NSFetchedResultsControllerDelegate { - 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 119ffa831..cb64e364b 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -66,8 +66,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) @@ -165,9 +164,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/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 2ed6b26ad..ed363f397 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -211,7 +211,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 diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 86ff2c269..1ca300c8f 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -292,10 +292,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) } diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 41b4d2355..192eefb65 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -193,41 +193,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 c2b8ca77d..f73094a8c 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -33,7 +33,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/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/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index 3deefd4d5..7f88d2ffe 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -15,7 +15,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, @@ -29,5 +29,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 a343983f8..c83401329 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -273,6 +273,8 @@ extension ProfileViewController { bindTitleView() bindMoreBarButtonItem() bindPager() + + viewModel.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { @@ -935,11 +937,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 9445b8a26..012ebf61b 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -82,7 +82,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) @@ -171,9 +171,10 @@ class ProfileViewModel: NSObject { .assign(to: &$isPagingEnabled) } -} -extension ProfileViewModel { + func viewDidLoad() { + + } // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index 8caf7d1f1..de987b512 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -59,7 +59,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 c6efa0857..cd0804b24 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -258,28 +258,34 @@ extension MainTabBarController { } .store(in: &disposeBag) - if let user = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user { - 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(_:))) @@ -451,9 +457,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 f3675799e..441b8ca29 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/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 2e02ca502..66ec904f7 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -79,7 +79,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( domain: authContext.mastodonAuthenticationBox.domain, entity: status, - me: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, + me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext), statusCache: nil, userCache: nil, networkDate: Date())) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index fada6734e..32a913587 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -126,7 +126,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/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 9004ad8cd..b457fe73f 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -17,6 +17,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/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 84d917eaf..7c6c33f8e 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -185,11 +185,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/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/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 @@ - + diff --git a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift index 0fdf55fec..3a30e9443 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/MastodonSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -20,9 +20,14 @@ public final class CoreDataStack { self.storeDescriptions = storeDescriptions } - public convenience init(databaseName: String = "shared") { + public convenience init(databaseName: String = "shared", isInMemory: Bool = false) { let storeURL = URL.storeURL(for: AppName.groupID, databaseName: databaseName) - let storeDescription = NSPersistentStoreDescription(url: storeURL) + 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) + } self.init(persistentStoreDescriptions: [storeDescription]) } @@ -115,16 +120,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/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 6f3c5f8eb..79715794b 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/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index 0ee653061..c1e8933ee 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -46,9 +46,18 @@ public class AppContext: ObservableObject { .eraseToAnyPublisher() public init() { + + let authProvider = AuthenticationServiceProvider.shared let _coreDataStack = CoreDataStack() + if authProvider.authenticationMigrationRequired { + authProvider.migrateLegacyAuthentications( + in: _coreDataStack.persistentContainer.viewContext + ) + } + let _managedObjectContext = _coreDataStack.persistentContainer.viewContext let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() + coreDataStack = _coreDataStack managedObjectContext = _managedObjectContext backgroundManagedObjectContext = _backgroundManagedObjectContext diff --git a/MastodonSDK/Sources/MastodonCore/AuthContext.swift b/MastodonSDK/Sources/MastodonCore/AuthContext.swift index ad8e7578d..cd11c49cb 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthContext.swift @@ -24,31 +24,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 { _ in - } 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..1d8b2d6cc --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -0,0 +1,116 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import Combine +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() {} + + @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 + 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 authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this? + return authentications.sorted(by: { $0.activedAt > $1.activedAt }) + } + + func restore() { + authentications = Self.keychain.allKeys().compactMap { + guard + let encoded = Self.keychain[$0], + let data = Data(base64Encoded: encoded) + else { return nil } + return try? JSONDecoder().decode(MastodonAuthentication.self, from: data) + } + } + + func migrateLegacyAuthentications(in context: NSManagedObjectContext) { + do { + let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest) + let migratedAuthentications = legacyAuthentications.compactMap { auth -> MastodonAuthentication? in + return 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 + ) + } + + 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 + userDefaults.didMigrateAuthentications = true + } catch { + userDefaults.didMigrateAuthentications = false + logger.log(level: .error, "Could not migrate legacy authentications") + } + } + + var authenticationMigrationRequired: Bool { + userDefaults.didMigrateAuthentications == false + } +} + +// 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 new file mode 100644 index 000000000..a9880472a --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -0,0 +1,107 @@ +// 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? + + internal var persistenceIdentifier: String { + "\(username)@\(domain)" + } + + 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(), + 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 5968f7a95..191917476 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -167,7 +167,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 2dafe4676..45543dd1f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -67,12 +67,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 @@ -116,10 +119,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 43a050ea2..8c4e48417 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -27,12 +27,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) @@ -60,10 +63,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 { @@ -108,7 +114,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 c5cc5801b..ec44afa93 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -28,12 +28,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 @@ -65,10 +68,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 { @@ -117,7 +123,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 2561ee675..8decfe632 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -36,7 +36,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) @@ -88,7 +88,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 } @@ -120,10 +120,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) @@ -144,7 +143,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 e5e330c0f..f90aab5d3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -35,7 +35,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 05670604d..f463501f6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift @@ -35,7 +35,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 b6a01998f..683a98166 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -36,8 +36,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 91c2f8201..c8d1bfb73 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -44,8 +44,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 97bd53469..5317cd4b0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -11,6 +11,10 @@ import CoreData import CoreDataStack import MastodonSDK +public extension Foundation.Notification.Name { + static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched") +} + extension APIService { public func homeTimeline( @@ -38,9 +42,21 @@ 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. + 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.authenticationRecord.object(in: managedObjectContext)?.user else { - assertionFailure() + 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 7b6b6c6c3..65f0eb811 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -66,13 +66,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 @@ -112,9 +114,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 505a06032..ef486448d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -35,7 +35,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( @@ -78,7 +78,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 0636f70c4..67d77463a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -29,7 +29,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 73b77fa3c..d3d5e1c15 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -27,11 +27,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 @@ -66,10 +68,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 7cbd63ca6..370ed4fcf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -41,7 +41,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 e1ef7b0f5..df607df93 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -27,7 +27,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 27900d493..0f4896949 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift @@ -33,7 +33,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 05ce66792..b67b79349 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -29,7 +29,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 79a391fe7..0a61fc687 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift @@ -73,7 +73,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 afa41b3e9..7006a7477 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -29,7 +29,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 b0ee77f26..b661c282b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift @@ -45,7 +45,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 551ec97de..000000000 --- a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+MastodonAuthentication.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// APIService+CoreData+MastodonAuthentication.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/2/3. -// - -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 2b05a98cd..71c619f19 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -21,10 +21,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( @@ -92,21 +91,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 @@ -122,10 +108,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) @@ -133,14 +118,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.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation() } } @@ -150,18 +128,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 } @@ -182,12 +151,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 @@ -202,19 +166,3 @@ extension AuthenticationService { } } - -// MARK: - NSFetchedResultsControllerDelegate -extension AuthenticationService: NSFetchedResultsControllerDelegate { - - 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 8444392a8..0d1509af8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -78,18 +78,8 @@ extension InstanceService { networkDate: response.networkDate ) - // 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 @@ -116,18 +106,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 b1f467385..f7c6c08cf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -41,7 +41,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 } @@ -100,13 +100,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( @@ -201,9 +201,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 { @@ -240,22 +239,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/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) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 8a1a1363a..1e669d654 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -156,7 +156,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 @@ -224,7 +224,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 { @@ -259,9 +259,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 }() @@ -319,7 +318,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 @@ -565,7 +564,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 @@ -621,7 +620,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 2ee2c91dc..5879eed4f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -237,9 +237,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 680ef039c..ab8b449d4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -686,9 +686,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/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) diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 26c2e872c..f30040150 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -160,8 +160,7 @@ extension ShareViewController { extension ShareViewController { private func setupAuthContext() throws -> AuthContext? { - let request = MastodonAuthentication.activeSortedFetchRequest // use active order - let _authentication = try context.managedObjectContext.fetch(request).first + let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().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 e311981f4..1f118aeeb 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -83,9 +83,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)