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 case all, followed, followers
} }
public struct Alerts: OptionSet { public struct Alerts: OptionSet, Hashable {
public static let mention = Alerts(rawValue: 1 << 0) public static let mention = Alerts(rawValue: 1 << 0)
public static let status = Alerts(rawValue: 1 << 1) public static let status = Alerts(rawValue: 1 << 1)
public static let reblog = Alerts(rawValue: 1 << 2) public static let reblog = Alerts(rawValue: 1 << 2)

View File

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

View File

@ -32,9 +32,10 @@ struct PushInstanceSettingsView: View {
HStack { HStack {
PrefsAccountView(account: account) PrefsAccountView(account: account)
Spacer() 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 .alertWithData("An Error Occurred", data: $error) { data in
Button("OK") {} Button("OK") {}
@ -90,18 +91,36 @@ struct PushInstanceSettingsView: View {
subscription = nil subscription = nil
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)") 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 { private enum Error: LocalizedError {
case enabling(any Swift.Error) case enabling(any Swift.Error)
case disabling(any Swift.Error) case disabling(any Swift.Error)
case updating(any Swift.Error)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .enabling(let error): case .enabling(let error):
"Enabling push: \(error.localizedDescription)" "Enabling push: \(error.localizedDescription)"
case .disabling(let error): case .disabling(let error):
"Disabling push: \(error.localizedDescription)" "Disabling push: \(error.localizedDescription)"
case .updating(let error):
"Updating settings: \(error.localizedDescription)"
} }
} }
} }

View File

@ -13,10 +13,11 @@ import PushNotifications
struct PushSubscriptionView: View { struct PushSubscriptionView: View {
let account: UserAccountInfo let account: UserAccountInfo
let subscription: PushSubscription? let subscription: PushSubscription?
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Void
var body: some View { var body: some View {
if let subscription { if let subscription {
Text("wee") PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription)
} else { } else {
Text("No notifications") Text("No notifications")
.foregroundStyle(.secondary) .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 { //#Preview {
// PushSubscriptionView() // PushSubscriptionView()
//} //}

View File

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

View File

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