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:
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 {
|
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/
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user