diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 0ca1ee705..48946635e 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -218,35 +218,16 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate { guard let subscription = setting.activeSubscription, setting.domain == authenticationBox.domain, - setting.userID == authenticationBox.userID, - let legacyViewModel = NotificationService.shared.dequeueNotificationViewModel(mastodonAuthenticationBox: authenticationBox), let deviceToken = NotificationService.shared.deviceToken.value else { return } + setting.userID == authenticationBox.userID else { return } - let queryData = Mastodon.API.Subscriptions.QueryData( - policy: viewModel.selectedPolicy.subscriptionPolicy, - alerts: Mastodon.API.Subscriptions.QueryData.Alerts( - favourite: viewModel.notifyFavorites, - follow: viewModel.notifyNewFollowers, - reblog: viewModel.notifyBoosts, - mention: viewModel.notifyMentions, - poll: subscription.alert.poll - ) + NotificationService.shared.updatePushNotificationSubscription(subscription.objectID, for: authenticationBox, policy: viewModel.selectedPolicy.subscriptionPolicy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts( + favourite: viewModel.notifyFavorites, + follow: viewModel.notifyNewFollowers, + reblog: viewModel.notifyBoosts, + mention: viewModel.notifyMentions, + poll: subscription.alert.poll ) - let query = legacyViewModel.createSubscribeQuery( - deviceToken: deviceToken, - queryData: queryData, - mastodonAuthenticationBox: authenticationBox ) - - APIService.shared.createSubscription( - subscriptionObjectID: subscription.objectID, - query: query, - mastodonAuthenticationBox: authenticationBox - ).sink(receiveCompletion: { completion in - print(completion) - }, receiveValue: { output in - print(output) - }) - .store(in: &disposeBag) } func showNotificationSettings(_ viewController: UIViewController) { diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 42fcd1cd1..7fa8ca2f9 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -64,7 +64,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension AppDelegate { func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - NotificationService.shared.deviceToken.value = deviceToken + NotificationService.shared.registrationStatus.send(.registrationTokenReceived(deviceToken)) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) { + NotificationService.shared.registrationStatus.send(.error(error)) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift index 5b90eb8c3..725e084ba 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Subscriptions.swift @@ -13,43 +13,39 @@ import MastodonSDK extension APIService { - public func createSubscription( + public func subscribeToPushNotifications( subscriptionObjectID: NSManagedObjectID, query: Mastodon.API.Subscriptions.CreateSubscriptionQuery, mastodonAuthenticationBox: MastodonAuthenticationBox - ) -> AnyPublisher, Error> { + ) async throws -> Mastodon.Entity.Subscription { let authorization = mastodonAuthenticationBox.userAuthorization let domain = mastodonAuthenticationBox.domain - return Mastodon.API.Subscriptions.createSubscription( + let responseContent = try await Mastodon.API.Subscriptions.createSubscription( session: session, domain: domain, authorization: authorization, query: query ) - .flatMap { response -> AnyPublisher, Error> in - let managedObjectContext = self.backgroundManagedObjectContext - return managedObjectContext.performChanges { - guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else { - assertionFailure() - return - } - - subscription.alert.update(favourite: response.value.alerts.favourite) - subscription.alert.update(reblog: response.value.alerts.reblog) - subscription.alert.update(follow: response.value.alerts.follow) - subscription.alert.update(mention: response.value.alerts.mention) - - subscription.endpoint = response.value.endpoint - subscription.serverKey = response.value.serverKey - subscription.userToken = authorization.accessToken - subscription.didUpdate(at: response.networkDate) + + let newSubscription = responseContent.value + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else { + assertionFailure() + return } - .setFailureType(to: Error.self) - .map { _ in return response } - .eraseToAnyPublisher() + subscription.alert.update(favourite: newSubscription.alerts.favourite) + subscription.alert.update(reblog: newSubscription.alerts.reblog) + subscription.alert.update(follow: newSubscription.alerts.follow) + subscription.alert.update(mention: newSubscription.alerts.mention) + + subscription.endpoint = newSubscription.endpoint + subscription.serverKey = newSubscription.serverKey + subscription.userToken = authorization.accessToken + subscription.didUpdate(at: responseContent.networkDate) } - .eraseToAnyPublisher() + return newSubscription } func cancelSubscription( diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index 7d3becbbd..3689c0891 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -16,6 +16,12 @@ import MastodonLocalization @MainActor public final class NotificationService { + public enum PushNotificationRegistrationStatus { + case registering + case error(Error) + case registrationTokenReceived(Data) + } + public static let shared = { NotificationService() }() public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut" @@ -25,8 +31,8 @@ public final class NotificationService { let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue") // input + public let registrationStatus = CurrentValueSubject(.registering) public let isNotificationPermissionGranted = CurrentValueSubject(false) - public let deviceToken = CurrentValueSubject(nil) public let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) // output @@ -36,13 +42,22 @@ public final class NotificationService { public let requestRevealNotificationPublisher = PassthroughSubject() private init() { - AuthenticationServiceProvider.shared.currentActiveUser - .sink(receiveValue: { [weak self] auth in + Publishers.CombineLatest( + AuthenticationServiceProvider.shared.currentActiveUser, + registrationStatus + ) + .sink(receiveValue: { [weak self] auth, registrationStatus in guard let self = self else { return } // request permission when sign-in - guard auth != nil else { return } - self.requestNotificationPermission() + switch (auth, registrationStatus) { + case (nil, _): + break + case (_, .registrationTokenReceived): + self.requestNotificationPermissionAndUpdateSubscriptions() + case (_, .registering), (_, .error): + break + } }) .store(in: &disposeBag) @@ -73,18 +88,22 @@ public final class NotificationService { } extension NotificationService { - private func requestNotificationPermission() { + private func requestNotificationPermissionAndUpdateSubscriptions() { let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in guard let self = self else { return } - + guard self.isNotificationPermissionGranted.value != granted else { return } self.isNotificationPermissionGranted.value = granted - - if let _ = error { - // Handle the error here. + switch (granted, registrationStatus.value) { + case (true, .registrationTokenReceived(let token)): + Task { + await self.updatePushNotificationSubscriptions(deviceToken: token) + } + case (true, .error), (true, .registering): + break + case (false, _): + break } - - // Enable or disable features based on the authorization. } } } @@ -169,7 +188,7 @@ extension NotificationService { private func fetchLatestNotifications( pushNotification: MastodonPushNotification ) async throws { - guard let authenticationBox = try await authenticationBox(for: pushNotification) else { return } + guard let authenticationBox = authenticationBox(for: pushNotification) else { return } _ = try await APIService.shared.notifications( olderThan: nil, @@ -231,6 +250,62 @@ extension NotificationService { } +extension NotificationService { + private func updatePushNotificationSubscriptions(deviceToken: Data) async { + do { + for userAuthBox in AuthenticationServiceProvider.shared.mastodonAuthenticationBoxes { + guard let setting = SettingService.shared.setting(for: userAuthBox) else { continue } + guard let subscription = setting.activeSubscription else { continue } + + let queryData = Mastodon.API.Subscriptions.QueryData( + policy: subscription.policy, + alerts: Mastodon.API.Subscriptions.QueryData.Alerts( + favourite: subscription.alert.favourite, + follow: subscription.alert.follow, + reblog: subscription.alert.reblog, + mention: subscription.alert.mention, + poll: subscription.alert.poll + ) + ) + let query = NotificationService.createSubscribeQuery( + deviceToken: deviceToken, + queryData: queryData, + mastodonAuthenticationBox: userAuthBox + ) + + let createSubscriptionTask = Task { + try await APIService.shared.subscribeToPushNotifications( + subscriptionObjectID: subscription.objectID, + query: query, + mastodonAuthenticationBox: userAuthBox + ) + } + let _ = try await createSubscriptionTask.value + } + } catch { + assertionFailure("error creating push notification subscription") + } + } + + public func updatePushNotificationSubscription(_ subscriptionObjectID: NSManagedObjectID, for userAuthBox: MastodonAuthenticationBox, policy: Mastodon.API.Subscriptions.QueryData.Policy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts) { + guard case let .registrationTokenReceived(deviceToken) = registrationStatus.value else { return } + let queryData = Mastodon.API.Subscriptions.QueryData(policy: policy, alerts: alerts) + let query = NotificationService.createSubscribeQuery( + deviceToken: deviceToken, + queryData: queryData, + mastodonAuthenticationBox: userAuthBox + ) + + Task { + let _ = try await APIService.shared.subscribeToPushNotifications( + subscriptionObjectID: subscriptionObjectID, + query: query, + mastodonAuthenticationBox: userAuthBox + ) + } + } +} + // MARK: - NotificationViewModel extension NotificationService { @@ -251,8 +326,8 @@ extension NotificationService { } } -extension NotificationService.NotificationViewModel { - public func createSubscribeQuery( +extension NotificationService { + public static func createSubscribeQuery( deviceToken: Data, queryData: Mastodon.API.Subscriptions.QueryData, mastodonAuthenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift index 276a8c04c..f85da4082 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/SettingService.swift @@ -66,62 +66,11 @@ public final class SettingService { .sink { [weak self] mastodonAuthenticationBoxes, settings in guard let self = self else { return } guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return } - let currentSetting = settings.first(where: { setting in - return setting.domain == activeMastodonAuthenticationBox.domain - && setting.userID == activeMastodonAuthenticationBox.userID - }) + let currentSetting = setting(for: activeMastodonAuthenticationBox) self.currentSetting.value = currentSetting } .store(in: &disposeBag) - - Publishers.CombineLatest3( - notificationService.deviceToken, - currentSetting.eraseToAnyPublisher(), - AuthenticationServiceProvider.shared.$mastodonAuthenticationBoxes - ) - .compactMap { [weak self] deviceToken, setting, mastodonAuthenticationBoxes -> AnyPublisher, Error>? in - guard let self = self else { return nil } - guard let deviceToken = deviceToken else { return nil } - guard let setting = setting else { return nil } - guard let authenticationBox = mastodonAuthenticationBoxes.first else { return nil } - - guard let subscription = setting.activeSubscription else { return nil } - - guard setting.domain == authenticationBox.domain, - setting.userID == authenticationBox.userID else { return nil } - - let _viewModel = notificationService.dequeueNotificationViewModel( - mastodonAuthenticationBox: authenticationBox - ) - guard let viewModel = _viewModel else { return nil } - let queryData = Mastodon.API.Subscriptions.QueryData( - policy: subscription.policy, - alerts: Mastodon.API.Subscriptions.QueryData.Alerts( - favourite: subscription.alert.favourite, - follow: subscription.alert.follow, - reblog: subscription.alert.reblog, - mention: subscription.alert.mention, - poll: subscription.alert.poll - ) - ) - let query = viewModel.createSubscribeQuery( - deviceToken: deviceToken, - queryData: queryData, - mastodonAuthenticationBox: authenticationBox - ) - - return apiService.createSubscription( - subscriptionObjectID: subscription.objectID, - query: query, - mastodonAuthenticationBox: authenticationBox - ) - } - .debounce(for: .seconds(3), scheduler: DispatchQueue.main) // limit subscribe request emit time interval - .sink(receiveValue: { _ in - }) - .store(in: &disposeBag) } - } extension SettingService { @@ -138,3 +87,12 @@ extension SettingService { return alertController } } + +extension SettingService { + public func setting(for userAuthBox: MastodonAuthenticationBox) -> Setting? { + return settingFetchedResultController.settings.value.first(where: { setting in + return setting.domain == userAuthBox.domain + && setting.userID == userAuthBox.userID + }) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift index b66b89aad..abac38563 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift @@ -61,24 +61,21 @@ extension Mastodon.API.Subscriptions { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - authorization: User token. Could be nil if status is public - /// - Returns: `AnyPublisher` contains `Subscription` nested in the response + /// - Returns: `Subscription` public static func createSubscription( session: URLSession, domain: String, authorization: Mastodon.API.OAuth.Authorization, query: CreateSubscriptionQuery - ) -> AnyPublisher, Error> { + ) async throws -> Mastodon.Response.Content { let request = Mastodon.API.post( url: pushEndpointURL(domain: domain), query: query, authorization: authorization ) - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response) - return Mastodon.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() + let (data, response) = try await session.data(for: request) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) } /// Change types of notifications