diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index d5905cbb7..5fd4c8256 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -167,6 +167,16 @@ extension MainTabBarController { notificationViewController.navigationController?.tabBarItem.image = image } .store(in: &disposeBag) + + context.notificationService.requestRevealNotificationPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] notificationID in + guard let self = self else { return } + self.coordinator.switchToTabBar(tab: .notification) + let threadViewModel = RemoteThreadViewModel(context: self.context, notificationID: notificationID) + self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index e79c355cf..e6e111018 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -47,4 +47,43 @@ final class RemoteThreadViewModel: ThreadViewModel { } .store(in: &disposeBag) } + + // FIXME: multiple account supports + init(context: AppContext, notificationID: Mastodon.Entity.Notification.ID) { + super.init(context: context, optionalStatus: nil) + + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let domain = activeMastodonAuthenticationBox.domain + context.apiService.notification( + notificationID: notificationID, + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + .retry(3) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote notification %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote notification %s fetched", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + guard let statusID = response.value.status?.id else { return } + + let managedObjectContext = context.managedObjectContext + let request = Status.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = Status.predicate(domain: domain, id: statusID) + guard let status = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return + } + self.rootItem.value = .root(statusObjectID: status.objectID, attribute: Item.StatusAttribute()) + } + .store(in: &disposeBag) + } + } diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index cbc0f9ed7..590842ce1 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -78,6 +78,34 @@ extension APIService { notificationID: notificationID, authorization: authorization ) + .flatMap { response -> AnyPublisher, Error> in + guard let status = response.value.status else { + return Just(response) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + + return APIService.Persist.persistStatus( + managedObjectContext: self.backgroundManagedObjectContext, + domain: domain, + query: nil, + response: response.map { _ in [status] }, + persistType: .lookUp, + requestMastodonUserID: nil, + log: OSLog.api + ) + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() } } diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index bfd96df79..75a5953ed 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -29,6 +29,7 @@ final class NotificationService { /// [Token: UserID] let notificationSubscriptionDict: [String: NotificationViewModel] = [:] let hasUnreadPushNotification = CurrentValueSubject(false) + let requestRevealNotificationPublisher = PassthroughSubject() init( apiService: APIService, diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 0cca6ddf5..73a13bed1 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -61,6 +61,8 @@ extension AppDelegate { // MARK: - UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { + + // notification present in the foreground func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, @@ -78,6 +80,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound]) } + // response to user action for notification func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -93,7 +96,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let notificationID = String(mastodonPushNotification.notificationID) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) appContext.notificationService.handlePushNotification(notificationID: notificationID) - + appContext.notificationService.requestRevealNotificationPublisher.send(notificationID) completionHandler() } @@ -105,6 +108,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { return mastodonPushNotification } + } extension AppContext {