Scaffolding for push subscription alert types

This commit is contained in:
Shadowfacts 2024-04-08 18:44:56 -04:00
parent 94c1eb2c81
commit bdd4a4d755
6 changed files with 103 additions and 35 deletions

View File

@ -57,7 +57,7 @@ public struct PushSubscription {
case all, followed, followers
}
public struct Alerts: OptionSet {
public struct Alerts: OptionSet, Hashable {
public static let mention = Alerts(rawValue: 1 << 0)
public static let status = Alerts(rawValue: 1 << 1)
public static let reblog = Alerts(rawValue: 1 << 2)

View File

@ -32,13 +32,7 @@ struct NotificationsPrefsView: View {
private var enableSection: some View {
Section {
HStack {
Text("Push Notifications")
Spacer()
TriStateToggle(titleKey: "Push Notifications Enabled", mode: $isSetup, onChange: isSetupChanged(newValue:))
}
TriStateToggle("Push Notifications", mode: $isSetup, onChange: isSetupChanged(newValue:))
}
.appGroupedListRowBackground()
.alertWithData("An Error Occurred", data: $error) { error in

View File

@ -32,9 +32,10 @@ struct PushInstanceSettingsView: View {
HStack {
PrefsAccountView(account: account)
Spacer()
TriStateToggle(titleKey: "\(account.instanceURL.host!) notifications enabled", mode: $mode, onChange: updateNotificationsEnabled(enabled:))
TriStateToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
.labelsHidden()
}
PushSubscriptionView(account: account, subscription: subscription)
PushSubscriptionView(account: account, subscription: subscription, updateSubscription: updateSubscription)
}
.alertWithData("An Error Occurred", data: $error) { data in
Button("OK") {}
@ -90,18 +91,36 @@ struct PushInstanceSettingsView: View {
subscription = nil
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
}
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async {
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
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
} catch {
PushManager.logger.error("Error updating subscription: \(String(describing: error))")
self.error = .updating(error)
}
}
}
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)"
}
}
}

View File

@ -13,10 +13,11 @@ import PushNotifications
struct PushSubscriptionView: View {
let account: UserAccountInfo
let subscription: PushSubscription?
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Void
var body: some View {
if let subscription {
Text("wee")
PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription)
} else {
Text("No notifications")
.foregroundStyle(.secondary)
@ -24,6 +25,51 @@ struct PushSubscriptionView: View {
}
}
private struct PushSubscriptionSettingsView: View {
let account: UserAccountInfo
let subscription: PushSubscription
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Void
@State private var isLoading: [PushSubscription.Alerts: Bool] = [:]
init(account: UserAccountInfo, subscription: PushSubscription, updateSubscription: @escaping (PushSubscription.Alerts, PushSubscription.Policy) async -> Void) {
self.account = account
self.subscription = subscription
self.updateSubscription = updateSubscription
}
var body: some View {
VStack(alignment: .prefsAvatar) {
TriStateToggle("Mentions", mode: alertsBinding(for: .mention)) {
await onChange(alert: .mention, value: $0)
}
}
// this is the default value of the alignment guide, but this modifier is loading bearing
.alignmentGuide(.prefsAvatar, computeValue: { dimension in
dimension[.leading]
})
// otherwise the flexible view makes the containing stack extend under the edge of the list row
.padding(.leading, 38)
}
private func alertsBinding(for alert: PushSubscription.Alerts) -> Binding<TriStateToggle.Mode> {
return Binding {
isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off
} set: { newValue in
isLoading[alert] = newValue == .loading
}
}
private func onChange(alert: PushSubscription.Alerts, value: Bool) async {
var newAlerts = subscription.alerts
if value {
newAlerts.insert(alert)
} else {
newAlerts.remove(alert)
}
await updateSubscription(newAlerts, subscription.policy)
}
}
//#Preview {
// PushSubscriptionView()
//}

View File

@ -10,40 +10,45 @@ import SwiftUI
struct TriStateToggle: View {
let titleKey: LocalizedStringKey
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
let labelHidden: Bool
@Binding var mode: Mode
let onChange: (Bool) async -> Void
@State private var isOn: Bool
init(titleKey: LocalizedStringKey, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Void) {
init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Void) {
self.titleKey = titleKey
self.labelHidden = labelHidden
self._mode = mode
self.onChange = onChange
self.isOn = mode.wrappedValue == .on
}
var body: some View {
toggleOrSpinner
.onChange(of: mode) { newValue in
switch newValue {
case .off:
isOn = false
case .loading:
break
case .on:
isOn = true
}
content
}
@ViewBuilder
private var content: some View {
if #available(iOS 16.0, *) {
LabeledContent(titleKey) {
toggleOrSpinner
}
} else if labelHidden {
toggleOrSpinner
} else {
HStack {
Text(titleKey)
Spacer()
toggleOrSpinner
}
}
}
@ViewBuilder
private var toggleOrSpinner: some View {
if mode == .loading {
ProgressView()
} else {
ZStack {
Toggle(titleKey, isOn: Binding {
isOn
mode == .on
} set: { newValue in
isOn = newValue
mode = .loading
Task {
await onChange(newValue)
@ -51,6 +56,11 @@ struct TriStateToggle: View {
}
})
.labelsHidden()
.opacity(mode == .loading ? 0 : 1)
if mode == .loading {
ProgressView()
}
}
}
@ -61,10 +71,9 @@ struct TriStateToggle: View {
}
}
#Preview {
@State var mode = TriStateToggle.Mode.on
return TriStateToggle(titleKey: "", mode: $mode) { _ in
return TriStateToggle("", mode: $mode) { _ in
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
}
}

View File

@ -14,7 +14,7 @@ struct PrefsAccountView: View {
let account: UserAccountInfo
var body: some View {
HStack {
HStack(spacing: 8) {
LocalAccountAvatarView(localAccountInfo: account)
VStack(alignment: .prefsAvatar) {
Text(verbatim: account.username)
@ -37,7 +37,7 @@ struct PrefsAccountView: View {
private struct AvatarAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
context[.leading]
}
}