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 {
/// 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 {
open override var childForStatusBarStyle: UIViewController? {
return selectedViewController
}
@ -156,9 +155,26 @@ extension MainTabBarController {
}
.store(in: &disposeBag)
#if DEBUG
// selectedIndex = 3
#endif
// handle push notification. toggle entry when finish fetch latest notification
context.notificationService.hasUnreadPushNotification
.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)
// 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
setNeedsStatusBarAppearanceUpdate()
}

View File

@ -61,23 +61,25 @@ extension NotificationViewModel.LoadLatestState {
query: query,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
.sink { completion in
switch completion {
case .failure(let error):
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)
case .finished:
// handle isFetchingLatestTimeline in fetch controller delegate
break
}
stateMachine.enter(Idle.self)
} receiveValue: { response in
if response.value.isEmpty {
viewModel.isFetchingLatestNotification.value = false
}
.sink { completion in
switch completion {
case .failure(let error):
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)
case .finished:
// toggle unread state
viewModel.context.notificationService.hasUnreadPushNotification.value = false
// handle isFetchingLatestTimeline in fetch controller delegate
break
}
.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()
}
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")
// input
weak var apiService: APIService?
weak var authenticationService: AuthenticationService?
let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
let deviceToken = CurrentValueSubject<Data?, Never>(nil)
@ -27,10 +28,13 @@ final class NotificationService {
// output
/// [Token: UserID]
let notificationSubscriptionDict: [String: NotificationViewModel] = [:]
let hasUnreadPushNotification = CurrentValueSubject<Bool, Never>(false)
init(
apiService: APIService,
authenticationService: AuthenticationService
) {
self.apiService = apiService
self.authenticationService = authenticationService
authenticationService.mastodonAuthentications
@ -94,9 +98,15 @@ extension NotificationService {
}
return _notificationSubscription
}
func handlePushNotification(notificationID: Mastodon.Entity.Notification.ID) {
hasUnreadPushNotification.value = true
}
}
// MARK: - NotificationViewModel
extension NotificationService {
final class NotificationViewModel {
@ -141,4 +151,5 @@ extension NotificationService.NotificationViewModel {
return query
}
}

View File

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

View File

@ -7,6 +7,7 @@
import os.log
import UIKit
import UserNotifications
import AppShared
@main
@ -66,12 +67,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
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,
let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] present", ((#file as NSString).lastPathComponent), #line, #function)
guard let mastodonPushNotification = AppDelegate.mastodonPushNotification(from: notification) else {
completionHandler([])
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(
@ -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)
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(
session: URLSession,
domain: String,
notificationID: String,
notificationID: Mastodon.Entity.Notification.ID,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
let request = Mastodon.API.get(