From 05cfecb797d1633edf73afe72603f98f190c1035 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 13 Apr 2024 18:59:42 -0400 Subject: [PATCH] Fix push notifications on Pleroma/Akkoma and older Mastodon versions --- .../InstanceFeatures/InstanceFeatures.swift | 25 ++++++++++ .../Pachyderm/Model/PushSubscription.swift | 28 ++++++++--- Tusker/API/MastodonController+Push.swift | 8 +++- .../NotificationsPrefsView.swift | 12 ----- .../PushInstanceSettingsView.swift | 4 +- .../Notifications/PushSubscriptionView.swift | 47 ++++++++++++------- 6 files changed, 88 insertions(+), 36 deletions(-) diff --git a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift index 91b950c4..86fc5520 100644 --- a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift +++ b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift @@ -184,6 +184,31 @@ public final class InstanceFeatures: ObservableObject { hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil)) } + public var pushNotificationTypeStatus: Bool { + hasMastodonVersion(3, 3, 0) + } + + public var pushNotificationTypeFollowRequest: Bool { + hasMastodonVersion(3, 1, 0) + } + + public var pushNotificationTypeUpdate: Bool { + hasMastodonVersion(3, 5, 0) + } + + public var pushNotificationPolicy: Bool { + hasMastodonVersion(3, 5, 0) + } + + public var pushNotificationPolicyMissingFromResponse: Bool { + switch instanceType { + case .mastodon(_, let version): + return version >= Version(3, 5, 0) && version < Version(4, 1, 0) + default: + return false + } + } + public init() { } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift index b91d252d..d0405dcb 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/PushSubscription.swift @@ -9,11 +9,11 @@ import Foundation public struct PushSubscription: Decodable, Sendable { - public let id: String - public let endpoint: URL - public let serverKey: String - public let alerts: Alerts - public let policy: Policy + public var id: String + public var endpoint: URL + public var serverKey: String + public var alerts: Alerts + public var policy: Policy public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -27,7 +27,8 @@ public struct PushSubscription: Decodable, Sendable { self.endpoint = try container.decode(URL.self, forKey: .endpoint) self.serverKey = try container.decode(String.self, forKey: .serverKey) self.alerts = try container.decode(PushSubscription.Alerts.self, forKey: .alerts) - self.policy = try container.decode(PushSubscription.Policy.self, forKey: .policy) + // added in mastodon 4.1.0 + self.policy = try container.decodeIfPresent(PushSubscription.Policy.self, forKey: .policy) ?? .all } public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request { @@ -96,6 +97,21 @@ extension PushSubscription { self.update = update } + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: PushSubscription.Alerts.CodingKeys.self) + self.mention = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.mention) + // status added in mastodon 3.3.0 + self.status = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.status) ?? false + self.reblog = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.reblog) + self.follow = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.follow) + // follow_request added in 3.1.0 + self.followRequest = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.followRequest) ?? false + self.favourite = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.favourite) + self.poll = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.poll) + // update added in mastodon 3.5.0 + self.update = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.update) ?? false + } + private enum CodingKeys: String, CodingKey { case mention case status diff --git a/Tusker/API/MastodonController+Push.swift b/Tusker/API/MastodonController+Push.swift index 99569855..0acb5817 100644 --- a/Tusker/API/MastodonController+Push.swift +++ b/Tusker/API/MastodonController+Push.swift @@ -33,7 +33,13 @@ extension MastodonController { func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription { let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy)) - return try await run(req).0 + var result = try await run(req).0 + if instanceFeatures.pushNotificationPolicyMissingFromResponse { + // see https://github.com/mastodon/mastodon/issues/23145 + // so just assume if the request was successful that it worked + result.policy = .init(policy) + } + return result } func deletePushSubscription() async throws { diff --git a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift index 9f610585..5b6f4b53 100644 --- a/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift +++ b/Tusker/Screens/Preferences/Notifications/NotificationsPrefsView.swift @@ -13,7 +13,6 @@ import PushNotifications import TuskerComponents struct NotificationsPrefsView: View { - @State private var error: NotificationsSetupError? @ObservedObject private var userAccounts = UserAccountsManager.shared var body: some View { @@ -48,14 +47,3 @@ struct NotificationsPrefsView: View { .navigationTitle("Notifications") } } - -private enum NotificationsSetupError: LocalizedError { - case requestingAuthorization(any Error) - - var errorDescription: String? { - switch self { - case .requestingAuthorization(let error): - "Notifications authorization request failed: \(error.localizedDescription)" - } - } -} diff --git a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift index bf371095..a50c008a 100644 --- a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift @@ -14,6 +14,7 @@ import TuskerComponents struct PushInstanceSettingsView: View { let account: UserAccountInfo + let mastodonController: MastodonController @State private var mode: AsyncToggle.Mode @State private var error: Error? @State private var subscription: PushNotifications.PushSubscription? @@ -22,6 +23,7 @@ struct PushInstanceSettingsView: View { @MainActor init(account: UserAccountInfo) { self.account = account + self.mastodonController = .getForAccount(account) let subscription = PushManager.shared.pushSubscription(account: account) self.subscription = subscription self.mode = subscription == nil ? .off : .on @@ -35,7 +37,7 @@ struct PushInstanceSettingsView: View { AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:)) .labelsHidden() } - PushSubscriptionView(account: account, subscription: subscription, updateSubscription: updateSubscription) + PushSubscriptionView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription) } .alertWithData("An Error Occurred", data: $error) { data in Button("OK") {} diff --git a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift index 8c693dbd..de24c5c6 100644 --- a/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushSubscriptionView.swift @@ -13,12 +13,13 @@ import TuskerComponents struct PushSubscriptionView: View { let account: UserAccountInfo + let mastodonController: MastodonController let subscription: PushSubscription? let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool var body: some View { if let subscription { - PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription) + PushSubscriptionSettingsView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription) } else { Text("No notifications") .font(.callout) @@ -29,28 +30,25 @@ struct PushSubscriptionView: View { private struct PushSubscriptionSettingsView: View { let account: UserAccountInfo + let mastodonController: MastodonController let subscription: PushSubscription let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool @State private var isLoading: [PushSubscription.Alerts: Bool] = [:] - init(account: UserAccountInfo, subscription: PushSubscription, updateSubscription: @escaping (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool) { - self.account = account - self.subscription = subscription - self.updateSubscription = updateSubscription - } - var body: some View { 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) + if mastodonController.instanceFeatures.pushNotificationPolicy { + 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) } - .pickerStyle(.menu) } // this is the default value of the alignment guide, but this modifier is loading bearing .alignmentGuide(.prefsAvatar, computeValue: { dimension in @@ -63,18 +61,35 @@ private struct PushSubscriptionSettingsView: View { private var alertsToggles: some View { GroupBox("Get notifications for") { VStack { - toggle("All", alert: [.mention, .favorite, .reblog, .follow, .followRequest, .poll, .update]) + toggle("All", alert: allSupportedAlertTypes) toggle("Mentions", alert: .mention) toggle("Favorites", alert: .favorite) toggle("Reblogs", alert: .reblog) - toggle("Follows", alert: [.follow, .followRequest]) + if mastodonController.instanceFeatures.pushNotificationTypeFollowRequest { + toggle("Follows", alert: [.follow, .followRequest]) + } else { + toggle("Follows", alert: .follow) + } toggle("Polls finishing", alert: .poll) - toggle("Edits", alert: .update) + if mastodonController.instanceFeatures.pushNotificationTypeUpdate { + toggle("Edits", alert: .update) + } // status notifications not supported until we can enable/disable them in the app } } } + private var allSupportedAlertTypes: PushSubscription.Alerts { + var alerts: PushSubscription.Alerts = [.mention, .favorite, .reblog, .follow, .poll] + if mastodonController.instanceFeatures.pushNotificationTypeFollowRequest { + alerts.insert(.followRequest) + } + if mastodonController.instanceFeatures.pushNotificationTypeUpdate { + alerts.insert(.update) + } + return alerts + } + private func toggle(_ titleKey: LocalizedStringKey, alert: PushSubscription.Alerts) -> some View { let binding: Binding = Binding { isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off