forked from shadowfacts/Tusker
Fix push notifications on Pleroma/Akkoma and older Mastodon versions
This commit is contained in:
parent
475b9911b1
commit
05cfecb797
|
@ -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() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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") {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue