forked from zelo72/mastodon-ios
feat: handle notification response
This commit is contained in:
parent
9c3e4a706e
commit
ed9c2ddd8f
|
@ -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/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ extension NotificationViewModel.LoadLatestState {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -95,8 +99,14 @@ 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ class AppContext: ObservableObject {
|
|||
apiService: _apiService
|
||||
)
|
||||
let _notificationService = NotificationService(
|
||||
apiService: _apiService,
|
||||
authenticationService: _authenticationService
|
||||
)
|
||||
notificationService = _notificationService
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue