Tusker/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift

142 lines
5.0 KiB
Swift

//
// NotificationsPrefsView.swift
// Tusker
//
// Created by Shadowfacts on 4/6/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import UserNotifications
import UserAccounts
import PushNotifications
import TuskerComponents
struct NotificationsPrefsView: View {
@State private var error: NotificationsSetupError?
@State private var isSetup = AsyncToggle.Mode.off
@State private var pushProxyRegistration: PushProxyRegistration?
@ObservedObject private var userAccounts = UserAccountsManager.shared
var body: some View {
List {
enableSection
if isSetup == .on,
let pushProxyRegistration {
accountsSection(pushProxyRegistration: pushProxyRegistration)
}
}
.listStyle(.insetGrouped)
.appGroupedListBackground(container: PreferencesNavigationController.self)
.navigationTitle("Notifications")
}
private var enableSection: some View {
Section {
AsyncToggle("Push Notifications", mode: $isSetup, onChange: isSetupChanged(newValue:))
}
.appGroupedListRowBackground()
.alertWithData("An Error Occurred", data: $error) { error in
Button("OK") {}
} message: { error in
Text(error.localizedDescription)
}
.task { @MainActor in
pushProxyRegistration = PushManager.shared.proxyRegistration
isSetup = pushProxyRegistration != nil ? .on : .off
if !UIApplication.shared.isRegisteredForRemoteNotifications {
_ = await registerForRemoteNotifications()
}
}
}
private func accountsSection(pushProxyRegistration: PushProxyRegistration) -> some View {
Section {
ForEach(userAccounts.accounts) { account in
PushInstanceSettingsView(account: account, pushProxyRegistration: pushProxyRegistration)
}
}
.appGroupedListRowBackground()
}
private func isSetupChanged(newValue: Bool) async -> Bool {
if newValue {
return await startRegistration()
} else {
return await unregister()
}
}
private func startRegistration() async -> Bool {
let authorized: Bool
do {
// TODO: support .providesAppNotificationSettings
authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert])
} catch {
self.error = .requestingAuthorization(error)
return false
}
guard authorized else {
return false
}
return await registerForRemoteNotifications()
}
private func registerForRemoteNotifications() async -> Bool {
do {
pushProxyRegistration = try await PushManager.shared.register()
return true
} catch {
self.error = .registering(error)
return false
}
}
private func unregister() async -> Bool {
do {
try await PushManager.shared.unregister()
pushProxyRegistration = nil
for subscription in PushManager.shared.subscriptions {
if let account = UserAccountsManager.shared.getAccount(id: subscription.accountID) {
let mastodonController = MastodonController.getForAccount(account)
do {
try await mastodonController.deletePushSubscription()
PushManager.shared.removeSubscription(account: account)
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
// this is a bit of a hack. the PushInstanceSettingsViews need to know to update
// their @State variables after we remove the subscription
NotificationCenter.default.post(name: .pushSubscriptionRemoved, object: account.id)
} catch {
PushManager.logger.error("Erroring removing push subscription: \(String(describing: error))")
}
}
}
return true
} catch {
self.error = .unregistering(error)
return false
}
}
}
private enum NotificationsSetupError: LocalizedError {
case requestingAuthorization(any Error)
case registering(any Error)
case unregistering(any Error)
var errorDescription: String? {
switch self {
case .requestingAuthorization(let error):
"Notifications authorization request failed: \(error.localizedDescription)"
case .registering(let error):
"Registration failed: \(error.localizedDescription)"
case .unregistering(let error):
"Deactivation failed: \(error.localizedDescription)"
}
}
}
extension Notification.Name {
static let pushSubscriptionRemoved = Notification.Name("Tusker.pushSubscriptionRemoved")
}