2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

feat: handle notification response

This commit is contained in:
CMK 2021-04-27 17:27:03 +08:00
parent 9c3e4a706e
commit ed9c2ddd8f
9 changed files with 148 additions and 27 deletions

View File

@ -46,6 +46,53 @@ extension UIViewController {
} }
extension UIViewController {
func viewController<T: UIViewController>(of type: T.Type) -> T? {
if let viewController = self as? T {
return viewController
}
// UITabBarController
if let tabBarController = self as? UITabBarController {
for tab in tabBarController.viewControllers ?? [] {
if let viewController = tab.viewController(of: type) {
return viewController
}
}
}
// UINavigationController
if let navigationController = self as? UINavigationController {
for page in navigationController.viewControllers {
if let viewController = page.viewController(of: type) {
return viewController
}
}
}
// UIPageController
if let pageViewController = self as? UIPageViewController {
for page in pageViewController.viewControllers ?? [] {
if let viewController = page.viewController(of: type) {
return viewController
}
}
}
// child view controller
for subview in self.view?.subviews ?? [] {
if let childViewController = subview.next as? UIViewController,
let viewController = childViewController.viewController(of: type) {
return viewController
}
}
return nil
}
}
extension UIViewController { extension UIViewController {
/// https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/ /// https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/

View File

@ -85,7 +85,6 @@ class MainTabBarController: UITabBarController {
extension MainTabBarController { extension MainTabBarController {
open override var childForStatusBarStyle: UIViewController? { open override var childForStatusBarStyle: UIViewController? {
return selectedViewController return selectedViewController
} }
@ -156,9 +155,26 @@ extension MainTabBarController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
#if DEBUG // handle push notification. toggle entry when finish fetch latest notification
// selectedIndex = 3 context.notificationService.hasUnreadPushNotification
#endif .receive(on: DispatchQueue.main)
.sink { [weak self] hasUnreadPushNotification in
guard let self = self else { return }
guard let notificationViewController = self.notificationViewController else { return }
let image = hasUnreadPushNotification ? UIImage(systemName: "bell.badge.fill")! : UIImage(systemName: "bell.fill")!
notificationViewController.tabBarItem.image = image
notificationViewController.navigationController?.tabBarItem.image = image
}
.store(in: &disposeBag)
} }
} }
extension MainTabBarController {
var notificationViewController: NotificationViewController? {
return viewController(of: NotificationViewController.self)
}
}

View File

@ -88,6 +88,11 @@ extension NotificationViewController {
tableView.deselectRow(with: transitionCoordinator, animated: animated) tableView.deselectRow(with: transitionCoordinator, animated: animated)
// fetch latest if has unread push notification
if context.notificationService.hasUnreadPushNotification.value {
viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
}
// needs trigger manually after onboarding dismiss // needs trigger manually after onboarding dismiss
setNeedsStatusBarAppearanceUpdate() setNeedsStatusBarAppearanceUpdate()
} }

View File

@ -61,23 +61,25 @@ extension NotificationViewModel.LoadLatestState {
query: query, query: query,
mastodonAuthenticationBox: activeMastodonAuthenticationBox mastodonAuthenticationBox: activeMastodonAuthenticationBox
) )
.sink { completion in .sink { completion in
switch completion { switch completion {
case .failure(let error): case .failure(let error):
viewModel.isFetchingLatestNotification.value = false viewModel.isFetchingLatestNotification.value = false
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
case .finished: case .finished:
// handle isFetchingLatestTimeline in fetch controller delegate // toggle unread state
break viewModel.context.notificationService.hasUnreadPushNotification.value = false
} // handle isFetchingLatestTimeline in fetch controller delegate
break
stateMachine.enter(Idle.self)
} receiveValue: { response in
if response.value.isEmpty {
viewModel.isFetchingLatestNotification.value = false
}
} }
.store(in: &viewModel.disposeBag)
stateMachine.enter(Idle.self)
} receiveValue: { response in
if response.value.isEmpty {
viewModel.isFetchingLatestNotification.value = false
}
}
.store(in: &viewModel.disposeBag)
} }
} }

