Tusker/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift

167 lines
6.2 KiB
Swift

//
// PushInstanceSettingsView.swift
// Tusker
//
// Created by Shadowfacts on 4/7/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import UserAccounts
import Pachyderm
import PushNotifications
import TuskerComponents
struct PushInstanceSettingsView: View {
let account: UserAccountInfo
let pushProxyRegistration: PushProxyRegistration
@State private var mode: AsyncToggle.Mode
@State private var error: Error?
@State private var subscription: PushNotifications.PushSubscription?
@MainActor
init(account: UserAccountInfo, pushProxyRegistration: PushProxyRegistration) {
self.account = account
self.pushProxyRegistration = pushProxyRegistration
let subscription = PushManager.shared.pushSubscription(account: account)
self.subscription = subscription
self.mode = subscription == nil ? .off : .on
}
var body: some View {
VStack(alignment: .prefsAvatar) {
HStack {
PrefsAccountView(account: account)
Spacer()
AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
.labelsHidden()
}
PushSubscriptionView(account: account, subscription: subscription, updateSubscription: updateSubscription)
}
.alertWithData("An Error Occurred", data: $error) { data in
Button("OK") {}
} message: { data in
Text(data.localizedDescription)
}
}
private func updateNotificationsEnabled(enabled: Bool) async -> Bool {
if enabled {
do {
try await enableNotifications()
} catch {
PushManager.logger.error("Error creating instance subscription: \(String(describing: error))")
self.error = .enabling(error)
return false
}
} else {
do {
try await disableNotifications()
} catch {
PushManager.logger.error("Error removing instance subscription: \(String(describing: error))")
self.error = .disabling(error)
return false
}
}
return true
}
private func enableNotifications() async throws {
let subscription = try await PushManager.shared.createSubscription(account: account)
let req = Pachyderm.PushSubscription.create(
endpoint: pushProxyRegistration.endpoint,
// mastodon docs just say "Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve."
// other apps use SecKeyCopyExternalRepresentation which is documented to use X9.63 for elliptic curve keys
// and that seems to work
publicKey: subscription.secretKey.publicKey.x963Representation,
authSecret: subscription.authSecret,
alerts: .init(subscription.alerts),
policy: .init(subscription.policy)
)
let mastodonController = await MastodonController.getForAccount(account)
do {
let (result, _) = try await mastodonController.run(req)
PushManager.logger.debug("Push subscription \(result.id) created on \(account.instanceURL)")
self.subscription = subscription
} catch {
// if creation failed, remove the subscription locally as well
await PushManager.shared.removeSubscription(account: account)
throw error
}
}
private func disableNotifications() async throws {
let req = Pachyderm.PushSubscription.delete()
let mastodonController = await MastodonController.getForAccount(account)
_ = try await mastodonController.run(req)
await PushManager.shared.removeSubscription(account: account)
subscription = nil
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
}
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
let mastodonController = await MastodonController.getForAccount(account)
do {
let (result, _) = try await mastodonController.run(req)
PushManager.logger.debug("Push subscription \(result.id) updated on \(account.instanceURL)")
subscription?.alerts = alerts
subscription?.policy = policy
return true
} catch {
PushManager.logger.error("Error updating subscription: \(String(describing: error))")
self.error = .updating(error)
return false
}
}
}
private enum Error: LocalizedError {
case enabling(any Swift.Error)
case disabling(any Swift.Error)
case updating(any Swift.Error)
var errorDescription: String? {
switch self {
case .enabling(let error):
"Enabling push: \(error.localizedDescription)"
case .disabling(let error):
"Disabling push: \(error.localizedDescription)"
case .updating(let error):
"Updating settings: \(error.localizedDescription)"
}
}
}
private extension Pachyderm.PushSubscription.Alerts {
init(_ alerts: PushNotifications.PushSubscription.Alerts) {
self.init(
mention: alerts.contains(.mention),
status: alerts.contains(.status),
reblog: alerts.contains(.reblog),
follow: alerts.contains(.follow),
followRequest: alerts.contains(.followRequest),
favourite: alerts.contains(.favorite),
poll: alerts.contains(.poll),
update: alerts.contains(.update)
)
}
}
private extension Pachyderm.PushSubscription.Policy {
init(_ policy: PushNotifications.PushSubscription.Policy) {
switch policy {
case .all:
self = .all
case .followers:
self = .followers
case .followed:
self = .followed
}
}
}
//#Preview {
// PushInstanceSettingsView()
//}