diff --git a/Mastodon/Scene/Settings/Notification Settings/NotificationSettingsViewController.swift b/Mastodon/Scene/Settings/Notification Settings/NotificationSettingsViewController.swift index f1d249e45..fa3a9cf72 100644 --- a/Mastodon/Scene/Settings/Notification Settings/NotificationSettingsViewController.swift +++ b/Mastodon/Scene/Settings/Notification Settings/NotificationSettingsViewController.swift @@ -1,8 +1,10 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit +import Combine import CoreDataStack import MastodonLocalization +import MastodonCore protocol NotificationSettingsViewControllerDelegate: AnyObject { func viewWillDisappear(_ viewController: UIViewController, viewModel: NotificationSettingsViewModel) @@ -16,11 +18,15 @@ class NotificationSettingsViewController: UIViewController { let tableView: UITableView var tableViewDataSource: UITableViewDiffableDataSource? + + var isNotificationPermissionGranted = NotificationService.shared.isNotificationPermissionGranted.value - let sections: [NotificationSettingsSection] + var sections: [NotificationSettingsSection] = [] var viewModel: NotificationSettingsViewModel + + var disposeBag = Set() - init(currentSetting: Setting?, notificationsEnabled: Bool) { + init(currentSetting: Setting?) { let activeSubscription = currentSetting?.activeSubscription let alert = activeSubscription?.alert viewModel = NotificationSettingsViewModel(selectedPolicy: activeSubscription?.notificationPolicy ?? .noone, @@ -29,27 +35,21 @@ class NotificationSettingsViewController: UIViewController { notifyFavorites: alert?.favourite ?? false, notifyNewFollowers: alert?.follow ?? false) - if notificationsEnabled { - sections = [ - NotificationSettingsSection(entries: [.policy]), - NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } ) - ] - } else { - sections = [ - NotificationSettingsSection(entries: [.notificationDisabled]), - NotificationSettingsSection(entries: [.policy]), - NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } ) - ] - } - tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(NotificationSettingTableViewCell.self, forCellReuseIdentifier: NotificationSettingTableViewCell.reuseIdentifier) tableView.register(NotificationSettingTableViewToggleCell.self, forCellReuseIdentifier: NotificationSettingTableViewToggleCell.reuseIdentifier) tableView.register(NotificationSettingsDisabledTableViewCell.self, forCellReuseIdentifier: NotificationSettingsDisabledTableViewCell.reuseIdentifier) - + super.init(nibName: nil, bundle: nil) - + + NotificationService.shared.isNotificationPermissionGranted + .receive(on: DispatchQueue.main) + .sink { [weak self] granted in + self?.reloadTableview(notificationsAllowed: granted) + } + .store(in: &disposeBag) + let tableViewDataSource = UITableViewDiffableDataSource(tableView: tableView) { [ weak self] tableView, indexPath, itemIdentifier in let cell: UITableViewCell @@ -64,14 +64,14 @@ class NotificationSettingsViewController: UIViewController { guard let self, let notificationCell = tableView.dequeueReusableCell(withIdentifier: NotificationSettingTableViewCell.reuseIdentifier, for: indexPath) as? NotificationSettingTableViewCell else { fatalError("WTF Wrong cell!?") } - notificationCell.configure(with: .policy, viewModel: self.viewModel, notificationsEnabled: notificationsEnabled) + notificationCell.configure(with: .policy, viewModel: self.viewModel, notificationsEnabled: isNotificationPermissionGranted) cell = notificationCell case .alert(let alert): guard let self, let toggleCell = tableView.dequeueReusableCell(withIdentifier: NotificationSettingTableViewToggleCell.reuseIdentifier, for: indexPath) as? NotificationSettingTableViewToggleCell else { fatalError("WTF Wrong cell!?") } - toggleCell.configure(with: alert, viewModel: self.viewModel, notificationsEnabled: notificationsEnabled) + toggleCell.configure(with: alert, viewModel: self.viewModel, notificationsEnabled: isNotificationPermissionGranted) toggleCell.delegate = self cell = toggleCell } @@ -88,29 +88,25 @@ class NotificationSettingsViewController: UIViewController { tableView.pinToParent() title = L10n.Scene.Settings.Notifications.title + + NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() - - var snapshot = NSDiffableDataSourceSnapshot() - - for section in sections { - snapshot.appendSections([section]) - snapshot.appendItems(section.entries) - } - - tableViewDataSource?.apply(snapshot, animatingDifferences: false) + reloadTableview(notificationsAllowed: isNotificationPermissionGranted) + checkIfPermissionGranted() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - if let snapshot = tableViewDataSource?.snapshot() { - tableViewDataSource?.applySnapshotUsingReloadData(snapshot) - } + checkIfPermissionGranted() + } + + @objc func willEnterForeground() { + checkIfPermissionGranted() } override func viewWillDisappear(_ animated: Bool) { @@ -118,6 +114,36 @@ class NotificationSettingsViewController: UIViewController { delegate?.viewWillDisappear(self, viewModel: viewModel) } + + func checkIfPermissionGranted() { + NotificationService.shared.requestUpdate(.allAccounts) + } + + func reloadTableview(notificationsAllowed: Bool) { + guard viewIfLoaded != nil else { return } + isNotificationPermissionGranted = notificationsAllowed + if notificationsAllowed { + sections = [ + NotificationSettingsSection(entries: [.policy]), + NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } ) + ] + } else { + sections = [ + NotificationSettingsSection(entries: [.notificationDisabled]), + NotificationSettingsSection(entries: [.policy]), + NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } ) + ] + } + + var snapshot = NSDiffableDataSourceSnapshot() + + for section in sections { + snapshot.appendSections([section]) + snapshot.appendItems(section.entries) + } + + tableViewDataSource?.applySnapshotUsingReloadData(snapshot) + } } extension NotificationSettingsViewController: UITableViewDelegate { diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 48946635e..376a3dd25 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -85,9 +85,8 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { navigationController.pushViewController(generalSettingsViewController, animated: true) case .notifications: - let currentSetting = SettingService.shared.currentSetting.value - let notificationsEnabled = NotificationService.shared.isNotificationPermissionGranted.value - let notificationViewController = NotificationSettingsViewController(currentSetting: currentSetting, notificationsEnabled: notificationsEnabled) + let currentSetting = SettingService.shared.currentSetting.value + let notificationViewController = NotificationSettingsViewController(currentSetting: currentSetting) notificationViewController.delegate = self navigationController.pushViewController(notificationViewController, animated: true) @@ -220,13 +219,14 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate { setting.domain == authenticationBox.domain, setting.userID == authenticationBox.userID else { return } - NotificationService.shared.updatePushNotificationSubscription(subscription.objectID, for: authenticationBox, policy: viewModel.selectedPolicy.subscriptionPolicy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts( - favourite: viewModel.notifyFavorites, - follow: viewModel.notifyNewFollowers, - reblog: viewModel.notifyBoosts, - mention: viewModel.notifyMentions, - poll: subscription.alert.poll - ) + NotificationService.shared.requestUpdate( + .singleAccount(subscriptionObjectID: subscription.objectID, userAuthBox: authenticationBox, policy: viewModel.selectedPolicy.subscriptionPolicy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts( + favourite: viewModel.notifyFavorites, + follow: viewModel.notifyNewFollowers, + reblog: viewModel.notifyBoosts, + mention: viewModel.notifyMentions, + poll: subscription.alert.poll) + ) ) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index f5d26a45f..c0ac09e49 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -16,6 +16,11 @@ import MastodonLocalization @MainActor public final class NotificationService { + public enum UpdateOperation { + case allAccounts + case singleAccount(subscriptionObjectID: NSManagedObjectID, userAuthBox: MastodonAuthenticationBox, policy: Mastodon.API.Subscriptions.QueryData.Policy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts) + } + public enum PushNotificationRegistrationStatus { case registering case errorRegisteringWithAPNS(Error) @@ -37,9 +42,17 @@ public final class NotificationService { public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut" - var disposeBag = Set() + private var disposeBag = Set() - let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue") + private let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue") + private var subscriptionUpdateQueue = [UpdateOperation]() + private var currentUpdateInProgress: UpdateOperation? = nil { + didSet { + if currentUpdateInProgress == nil { + doNextUpdate() + } + } + } // input public let registrationStatus = CurrentValueSubject(.registering) @@ -65,11 +78,11 @@ public final class NotificationService { case (nil, _): break case (_, .registrationTokenReceived), (_, .errorUpdatingSubscriptions): - self.requestNotificationPermissionAndUpdateSubscriptions() + self.requestUpdate(.allAccounts) case (_, .subscriptionsUpdated(_, let subscribedAccounts)): guard let userIdentifier = auth?.globallyUniqueUserIdentifier else { return } if !subscribedAccounts.contains(userIdentifier) { - self.requestNotificationPermissionAndUpdateSubscriptions() + self.requestUpdate(.allAccounts) } case (_, .registering), (_, .errorRegisteringWithAPNS): break @@ -104,25 +117,21 @@ public final class NotificationService { } extension NotificationService { - private func requestNotificationPermissionAndUpdateSubscriptions() { + private func requestNotificationPermissionAndUpdateSubscriptions() async throws { let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in - guard let self = self else { return } - guard self.isNotificationPermissionGranted.value != granted else { return } - self.isNotificationPermissionGranted.value = granted - switch (granted, registrationStatus.value) { - case (true, .registrationTokenReceived), (true, .errorUpdatingSubscriptions), (true, .subscriptionsUpdated): - guard let token = registrationStatus.value.deviceToken else { return } - Task { - await self.updatePushNotificationSubscriptions(deviceToken: token) - } - case (true, .errorRegisteringWithAPNS): - UIApplication.shared.registerForRemoteNotifications() - case (true, .registering): - break - case (false, _): - break - } + let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge]) + guard isNotificationPermissionGranted.value != granted else { return } + isNotificationPermissionGranted.value = granted + switch (granted, registrationStatus.value) { + case (true, .registrationTokenReceived), (true, .errorUpdatingSubscriptions), (true, .subscriptionsUpdated): + guard let token = registrationStatus.value.deviceToken else { return } + await updatePushNotificationSubscriptions(deviceToken: token) + case (true, .errorRegisteringWithAPNS): + UIApplication.shared.registerForRemoteNotifications() + case (true, .registering): + break + case (false, _): + break } } } @@ -321,7 +330,7 @@ extension NotificationService { } } - public func updatePushNotificationSubscription(_ subscriptionObjectID: NSManagedObjectID, for userAuthBox: MastodonAuthenticationBox, policy: Mastodon.API.Subscriptions.QueryData.Policy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts) { + private func updatePushNotificationSubscription(_ subscriptionObjectID: NSManagedObjectID, for userAuthBox: MastodonAuthenticationBox, policy: Mastodon.API.Subscriptions.QueryData.Policy, alerts: Mastodon.API.Subscriptions.QueryData.Alerts) async throws { guard case let .registrationTokenReceived(deviceToken) = registrationStatus.value else { return } let queryData = Mastodon.API.Subscriptions.QueryData(policy: policy, alerts: alerts) let query = NotificationService.createSubscribeQuery( @@ -329,13 +338,37 @@ extension NotificationService { queryData: queryData, mastodonAuthenticationBox: userAuthBox ) - - Task { - let _ = try await APIService.shared.subscribeToPushNotifications( - subscriptionObjectID: subscriptionObjectID, - query: query, - mastodonAuthenticationBox: userAuthBox - ) + let _ = try await APIService.shared.subscribeToPushNotifications( + subscriptionObjectID: subscriptionObjectID, + query: query, + mastodonAuthenticationBox: userAuthBox + ) + } +} + +extension NotificationService { + public func requestUpdate(_ updateOperation: UpdateOperation) { + subscriptionUpdateQueue.append(updateOperation) + doNextUpdate() + } + + private func doNextUpdate() { + guard currentUpdateInProgress == nil else { return } + guard !subscriptionUpdateQueue.isEmpty else { return } + currentUpdateInProgress = subscriptionUpdateQueue.removeFirst() + switch currentUpdateInProgress { + case .allAccounts: + Task { + try? await requestNotificationPermissionAndUpdateSubscriptions() + currentUpdateInProgress = nil + } + case let .singleAccount(subscriptionObjectID, userAuthBox, policy, alerts): + Task { + try? await updatePushNotificationSubscription(subscriptionObjectID, for: userAuthBox, policy: policy, alerts: alerts) + currentUpdateInProgress = nil + } + case nil: + break } } }