Fix push notifications on Pleroma/Akkoma and older Mastodon versions

This commit is contained in:
Shadowfacts 2024-04-13 18:59:42 -04:00
parent 475b9911b1
commit 05cfecb797
6 changed files with 88 additions and 36 deletions

View File

@ -184,6 +184,31 @@ public final class InstanceFeatures: ObservableObject {
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil)) 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() { public init() {
} }

View File

@ -9,11 +9,11 @@
import Foundation import Foundation
public struct PushSubscription: Decodable, Sendable { public struct PushSubscription: Decodable, Sendable {
public let id: String public var id: String
public let endpoint: URL public var endpoint: URL
public let serverKey: String public var serverKey: String
public let alerts: Alerts public var alerts: Alerts
public let policy: Policy public var policy: Policy
public init(from decoder: any Decoder) throws { public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) 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.endpoint = try container.decode(URL.self, forKey: .endpoint)
self.serverKey = try container.decode(String.self, forKey: .serverKey) self.serverKey = try container.decode(String.self, forKey: .serverKey)
self.alerts = try container.decode(PushSubscription.Alerts.self, forKey: .alerts) 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<PushSubscription> { public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
@ -96,6 +97,21 @@ extension PushSubscription {
self.update = update self.update = update
} }
public init(from decoder: any Decoder) throws {
let container: KeyedDecodingContainer<PushSubscription.Alerts.CodingKeys> = 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 { private enum CodingKeys: String, CodingKey {
case mention case mention
case status case status

View File

@ -33,7 +33,13 @@ extension MastodonController {
func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription { 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)) 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 { func deletePushSubscription() async throws {

View File

@ -13,7 +13,6 @@ import PushNotifications
import TuskerComponents import TuskerComponents
struct NotificationsPrefsView: View { struct NotificationsPrefsView: View {
@State private var error: NotificationsSetupError?
@ObservedObject private var userAccounts = UserAccountsManager.shared @ObservedObject private var userAccounts = UserAccountsManager.shared
var body: some View { var body: some View {
@ -48,14 +47,3 @@ struct NotificationsPrefsView: View {
.navigationTitle("Notifications") .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)"
}
}
}

View File

@ -14,6 +14,7 @@ import TuskerComponents
struct PushInstanceSettingsView: View { struct PushInstanceSettingsView: View {
let account: UserAccountInfo let account: UserAccountInfo
let mastodonController: MastodonController
@State private var mode: AsyncToggle.Mode @State private var mode: AsyncToggle.Mode
@State private var error: Error? @State private var error: Error?
@State private var subscription: PushNotifications.PushSubscription? @State private var subscription: PushNotifications.PushSubscription?
@ -22,6 +23,7 @@ struct PushInstanceSettingsView: View {
@MainActor @MainActor
init(account: UserAccountInfo) { init(account: UserAccountInfo) {
self.account = account self.account = account
self.mastodonController = .getForAccount(account)
let subscription = PushManager.shared.pushSubscription(account: account) let subscription = PushManager.shared.pushSubscription(account: account)
self.subscription = subscription self.subscription = subscription
self.mode = subscription == nil ? .off : .on 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:)) AsyncToggle("\(account.instanceURL.host!) notifications enabled", labelHidden: true, mode: $mode, onChange: updateNotificationsEnabled(enabled:))
.labelsHidden() .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 .alertWithData("An Error Occurred", data: $error) { data in
Button("OK") {} Button("OK") {}

View File

@ -13,12 +13,13 @@ import TuskerComponents
struct PushSubscriptionView: View { struct PushSubscriptionView: View {
let account: UserAccountInfo let account: UserAccountInfo
let mastodonController: MastodonController
let subscription: PushSubscription? let subscription: PushSubscription?
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
var body: some View { var body: some View {
if let subscription { if let subscription {
PushSubscriptionSettingsView(account: account, subscription: subscription, updateSubscription: updateSubscription) PushSubscriptionSettingsView(account: account, mastodonController: mastodonController, subscription: subscription, updateSubscription: updateSubscription)
} else { } else {
Text("No notifications") Text("No notifications")
.font(.callout) .font(.callout)
@ -29,28 +30,25 @@ struct PushSubscriptionView: View {
private struct PushSubscriptionSettingsView: View { private struct PushSubscriptionSettingsView: View {
let account: UserAccountInfo let account: UserAccountInfo
let mastodonController: MastodonController
let subscription: PushSubscription let subscription: PushSubscription
let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool let updateSubscription: (PushSubscription.Alerts, PushSubscription.Policy) async -> Bool
@State private var isLoading: [PushSubscription.Alerts: 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 { var body: some View {
VStack { VStack {
alertsToggles alertsToggles
AsyncPicker("From", alignment: .trailing, value: .constant(subscription.policy)) { newPolicy in if mastodonController.instanceFeatures.pushNotificationPolicy {
await updateSubscription(subscription.alerts, newPolicy) AsyncPicker("From", alignment: .trailing, value: .constant(subscription.policy)) { newPolicy in
} content: { await updateSubscription(subscription.alerts, newPolicy)
ForEach(PushSubscription.Policy.allCases) { } content: {
Text($0.displayName).tag($0) 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 // this is the default value of the alignment guide, but this modifier is loading bearing
.alignmentGuide(.prefsAvatar, computeValue: { dimension in .alignmentGuide(.prefsAvatar, computeValue: { dimension in
@ -63,18 +61,35 @@ private struct PushSubscriptionSettingsView: View {
private var alertsToggles: some View { private var alertsToggles: some View {
GroupBox("Get notifications for") { GroupBox("Get notifications for") {
VStack { VStack {
toggle("All", alert: [.mention, .favorite, .reblog, .follow, .followRequest, .poll, .update]) toggle("All", alert: allSupportedAlertTypes)
toggle("Mentions", alert: .mention) toggle("Mentions", alert: .mention)
toggle("Favorites", alert: .favorite) toggle("Favorites", alert: .favorite)
toggle("Reblogs", alert: .reblog) 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("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 // 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 { private func toggle(_ titleKey: LocalizedStringKey, alert: PushSubscription.Alerts) -> some View {
let binding: Binding<AsyncToggle.Mode> = Binding { let binding: Binding<AsyncToggle.Mode> = Binding {
isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off isLoading[alert] == true ? .loading : subscription.alerts.contains(alert) ? .on : .off