forked from shadowfacts/Tusker
Scaffolding for push subscription alert types
This commit is contained in:
parent
94c1eb2c81
commit
bdd4a4d755
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
//}
|
//}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue