diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 7387fa1e27..6c92f19553 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -65,7 +65,7 @@ final public class SceneCoordinator { } let domain = authentication.domain let userID = authentication.userID - let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID) + let isSuccess = try await AuthenticationServiceProvider.shared.activeMastodonUser(domain: domain, userID: userID) guard isSuccess else { return } self.setup() @@ -648,8 +648,8 @@ extension SceneCoordinator: SettingsCoordinatorDelegate { self.appContext.notificationService.clearNotificationCountForActiveUser() Task { @MainActor in - try await self.appContext.authenticationService.signOutMastodonUser( - authenticationBox: authenticationBox + try await AuthenticationServiceProvider.shared.signOutMastodonUser( + authentication: authenticationBox.authentication ) let userIdentifier = authenticationBox FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 516f63be24..75d51cb5a1 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -32,7 +32,7 @@ extension DataSourceFacade { authenticationBox: dependency.authenticationBox ).value - dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + AuthenticationServiceProvider.shared.fetchFollowingAndBlockedAsync() NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index f618b4c3c7..c942915edb 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -109,7 +109,7 @@ extension AccountListViewController: UITableViewDelegate { Task { @MainActor in do { - try await self.viewModel.context.authenticationService.signOutMastodonUser(authentication: record) + try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: record) let userIdentifier = record FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) @@ -144,7 +144,7 @@ extension AccountListViewController: UITableViewDelegate { case .authentication(let record): assert(Thread.isMainThread) Task { @MainActor in - let isActive = try await context.authenticationService.activeMastodonUser(domain: record.domain, userID: record.userID) + let isActive = try await AuthenticationServiceProvider.shared.activeMastodonUser(domain: record.domain, userID: record.userID) guard isActive else { return } self.coordinator.setup() } // end Task @@ -157,8 +157,8 @@ extension AccountListViewController: UITableViewDelegate { let logoutAction = UIAlertAction(title: L10n.Scene.AccountList.logoutAllAccounts, style: .destructive) { _ in Task { @MainActor in self.coordinator.showLoading() - for authenticationBox in self.context.authenticationService.mastodonAuthenticationBoxes { - try? await self.context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) + for authenticationBox in AuthenticationServiceProvider.shared.mastodonAuthenticationBoxes { + try? await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: authenticationBox.authentication) } self.coordinator.hideLoading() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index e67ca2a014..3816f6ffba 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -641,7 +641,7 @@ extension HomeTimelineViewController { guard let authContext = viewModel?.authenticationBox else { return } Task { @MainActor in - try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox) + try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: authenticationBox.authentication) let userIdentifier = authenticationBox FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) FileManager.default.invalidateNotificationsAll(for: userIdentifier) diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift index 22f77332bf..8bcfcf7c4a 100644 --- a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift @@ -143,7 +143,7 @@ class NotificationPolicyViewController: UIViewController { // MARK: - Action @objc private func save(_ sender: UIButton) { - guard let authenticationBox = viewModel.appContext.authenticationService.mastodonAuthenticationBoxes.first else { return } + guard let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return } Task { [weak self] in guard let self else { return } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 50ce70ff67..ef69aa8f67 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -125,7 +125,7 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { .authenticated.sink { (domain, account) in Task { @MainActor in do { - _ = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: account.id) + _ = try await AuthenticationServiceProvider.shared.activeMastodonUser(domain: domain, userID: account.id) FileManager.default.store(account: account, forUserID: MastodonUserIdentifier(domain: domain, userID: account.id)) self.coordinator.setup() diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index f31f4143b2..b2d7822b7f 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -151,7 +151,7 @@ extension MastodonPickServerViewController { .authenticated .asyncMap { domain, user -> Result in do { - let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) + let result = try await AuthenticationServiceProvider.shared.activeMastodonUser(domain: domain, userID: user.id) return .success(result) } catch { return .failure(error) diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift index dde2464f96..2067cf7cc0 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewModel.swift @@ -25,7 +25,7 @@ final class WelcomeViewModel { init(context: AppContext) { self.context = context - context.authenticationService.$mastodonAuthenticationBoxes + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes .map { !$0.isEmpty } .assign(to: &$needsShowDismissEntry) } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 4ab3acdf38..de5e8fea77 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -213,7 +213,7 @@ extension MainTabBarController { guard let profileTabItem = _profileTabItem else { return } profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback) - self.context.authenticationService.updateActiveUserAccountPublisher + AuthenticationServiceProvider.shared.updateActiveUserAccountPublisher .sink { [weak self] in self?.updateUserAccount() } diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 8aeb142f45..0a76d6b2e6 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -13,9 +13,9 @@ import MastodonUI @main class AppDelegate: UIResponder, UIApplicationDelegate { - - let appContext = AppContext() - + + var appContext: AppContext { return AppContext.shared } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AuthenticationServiceProvider.shared.prepareForUse() diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index a692c45f1d..c56064ba81 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -95,7 +95,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { AppContext.shared.statusFilterService.filterUpdatePublisher.send() // trigger authenticated user account update - AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send() + AuthenticationServiceProvider.shared.updateActiveUserAccountPublisher.send() if let shortcutItem = savedShortCutItem { Task { @@ -208,7 +208,7 @@ extension SceneDelegate { return false } - let _isActive = try? await coordinator.appContext.authenticationService.activeMastodonUser( + let _isActive = try? await AuthenticationServiceProvider.shared.activeMastodonUser( domain: authentication.domain, userID: authentication.userID ) diff --git a/MastodonIntent/Handler/FollowersCountIntentHandler.swift b/MastodonIntent/Handler/FollowersCountIntentHandler.swift index fa5b53bc25..287c2fab1e 100644 --- a/MastodonIntent/Handler/FollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/FollowersCountIntentHandler.swift @@ -18,15 +18,12 @@ class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling { func provideAccountOptionsCollection(for intent: FollowersCountIntent, searchTerm: String?) async throws -> INObjectCollection { guard let searchTerm = searchTerm, - let authenticationBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return INObjectCollection(items: []) } - let results = try await WidgetExtension.appContext + let results = try await AppContext.shared .apiService .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) diff --git a/MastodonIntent/Handler/HashtagIntentHandler.swift b/MastodonIntent/Handler/HashtagIntentHandler.swift index d5c922765d..60b721a5b8 100644 --- a/MastodonIntent/Handler/HashtagIntentHandler.swift +++ b/MastodonIntent/Handler/HashtagIntentHandler.swift @@ -3,14 +3,12 @@ import Foundation import Intents import MastodonSDK +import MastodonCore class HashtagIntentHandler: INExtension, HashtagIntentHandling { func provideHashtagOptionsCollection(for intent: HashtagIntent, searchTerm: String?) async throws -> INObjectCollection { - guard let authenticationBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + guard let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return INObjectCollection(items: []) } @@ -18,8 +16,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling { var results: [NSString] = [] if let searchTerm, searchTerm.isEmpty == false { - let searchResults = try await WidgetExtension.appContext - .apiService + let searchResults = try await AppContext.shared.apiService .search(query: .init(q: searchTerm, type: .hashtags), authenticationBox: authenticationBox) .value .hashtags @@ -28,7 +25,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling { results = searchResults } else { - let followedTags = try await WidgetExtension.appContext.apiService.getFollowedTags( + let followedTags = try await AppContext.shared.apiService.getFollowedTags( domain: authenticationBox.domain, query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), authenticationBox: authenticationBox) diff --git a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift index f493986303..b341a8e2a4 100644 --- a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift @@ -10,15 +10,12 @@ class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountIntentHa func provideAccountsOptionsCollection(for intent: MultiFollowersCountIntent, searchTerm: String?) async throws -> INObjectCollection { guard let searchTerm = searchTerm, - let authenticationBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return INObjectCollection(items: []) } - let results = try await WidgetExtension.appContext + let results = try await AppContext.shared .apiService .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index 61e8fb46e3..c22bd4761b 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -13,6 +13,7 @@ import CoreDataStack import AlamofireImage public class AppContext: ObservableObject { + public static let shared = AppContext() public var disposeBag = Set() @@ -21,7 +22,6 @@ public class AppContext: ObservableObject { public let backgroundManagedObjectContext: NSManagedObjectContext public let apiService: APIService - public let authenticationService: AuthenticationService public let emojiService: EmojiService public let publisherService: PublisherService @@ -45,7 +45,7 @@ public class AppContext: ObservableObject { .share() .eraseToAnyPublisher() - public init() { + private init() { let authProvider = AuthenticationServiceProvider.shared let _coreDataStack = CoreDataStack() @@ -65,45 +65,39 @@ public class AppContext: ObservableObject { let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext) apiService = _apiService - let _authenticationService = AuthenticationService( - managedObjectContext: _managedObjectContext, - backgroundManagedObjectContext: _backgroundManagedObjectContext, - apiService: _apiService - ) - authenticationService = _authenticationService +// let _authenticationService = AuthenticationService( +// managedObjectContext: _managedObjectContext, +// backgroundManagedObjectContext: _backgroundManagedObjectContext, +// apiService: _apiService +// ) +// authenticationService = _authenticationService emojiService = EmojiService( - apiService: apiService, - authenticationService: _authenticationService + apiService: apiService ) publisherService = .init(apiService: _apiService) let _notificationService = NotificationService( - apiService: _apiService, - authenticationService: _authenticationService + apiService: _apiService ) notificationService = _notificationService settingService = SettingService( apiService: _apiService, - authenticationService: _authenticationService, notificationService: _notificationService ) instanceService = InstanceService( - apiService: _apiService, - authenticationService: _authenticationService + apiService: _apiService ) blockDomainService = BlockDomainService( - backgroundManagedObjectContext: _backgroundManagedObjectContext, - authenticationService: _authenticationService + backgroundManagedObjectContext: _backgroundManagedObjectContext ) statusFilterService = StatusFilterService( - apiService: _apiService, - authenticationService: _authenticationService + apiService: _apiService ) documentStore = DocumentStore() diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 9a791ec9d9..7f5823fecf 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -15,7 +15,42 @@ public class AuthenticationServiceProvider: ObservableObject { private static let keychain = Keychain(service: "org.joinmastodon.app.authentications", accessGroup: AppName.groupID) private let userDefaults: UserDefaults = .shared - private init() {} + var disposeBag = Set() + + @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] + public let updateActiveUserAccountPublisher = PassthroughSubject() + + private init() { + $mastodonAuthenticationBoxes + .throttle(for: 3, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] boxes in + Task { [weak self] in + for authBox in boxes { + do { try await self?.fetchFollowedBlockedUserIds(authBox) } + catch {} + } + } + } + .store(in: &disposeBag) + + + // TODO: verify credentials for active authentication + + $authentications + .map { authentications -> [MastodonAuthenticationBox] in + return authentications + .sorted(by: { $0.activedAt > $1.activedAt }) + .compactMap { authentication -> MastodonAuthenticationBox? in + return MastodonAuthenticationBox(authentication: authentication) + } + } + .assign(to: &$mastodonAuthenticationBoxes) + + Task { + await prepareForUse() + authentications = authenticationSortedByActivation() + } + } @Published public var authentications: [MastodonAuthentication] = [] { didSet { @@ -83,6 +118,37 @@ public extension AuthenticationServiceProvider { return authentications.sorted(by: { $0.activedAt > $1.activedAt }) } + var activeAuthentication: MastodonAuthenticationBox? { + guard let active = authenticationSortedByActivation().first else { return nil } + return MastodonAuthenticationBox(authentication: active) + } + + func fetchFollowingAndBlockedAsync() { + /// We're dispatching this as a separate async call to not block the caller + /// Also we'll only be updating the current active user as the state will be refreshed upon user-change anyways + Task { + if let authBox = activeAuthentication { + do { try await fetchFollowedBlockedUserIds(authBox) } + catch {} + } + } + } + + func activeMastodonUser(domain: String, userID: String) async throws -> Bool { + var isActive = false + + AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID) + + isActive = true + + return isActive + } + + func signOutMastodonUser(authentication: MastodonAuthentication) async throws { + try await AuthenticationServiceProvider.shared.delete(authentication: authentication) + _ = try await AppContext.shared.apiService.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization) + } + @MainActor func prepareForUse() { if authentications.isEmpty { @@ -169,6 +235,7 @@ public extension AuthenticationServiceProvider { } // MARK: - Private +private typealias IterativeResponse = (ids: [String], maxID: String?) private extension AuthenticationServiceProvider { func persist(_ authentications: [MastodonAuthentication]) { DispatchQueue.main.async { @@ -177,4 +244,48 @@ private extension AuthenticationServiceProvider { } } } + + func fetchFollowedBlockedUserIds( + _ authBox: MastodonAuthenticationBox, + _ previousFollowingIDs: [String]? = nil, + _ maxID: String? = nil + ) async throws { + let apiService = AppContext.shared.apiService + + let followingResponse = try await fetchFollowing(maxID, apiService, authBox) + let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids + + if let nextMaxID = followingResponse.maxID { + return try await fetchFollowedBlockedUserIds(authBox, followingIds, nextMaxID) + } + + let blockedIds = try await apiService.getBlocked( + authenticationBox: authBox + ).value.map { $0.id } + + let followRequestIds = try await apiService.pendingFollowRequest(userID: authBox.userID, + authenticationBox: authBox) + .value.map { $0.id } + + authBox.inMemoryCache.followRequestedUserIDs = followRequestIds + authBox.inMemoryCache.followingUserIds = followingIds + authBox.inMemoryCache.blockedUserIds = blockedIds + } + + private func fetchFollowing( + _ maxID: String?, + _ apiService: APIService, + _ mastodonAuthenticationBox: MastodonAuthenticationBox + ) async throws -> IterativeResponse { + let response = try await apiService.following( + userID: mastodonAuthenticationBox.userID, + maxID: maxID, + authenticationBox: mastodonAuthenticationBox + ) + + let ids: [String] = response.value.map { $0.id } + let maxID: String? = response.link?.maxID + + return (ids, maxID) + } } diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index 6ec2ab4aa1..3ee2a9387f 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -169,7 +169,7 @@ private extension FeedDataController { func load(kind: MastodonFeed.Kind, maxID: MastodonStatus.ID?) async throws -> [MastodonFeed] { switch kind { case .home(let timeline): - await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService) + await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService) let response: Mastodon.Response.Content<[Mastodon.Entity.Status]> diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift deleted file mode 100644 index 03a0fad8b8..0000000000 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// AuthenticationService.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/2/3. -// - -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -private typealias IterativeResponse = (ids: [String], maxID: String?) - -public final class AuthenticationService: NSObject { - - var disposeBag = Set() - - // input - weak var apiService: APIService? - let managedObjectContext: NSManagedObjectContext // read-only - let backgroundManagedObjectContext: NSManagedObjectContext - let authenticationServiceProvider = AuthenticationServiceProvider.shared - - // output - @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] - - private func fetchFollowedBlockedUserIds( - _ authBox: MastodonAuthenticationBox, - _ previousFollowingIDs: [String]? = nil, - _ maxID: String? = nil - ) async throws { - guard let apiService else { return } - - let followingResponse = try await fetchFollowing(maxID, apiService, authBox) - let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids - - if let nextMaxID = followingResponse.maxID { - return try await fetchFollowedBlockedUserIds(authBox, followingIds, nextMaxID) - } - - let blockedIds = try await apiService.getBlocked( - authenticationBox: authBox - ).value.map { $0.id } - - let followRequestIds = try await apiService.pendingFollowRequest(userID: authBox.userID, - authenticationBox: authBox) - .value.map { $0.id } - - authBox.inMemoryCache.followRequestedUserIDs = followRequestIds - authBox.inMemoryCache.followingUserIds = followingIds - authBox.inMemoryCache.blockedUserIds = blockedIds - } - - private func fetchFollowing( - _ maxID: String?, - _ apiService: APIService, - _ mastodonAuthenticationBox: MastodonAuthenticationBox - ) async throws -> IterativeResponse { - let response = try await apiService.following( - userID: mastodonAuthenticationBox.userID, - maxID: maxID, - authenticationBox: mastodonAuthenticationBox - ) - - let ids: [String] = response.value.map { $0.id } - let maxID: String? = response.link?.maxID - - return (ids, maxID) - } - - public func fetchFollowingAndBlockedAsync() { - /// We're dispatching this as a separate async call to not block the caller - /// Also we'll only be updating the current active user as the state will be reflesh upon user-change anyways - Task { - if let authBox = mastodonAuthenticationBoxes.first { - do { try await fetchFollowedBlockedUserIds(authBox) } - catch {} - } - } - } - - public let updateActiveUserAccountPublisher = PassthroughSubject() - - init( - managedObjectContext: NSManagedObjectContext, - backgroundManagedObjectContext: NSManagedObjectContext, - apiService: APIService - ) { - self.managedObjectContext = managedObjectContext - self.backgroundManagedObjectContext = backgroundManagedObjectContext - self.apiService = apiService - - super.init() - - $mastodonAuthenticationBoxes - .throttle(for: 3, scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] boxes in - Task { [weak self] in - for authBox in boxes { - do { try await self?.fetchFollowedBlockedUserIds(authBox) } - catch {} - } - } - } - .store(in: &disposeBag) - - - // TODO: verify credentials for active authentication - - authenticationServiceProvider.$authentications - .map { authentications -> [MastodonAuthenticationBox] in - return authentications - .sorted(by: { $0.activedAt > $1.activedAt }) - .compactMap { authentication -> MastodonAuthenticationBox? in - return MastodonAuthenticationBox(authentication: authentication) - } - } - .assign(to: &$mastodonAuthenticationBoxes) - - AuthenticationServiceProvider.shared.authentications = AuthenticationServiceProvider.shared.authenticationSortedByActivation() - } - -} - -extension AuthenticationService { - - public func activeMastodonUser(domain: String, userID: String) async throws -> Bool { - var isActive = false - - AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID) - - isActive = true - - return isActive - } - - public func signOutMastodonUser(authentication: MastodonAuthentication) async throws { - try await AuthenticationServiceProvider.shared.delete(authentication: authentication) - _ = try await apiService?.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization) - } - - public func signOutMastodonUser(authenticationBox: MastodonAuthenticationBox) async throws { - do { - try await AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication) - } catch { - assertionFailure("Failed to delete Authentication: \(error)") - } - - // cancel push notification subscription - do { - _ = try await apiService?.cancelSubscription( - domain: authenticationBox.domain, - authorization: authenticationBox.userAuthorization - ) - } catch { - // do nothing - } - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift index 02b8bdccdc..59e7f575c4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/BlockDomainService.swift @@ -17,17 +17,14 @@ public final class BlockDomainService { // input weak var backgroundManagedObjectContext: NSManagedObjectContext? - weak var authenticationService: AuthenticationService? // output let blockedDomains = CurrentValueSubject<[String], Never>([]) init( - backgroundManagedObjectContext: NSManagedObjectContext, - authenticationService: AuthenticationService + backgroundManagedObjectContext: NSManagedObjectContext ) { self.backgroundManagedObjectContext = backgroundManagedObjectContext - self.authenticationService = authenticationService // backgroundManagedObjectContext.perform { // let _blockedDomains: [DomainBlock] = { diff --git a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift index 0512e897e9..d222a1a8f9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService+CustomEmojiViewModel+LoadState.swift @@ -34,7 +34,7 @@ extension EmojiService.CustomEmojiViewModel.LoadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel, - let authenticationBox = viewModel.service.authenticationService.mastodonAuthenticationBoxes.first, + let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication, let stateMachine else { return } let apiService = viewModel.service.apiService diff --git a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift index 7b28c40449..2a25b61009 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Emoji/EmojiService.swift @@ -11,14 +11,12 @@ import MastodonSDK public final class EmojiService { let apiService: APIService - let authenticationService: AuthenticationService let workingQueue = DispatchQueue(label: "org.joinmastodon.app.EmojiService.working-queue") private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:] - init(apiService: APIService, authenticationService: AuthenticationService) { + init(apiService: APIService) { self.apiService = apiService - self.authenticationService = authenticationService } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index d0670afe67..b8cb57bc76 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -18,19 +18,16 @@ public final class InstanceService { // input let backgroundManagedObjectContext: NSManagedObjectContext weak var apiService: APIService? - weak var authenticationService: AuthenticationService? // output init( - apiService: APIService, - authenticationService: AuthenticationService + apiService: APIService ) { self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext self.apiService = apiService - self.authenticationService = authenticationService - authenticationService.$mastodonAuthenticationBoxes + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes .receive(on: DispatchQueue.main) .compactMap { $0.first?.domain } .removeDuplicates() // prevent infinity loop @@ -47,16 +44,16 @@ extension InstanceService { func updateInstance(domain: String) async { guard let apiService else { return } - let response = try? await apiService.instance(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first) + let response = try? await apiService.instance(domain: domain, authenticationBox: AuthenticationServiceProvider.shared.activeAuthentication) .singleOutput() if response?.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true { - guard let instanceV2 = try? await apiService.instanceV2(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first).singleOutput() else { + guard let instanceV2 = try? await apiService.instanceV2(domain: domain, authenticationBox: AuthenticationServiceProvider.shared.activeAuthentication).singleOutput() else { return } self.updateInstanceV2(domain: domain, response: instanceV2) - if let translationResponse = try? await apiService.translationLanguages(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first).singleOutput() { + if let translationResponse = try? await apiService.translationLanguages(domain: domain, authenticationBox: AuthenticationServiceProvider.shared.activeAuthentication).singleOutput() { updateTranslationLanguages(domain: domain, response: translationResponse) } } else if let response { diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index 3449d89148..7558169c8d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -23,7 +23,6 @@ public final class NotificationService { // input weak var apiService: APIService? - weak var authenticationService: AuthenticationService? public let isNotificationPermissionGranted = CurrentValueSubject(false) public let deviceToken = CurrentValueSubject(nil) public let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) @@ -35,11 +34,9 @@ public final class NotificationService { public let requestRevealNotificationPublisher = PassthroughSubject() init( - apiService: APIService, - authenticationService: AuthenticationService + apiService: APIService ) { self.apiService = apiService - self.authenticationService = authenticationService AuthenticationServiceProvider.shared.$authentications .sink(receiveValue: { [weak self] mastodonAuthentications in @@ -52,7 +49,7 @@ public final class NotificationService { .store(in: &disposeBag) Publishers.CombineLatest( - authenticationService.$mastodonAuthenticationBoxes, + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes, applicationIconBadgeNeedsUpdate ) .receive(on: DispatchQueue.main) @@ -96,7 +93,6 @@ extension NotificationService { extension NotificationService { public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] { - guard let authenticationService = self.authenticationService else { return [] } var items: [UIApplicationShortcutItem] = [] for authentication in AuthenticationServiceProvider.shared.authentications { @@ -163,8 +159,7 @@ extension NotificationService { extension NotificationService { public func clearNotificationCountForActiveUser() { - guard let authenticationService = self.authenticationService else { return } - if let accessToken = authenticationService.mastodonAuthenticationBoxes.first?.userAuthorization.accessToken { + if let accessToken = AuthenticationServiceProvider.shared.activeAuthentication?.userAuthorization.accessToken { UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) } @@ -191,7 +186,7 @@ extension NotificationService { ) async throws { // Subscription maybe failed to cancel when sign-out // Try cancel again if receive that kind push notification - guard let managedObjectContext = authenticationService?.managedObjectContext else { return } + let managedObjectContext = AppContext.shared.managedObjectContext guard let apiService = apiService else { return } let userAccessToken = pushNotification.accessToken @@ -218,8 +213,7 @@ extension NotificationService { } private func domain(for pushNotification: MastodonPushNotification) async throws -> String? { - guard let authenticationService = self.authenticationService else { return nil } - let managedObjectContext = authenticationService.managedObjectContext + let managedObjectContext = AppContext.shared.managedObjectContext return try await managedObjectContext.perform { let subscriptionRequest = NotificationSubscription.sortedFetchRequest subscriptionRequest.predicate = NotificationSubscription.predicate(userToken: pushNotification.accessToken) @@ -235,7 +229,6 @@ extension NotificationService { } private func authenticationBox(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationBox? { - guard self.authenticationService != nil else { return nil } let results = AuthenticationServiceProvider.shared.authentications.filter { $0.userAccessToken == pushNotification.accessToken } guard let authentication = results.first else { return nil } diff --git a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index 2206470360..dbcbbb291a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift @@ -19,7 +19,6 @@ public final class SettingService { // input weak var apiService: APIService? - weak var authenticationService: AuthenticationService? weak var notificationService: NotificationService? // output @@ -28,24 +27,21 @@ public final class SettingService { init( apiService: APIService, - authenticationService: AuthenticationService, notificationService: NotificationService ) { self.apiService = apiService - self.authenticationService = authenticationService self.notificationService = notificationService self.settingFetchedResultController = SettingFetchedResultController( - managedObjectContext: authenticationService.managedObjectContext, + managedObjectContext: AppContext.shared.managedObjectContext, additionalPredicate: nil ) // create setting (if non-exist) for authenticated users - authenticationService.$mastodonAuthenticationBoxes + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes .compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[MastodonAuthenticationBox], Never>? in guard let self = self else { return nil } - guard let authenticationService = self.authenticationService else { return nil } - let managedObjectContext = authenticationService.backgroundManagedObjectContext + let managedObjectContext = AppContext.shared.backgroundManagedObjectContext return managedObjectContext.performChanges { for authenticationBox in mastodonAuthenticationBoxes { let domain = authenticationBox.domain @@ -69,7 +65,7 @@ public final class SettingService { // bind current setting Publishers.CombineLatest( - authenticationService.$mastodonAuthenticationBoxes, + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes, settingFetchedResultController.settings ) .sink { [weak self] mastodonAuthenticationBoxes, settings in @@ -86,7 +82,7 @@ public final class SettingService { Publishers.CombineLatest3( notificationService.deviceToken, currentSetting.eraseToAnyPublisher(), - authenticationService.$mastodonAuthenticationBoxes + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes ) .compactMap { [weak self] deviceToken, setting, mastodonAuthenticationBoxes -> AnyPublisher, Error>? in guard let self = self else { return nil } diff --git a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift index 6be64bcbab..e0ebd62caf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/StatusFilterService.swift @@ -18,18 +18,15 @@ public final class StatusFilterService { // input weak var apiService: APIService? - weak var authenticationService: AuthenticationService? public let filterUpdatePublisher = PassthroughSubject() // output @Published public var activeFilters: [Mastodon.Entity.Filter] = [] init( - apiService: APIService, - authenticationService: AuthenticationService + apiService: APIService ) { self.apiService = apiService - self.authenticationService = authenticationService // fetch account filters every 300s // also trigger fetch when app resume from background @@ -44,7 +41,7 @@ public final class StatusFilterService { .store(in: &disposeBag) Publishers.CombineLatest( - authenticationService.$mastodonAuthenticationBoxes, + AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes, filterUpdatePublisher ) .flatMap { mastodonAuthenticationBoxes, _ -> AnyPublisher, Error>, Never> in diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift index 8139130274..77fdaf4312 100644 --- a/OpenInActionExtension/ActionRequestHandler.swift +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -16,11 +16,6 @@ import MastodonLocalization class ActionRequestHandler: NSObject, NSExtensionRequestHandling { var extensionContext: NSExtensionContext? var cancellables = [AnyCancellable]() - - /// Capturing a static shared instance of AppContext here as otherwise there - /// will be lifecycle issues and we don't want to keep multiple AppContexts around - /// in case there another Action Extension process is spawned - private static let appContext = AppContext() func beginRequest(with context: NSExtensionContext) { // Do not call super in an Action extension with no user interface @@ -64,10 +59,7 @@ class ActionRequestHandler: NSObject, NSExtensionRequestHandling { private extension ActionRequestHandler { func performSearch(for url: String) { guard - let activeAuthenticationBox = Self.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let activeAuthenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return doneWithResults(nil) } @@ -111,10 +103,7 @@ private extension ActionRequestHandler { guard let url = URL(string: query), let host = url.host, - let activeAuthenticationBox = Self.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let activeAuthenticationBox = AuthenticationServiceProvider.shared.activeAuthentication else { return doneWithInvalidLink() diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 368741c047..aac63919d4 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -308,6 +308,3 @@ extension ShareViewController { } } -extension AppContext { - static let shared = AppContext() -} diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 45dcbd1f9d..c207d88374 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -76,10 +76,7 @@ private extension FollowersCountWidgetProvider { await AuthenticationServiceProvider.shared.prepareForUse() guard - let authBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authBox = AuthenticationServiceProvider.shared.activeAuthentication else { guard !context.isPreview else { return completion(.placeholder) @@ -94,7 +91,7 @@ private extension FollowersCountWidgetProvider { } guard - let resultingAccount = try await WidgetExtension.appContext + let resultingAccount = try await AppContext.shared .apiService .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) .value diff --git a/WidgetExtension/Variants/Hashtag/HashtagWidget.swift b/WidgetExtension/Variants/Hashtag/HashtagWidget.swift index a8f43939f0..53244b969d 100644 --- a/WidgetExtension/Variants/Hashtag/HashtagWidget.swift +++ b/WidgetExtension/Variants/Hashtag/HashtagWidget.swift @@ -31,10 +31,7 @@ extension HashtagWidgetProvider { AuthenticationServiceProvider.shared.prepareForUse() guard - let authBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authBox = AuthenticationServiceProvider.shared.activeAuthentication else { if context.isPreview { return completion(.placeholder) @@ -54,7 +51,7 @@ extension HashtagWidgetProvider { Task { do { - let mostRecentStatuses = try await WidgetExtension.appContext + let mostRecentStatuses = try await AppContext.shared .apiService .hashtagTimeline(limit: 40, hashtag: desiredHashtag, authenticationBox: authBox) .value diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift index cd1abad86c..49be335564 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift @@ -83,10 +83,7 @@ private extension LatestFollowersWidgetProvider { AuthenticationServiceProvider.shared.prepareForUse() guard - let authBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authBox = AuthenticationServiceProvider.shared.activeAuthentication else { guard !context.isPreview else { return completion(.placeholder) @@ -96,7 +93,7 @@ private extension LatestFollowersWidgetProvider { var accounts = [LatestFollowersEntryAccountable]() - let followers = try await WidgetExtension.appContext + let followers = try await AppContext.shared .apiService .followers(userID: authBox.userID, maxID: nil, authenticationBox: authBox) .value diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 2cb9748d5a..feb10cc550 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -76,10 +76,7 @@ private extension MultiFollowersCountWidgetProvider { await AuthenticationServiceProvider.shared.prepareForUse() guard - let authBox = WidgetExtension.appContext - .authenticationService - .mastodonAuthenticationBoxes - .first + let authBox = AuthenticationServiceProvider.shared.activeAuthentication else { guard !context.isPreview else { return completion(.placeholder) @@ -101,7 +98,7 @@ private extension MultiFollowersCountWidgetProvider { for desiredAccount in desiredAccounts { guard - let resultingAccount = try await WidgetExtension.appContext + let resultingAccount = try await AppContext.shared .apiService .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) .value diff --git a/WidgetExtension/WidgetExtension.swift b/WidgetExtension/WidgetExtension.swift index 43dd95b6d2..470cccc6b5 100644 --- a/WidgetExtension/WidgetExtension.swift +++ b/WidgetExtension/WidgetExtension.swift @@ -4,6 +4,3 @@ import MastodonCore import MastodonSDK import MastodonLocalization -enum WidgetExtension { - static let appContext = AppContext() -}