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:
parent
428fa2b19d
commit
a53b454a92
@ -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 {
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user