View File

@ -64,4 +64,20 @@ extension APIService {
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func notification(
notificationID: Mastodon.Entity.Notification.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization
return Mastodon.API.Notifications.getNotification(
session: session,
domain: domain,
notificationID: notificationID,
authorization: authorization
)
}
} }

View File

@ -20,6 +20,7 @@ final class NotificationService {
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue") let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue")
// input // input
weak var apiService: APIService?
weak var authenticationService: AuthenticationService? weak var authenticationService: AuthenticationService?
let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false) let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
let deviceToken = CurrentValueSubject<Data?, Never>(nil) let deviceToken = CurrentValueSubject<Data?, Never>(nil)
@ -27,10 +28,13 @@ final class NotificationService {
// output // output
/// [Token: UserID] /// [Token: UserID]
let notificationSubscriptionDict: [String: NotificationViewModel] = [:] let notificationSubscriptionDict: [String: NotificationViewModel] = [:]
let hasUnreadPushNotification = CurrentValueSubject<Bool, Never>(false)
init( init(
apiService: APIService,
authenticationService: AuthenticationService authenticationService: AuthenticationService
) { ) {
self.apiService = apiService
self.authenticationService = authenticationService self.authenticationService = authenticationService
authenticationService.mastodonAuthentications authenticationService.mastodonAuthentications
@ -94,9 +98,15 @@ extension NotificationService {
} }
return _notificationSubscription return _notificationSubscription
} }
func handlePushNotification(notificationID: Mastodon.Entity.Notification.ID) {
hasUnreadPushNotification.value = true
}
} }
// MARK: - NotificationViewModel
extension NotificationService { extension NotificationService {
final class NotificationViewModel { final class NotificationViewModel {
@ -141,4 +151,5 @@ extension NotificationService.NotificationViewModel {
return query return query
} }
} }

View File

@ -61,6 +61,7 @@ class AppContext: ObservableObject {
apiService: _apiService apiService: _apiService
) )
let _notificationService = NotificationService( let _notificationService = NotificationService(
apiService: _apiService,
authenticationService: _authenticationService authenticationService: _authenticationService
) )
notificationService = _notificationService notificationService = _notificationService

View File

@ -7,6 +7,7 @@
import os.log import os.log
import UIKit import UIKit
import UserNotifications
import AppShared import AppShared
@main @main
@ -66,12 +67,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) { ) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function)
if let plaintext = notification.request.content.userInfo["plaintext"] as? Data, guard let mastodonPushNotification = AppDelegate.mastodonPushNotification(from: notification) else {
let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext) { completionHandler([])
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] present", ((#file as NSString).lastPathComponent), #line, #function) return
} }
completionHandler(.banner)
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)
completionHandler([.sound])
} }
func userNotificationCenter( func userNotificationCenter(
@ -81,6 +85,25 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
) { ) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification]", ((#file as NSString).lastPathComponent), #line, #function)
guard let mastodonPushNotification = AppDelegate.mastodonPushNotification(from: response.notification) else {
completionHandler()
return
}
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)
completionHandler()
}
private static func mastodonPushNotification(from notification: UNNotification) -> MastodonPushNotification? {
guard let plaintext = notification.request.content.userInfo["plaintext"] as? Data,
let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext) else {
return nil
}
return mastodonPushNotification
} }
} }

View File

@ -67,7 +67,7 @@ extension Mastodon.API.Notifications {
public static func getNotification( public static func getNotification(
session: URLSession, session: URLSession,
domain: String, domain: String,
notificationID: String, notificationID: Mastodon.Entity.Notification.ID,
authorization: Mastodon.API.OAuth.Authorization authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
let request = Mastodon.API.get( let request = Mastodon.API.get(