diff --git a/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift b/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift index bafabcd0..b326363a 100644 --- a/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift +++ b/Packages/PushNotifications/Sources/PushNotifications/PushSubscription.swift @@ -53,8 +53,12 @@ public struct PushSubscription { self.policy = policy } - public enum Policy: String { + public enum Policy: String, CaseIterable, Identifiable { case all, followed, followers + + public var id: some Hashable { + self + } } public struct Alerts: OptionSet, Hashable { diff --git a/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift new file mode 100644 index 00000000..dc4ebd1a --- /dev/null +++ b/Packages/TuskerComponents/Sources/TuskerComponents/AsyncPicker.swift @@ -0,0 +1,83 @@ +// +// AsyncPicker.swift +// TuskerComponents +// +// Created by Shadowfacts on 4/9/24. +// + +import SwiftUI + +public struct AsyncPicker: View { + let titleKey: LocalizedStringKey + @available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent") + let labelHidden: Bool + let alignment: Alignment + @Binding var value: V + let onChange: (V) async -> Bool + let content: Content + @State private var isLoading = false + + public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) { + self.titleKey = titleKey + self.labelHidden = labelHidden + self.alignment = alignment + self._value = value + self.onChange = onChange + self.content = content() + } + + public var body: some View { + if #available(iOS 16.0, *) { + LabeledContent(titleKey) { + picker + } + } else if labelHidden { + picker + } else { + HStack { + Text(titleKey) + Spacer() + picker + } + } + } + + private var picker: some View { + ZStack(alignment: alignment) { + Picker(titleKey, selection: Binding(get: { + value + }, set: { newValue in + let oldValue = value + value = newValue + isLoading = true + Task { + let operationCompleted = await onChange(newValue) + if !operationCompleted { + value = oldValue + } + isLoading = false + } + })) { + content + } + .labelsHidden() + .opacity(isLoading ? 0 : 1) + + if isLoading { + ProgressView() + } + } + } +} + +#Preview { + @State var value = 0 + return AsyncPicker("", value: $value) { _ in + try! await Task.sleep(nanoseconds: NSEC_PER_SEC) + return true + } content: { + ForEach(0..<10) { + Text("\($0)").tag($0) + } + } +} diff --git a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift index 6240fa03..288f390c 100644 --- a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift +++ b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift @@ -70,6 +70,7 @@ struct NotificationsPrefsView: View { 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) diff --git a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift index dce41c53..8c693dbd 100644 --- a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift @@ -21,6 +21,7 @@ struct PushSubscriptionView: View { PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription) } else { Text("No notifications") + .font(.callout) .foregroundStyle(.secondary) } } @@ -39,15 +40,17 @@ private struct PushSubscriptionSettingsView: View { } var body: some View { - VStack(alignment: .prefsAvatar) { - toggle("All", alert: [.mention, .favorite, .reblog, .follow, .followRequest, .poll, .update]) - toggle("Mentions", alert: .mention) - toggle("Favorites", alert: .favorite) - toggle("Reblogs", alert: .reblog) - toggle("Follows", alert: [.follow, .followRequest]) - toggle("Polls", alert: .poll) - toggle("Edits", alert: .update) - // status notifications not supported until we can enable/disable them in the app + VStack { + alertsToggles + + AsyncPicker("From", alignment: .trailing, value: .constant(subscription.policy)) { newPolicy in + await updateSubscription(subscription.alerts, newPolicy) + } content: { + ForEach(PushSubscription.Policy.allCases) { + Text($0.displayName).tag($0) + } + } + .pickerStyle(.menu) } // this is the default value of the alignment guide, but this modifier is loading bearing .alignmentGuide(.prefsAvatar, computeValue: { dimension in @@ -57,6 +60,21 @@ private struct PushSubscriptionSettingsView: View { .padding(.leading, 38) } + private var alertsToggles: some View { + GroupBox("Get notifications for") { + VStack { + toggle("All", alert: [.mention, .favorite, .reblog, .follow, .followRequest, .poll, .update]) + toggle("Mentions", alert: .mention) + toggle("Favorites", alert: .favorite) + toggle("Reblogs", alert: .reblog) + toggle("Follows", alert: [.follow, .followRequest]) + toggle("Polls finishing", alert: .poll) + toggle("Edits", alert: .update) + // status notifications not supported until we can enable/disable them in the app + } + } + } + private func toggle(_ titleKey: LocalizedStringKey, alert: PushSubscription.Alerts) -> some View { let binding: Binding = Binding { isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off @@ -64,11 +82,11 @@ private struct PushSubscriptionSettingsView: View { isLoading[alert] = newValue == .loading } return AsyncToggle(titleKey, mode: binding) { - return await updateSubscription(alert: alert, isOn: $0) + return await updateAlert(alert, isOn: $0) } } - private func updateSubscription(alert: PushSubscription.Alerts, isOn: Bool) async -> Bool { + private func updateAlert(_ alert: PushSubscription.Alerts, isOn: Bool) async -> Bool { var newAlerts = subscription.alerts if isOn { newAlerts.insert(alert) @@ -79,6 +97,19 @@ private struct PushSubscriptionSettingsView: View { } } +private extension PushSubscription.Policy { + var displayName: String { + switch self { + case .all: + "Anyone" + case .followed: + "Accounts you follow" + case .followers: + "Your followers" + } + } +} + //#Preview { // PushSubscriptionView() //}