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

Check if notification permission has been granted when returning from the background.

This whole area needs work. This solution updates the subscriptions more often than necessary.

Contributes to IOS-384
This commit is contained in:
shannon 2025-04-01 15:17:52 -04:00
parent 428fa2b19d
commit a53b454a92
3 changed files with 131 additions and 72 deletions

View File

@ -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<NotificationSettingsSection, NotificationSettingEntry>?
var isNotificationPermissionGranted = NotificationService.shared.isNotificationPermissionGranted.value
let sections: [NotificationSettingsSection]
var sections: [NotificationSettingsSection] = []
var viewModel: NotificationSettingsViewModel
var disposeBag = Set<AnyCancellable>()
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<NotificationSettingsSection, NotificationSettingEntry>(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<NotificationSettingsSection, NotificationSettingEntry>()
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<NotificationSettingsSection, NotificationSettingEntry>()
for section in sections {
snapshot.appendSections([section])
snapshot.appendItems(section.entries)
}
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
}
}
extension NotificationSettingsViewController: UITableViewDelegate {

View File

@ -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)
)
)
}

View File

@ -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<AnyCancellable>()
private var disposeBag = Set<AnyCancellable>()
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<PushNotificationRegistrationStatus, Never>(.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
}
}
}