diff --git a/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift b/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift index 07e24757..716f5156 100644 --- a/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift +++ b/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift @@ -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) diff --git a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift index 1717ce5d..5c15083d 100644 --- a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift +++ b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift @@ -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 diff --git a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift index 655da076..0dc4ecc3 100644 --- a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift @@ -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)" } } } diff --git a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift index 0cb39098..f33969fd 100644 --- a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift @@ -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 { + 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() //} diff --git a/Tusker/Screens/Preferences/Notifications/TriStateToggle.swift b/Tusker/Screens/Preferences/Notifications/TriStateToggle.swift index 32b48b9a..40936c94 100644 --- a/Tusker/Screens/Preferences/Notifications/TriStateToggle.swift +++ b/Tusker/Screens/Preferences/Notifications/TriStateToggle.swift @@ -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, onChange: @escaping (Bool) async -> Void) { + init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding, 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) } } diff --git a/Tusker/Screens/Preferences/PrefsAccountView.swift b/Tusker/Screens/Preferences/PrefsAccountView.swift index b4ed2617..8eda85ff 100644 --- a/Tusker/Screens/Preferences/PrefsAccountView.swift +++ b/Tusker/Screens/Preferences/PrefsAccountView.swift @@ -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] } }