Start account-specific push subscriptions

This commit is contained in:
Shadowfacts 2024-04-07 23:14:12 -04:00
parent 9fad2a882a
commit f98589b419
8 changed files with 298 additions and 55 deletions

View File

@ -12,13 +12,82 @@ public struct PushSubscription: Decodable, Sendable {
public let id: String
public let endpoint: URL
public let serverKey: String
// TODO: WTF is this?
// public let alerts
public let alerts: Alerts
public let policy: Policy
public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
return Request(method: .post, path: "/api/v1/push/subscription", body: ParametersBody([
"subscription[endpoint]" => endpoint.absoluteString,
"subscription[keys][p256dh]" => publicKey.base64EncodedString(),
"subscription[keys][auth]" => authSecret.base64EncodedString(),
"data[alerts][mention]" => alerts.mention,
"data[alerts][status]" => alerts.status,
"data[alerts][reblog]" => alerts.reblog,
"data[alerts][follow]" => alerts.follow,
"data[alerts][follow_request]" => alerts.followRequest,
"data[alerts][favourite]" => alerts.favourite,
"data[alerts][poll]" => alerts.poll,
"data[alerts][update]" => alerts.update,
"data[policy]" => policy.rawValue,
]))
}
public static func update(alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
return Request(method: .put, path: "/api/v1/push/subscription", body: ParametersBody([
"data[alerts][mention]" => alerts.mention,
"data[alerts][status]" => alerts.status,
"data[alerts][reblog]" => alerts.reblog,
"data[alerts][follow]" => alerts.follow,
"data[alerts][follow_request]" => alerts.followRequest,
"data[alerts][favourite]" => alerts.favourite,
"data[alerts][poll]" => alerts.poll,
"data[alerts][update]" => alerts.update,
"data[policy]" => policy.rawValue,
]))
}
public static func delete() -> Request<Empty> {
return Request(method: .delete, path: "/api/v1/push/subscription")
}
private enum CodingKeys: String, CodingKey {
case id
case endpoint
case serverKey = "server_key"
// case alerts
case alerts
case policy
}
}
extension PushSubscription {
public struct Alerts: Decodable, Sendable {
public let mention: Bool
public let status: Bool
public let reblog: Bool
public let follow: Bool
public let followRequest: Bool
public let favourite: Bool
public let poll: Bool
public let update: Bool
private enum CodingKeys: String, CodingKey {
case mention
case status
case reblog
case follow
case followRequest = "follow_request"
case favourite
case poll
case update
}
}
}
extension PushSubscription {
public enum Policy: String, Decodable, Sendable {
case all
case followed
case followers
case none
}
}

View File

@ -124,6 +124,7 @@
D64B967C2BC19C28002C8990 /* NotificationsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */; };
D64B967F2BC1D447002C8990 /* PushManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967E2BC1D447002C8990 /* PushManager.swift */; };
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
@ -132,6 +133,8 @@
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; };
D65A261B2BC3928A005EB5D8 /* TriStateToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65A261A2BC3928A005EB5D8 /* TriStateToggle.swift */; };
D65A261D2BC39399005EB5D8 /* PushInstanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */; };
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; };
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B552971F98300DABDFB /* ReportView.swift */; };
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */; };
@ -527,12 +530,15 @@
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPrefsView.swift; sourceTree = "<group>"; };
D64B967E2BC1D447002C8990 /* PushManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushManager.swift; sourceTree = "<group>"; };
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAccountView.swift; sourceTree = "<group>"; };
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriptionView.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
D65A261A2BC3928A005EB5D8 /* TriStateToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriStateToggle.swift; sourceTree = "<group>"; };
D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushInstanceSettingsView.swift; sourceTree = "<group>"; };
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
D65B4B552971F98300DABDFB /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSelectRulesView.swift; sourceTree = "<group>"; };
@ -1126,7 +1132,7 @@
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */,
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */,
D64B96822BC3892B002C8990 /* Notifications */,
D60089172981FEA4005B4D00 /* Tip Jar */,
D68A76EF2953910A001DA1B3 /* About */,
);
@ -1197,6 +1203,17 @@
path = Push;
sourceTree = "<group>";
};
D64B96822BC3892B002C8990 /* Notifications */ = {
isa = PBXGroup;
children = (
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */,
D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */,
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */,
D65A261A2BC3928A005EB5D8 /* TriStateToggle.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
D65A37F221472F300087646E /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -2082,6 +2099,7 @@
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
D65A261D2BC39399005EB5D8 /* PushInstanceSettingsView.swift in Sources */,
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
@ -2141,6 +2159,7 @@
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
D65A261B2BC3928A005EB5D8 /* TriStateToggle.swift in Sources */,
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
D64B967F2BC1D447002C8990 /* PushManager.swift in Sources */,
D61F75882932DB6000C0B37F /* StatusSwipeActions.swift in Sources */,
@ -2239,6 +2258,7 @@
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */,
D6CF5B832AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift in Sources */,
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,

View File

@ -12,6 +12,7 @@ import OSLog
import Sentry
#endif
import Pachyderm
import UserAccounts
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PushManager")
@ -40,10 +41,12 @@ struct PushManager {
}
@MainActor
protocol _PushManager: ObservableObject {
protocol _PushManager {
var enabled: Bool { get }
var pushProxyRegistration: PushProxyRegistration? { get }
func pushSubscription(account: UserAccountInfo) -> PushSubscription?
func register(transactionID: UInt64) async throws -> PushProxyRegistration
func unregister() async throws
func updateIfNecessary() async
@ -61,6 +64,10 @@ private class DisabledPushManager: _PushManager {
nil
}
func pushSubscription(account: UserAccountInfo) -> PushSubscription? {
nil
}
func register(transactionID: UInt64) async throws -> PushProxyRegistration {
throw Disabled()
}
@ -112,7 +119,6 @@ private class PushManagerImpl: _PushManager {
}
}
set {
objectWillChange.send()
defaults.setValue(newValue?.defaultsDict, forKey: "PushProxyRegistration")
}
}
@ -125,7 +131,6 @@ private class PushManagerImpl: _PushManager {
}
}
set {
objectWillChange.send()
defaults.setValue(newValue.map(\.defaultsDict), forKey: "PushSubscriptions")
}
}
@ -134,6 +139,10 @@ private class PushManagerImpl: _PushManager {
self.endpoint = endpoint
}
func pushSubscription(account: UserAccountInfo) -> PushSubscription? {
pushSubscriptions.first { $0.accountID == account.id }
}
func register(transactionID: UInt64) async throws -> PushProxyRegistration {
guard remoteNotificationsRegistrationContinuation == nil else {
throw PushRegistrationError.alreadyRegistering

View File

@ -56,7 +56,7 @@ struct NotificationsPrefsView: View {
private var accountsSection: some View {
Section {
ForEach(userAccounts.accounts) { account in
PrefsAccountView(account: account)
PushInstanceSettingsView(account: account)
}
}
.appGroupedListRowBackground()
@ -112,52 +112,6 @@ struct NotificationsPrefsView: View {
}
}
private struct TriStateToggle: View {
let titleKey: LocalizedStringKey
@Binding var mode: Mode
let onChange: (Bool) async -> Void
@State private var isOn: Bool = false
var body: some View {
toggleOrSpinner
.onChange(of: mode) { newValue in
switch newValue {
case .off:
isOn = false
case .loading:
break
case .on:
isOn = true
}
}
}
@ViewBuilder
private var toggleOrSpinner: some View {
if mode == .loading {
ProgressView()
} else {
Toggle(titleKey, isOn: Binding {
isOn
} set: { newValue in
isOn = newValue
mode = .loading
Task {
await onChange(newValue)
mode = newValue ? .on : .off
}
})
.labelsHidden()
}
}
enum Mode {
case off
case loading
case on
}
}
private enum NotificationsSetupError: LocalizedError {
case requestingAuthorization(any Error)
case registering(any Error)

View File

@ -0,0 +1,82 @@
//
// PushInstanceSettingsView.swift
// Tusker
//
// Created by Shadowfacts on 4/7/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import UserAccounts
import Pachyderm
struct PushInstanceSettingsView: View {
let account: UserAccountInfo
let pushProxyRegistration: PushProxyRegistration
@State private var mode: TriStateToggle.Mode
@State private var error: Error?
var body: some View {
VStack(alignment: .prefsAvatar) {
HStack {
PrefsAccountView(account: account)
TriStateToggle(titleKey: "\(account.instanceURL.host!) notifications enabled", mode: $mode, onChange: updateNotificationsEnabled(enabled:))
}
PushSubscriptionView(account: account)
}
.alertWithData("An Error Occurred", data: $error) { data in
Button("OK") {}
} message: { data in
Text(data.localizedDescription)
}
}
private func updateNotificationsEnabled(enabled: Bool) async {
if enabled {
do {
try await enableNotifications()
} catch {
self.error = .enabling(error)
}
} else {
do {
try await disableNotifications()
} catch {
self.error = .disabling(error)
}
}
}
private func enableNotifications() async throws {
let req = Pachyderm.PushSubscription.create(
endpoint: pushProxyRegistration.endpoint,
publicKey: <#T##Data#>,
authSecret: <#T##Data#>,
alerts: <#T##PushSubscription.Alerts#>,
policy: <#T##PushSubscription.Policy#>
)
}
private func disableNotifications() async throws {
}
}
private enum Error: LocalizedError {
case enabling(any Swift.Error)
case disabling(any Swift.Error)
var errorDescription: String? {
switch self {
case .enabling(let error):
"Enabling push: \(error.localizedDescription)"
case .disabling(let error):
"Disabling push: \(error.localizedDescription)"
}
}
}
//#Preview {
// PushInstanceSettingsView()
//}

View File

@ -0,0 +1,33 @@
//
// PushSubscriptionView.swift
// Tusker
//
// Created by Shadowfacts on 4/7/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import UserAccounts
struct PushSubscriptionView: View {
let account: UserAccountInfo
@State private var subscription: PushSubscription?
@MainActor
init(account: UserAccountInfo) {
self.account = account
self.subscription = PushManager.shared.pushSubscription(account: account)
}
var body: some View {
if let subscription {
} else {
Text("No notifications")
}
}
}
//#Preview {
// PushSubscriptionView()
//}

View File

@ -0,0 +1,63 @@
//
// TriStateToggle.swift
// Tusker
//
// Created by Shadowfacts on 4/7/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
struct TriStateToggle: View {
let titleKey: LocalizedStringKey
@Binding var mode: Mode
let onChange: (Bool) async -> Void
@State private var isOn: Bool = false
var body: some View {
toggleOrSpinner
.onChange(of: mode) { newValue in
switch newValue {
case .off:
isOn = false
case .loading:
break
case .on:
isOn = true
}
}
}
@ViewBuilder
private var toggleOrSpinner: some View {
if mode == .loading {
ProgressView()
} else {
Toggle(titleKey, isOn: Binding {
isOn
} set: { newValue in
isOn = newValue
mode = .loading
Task {
await onChange(newValue)
mode = newValue ? .on : .off
}
})
.labelsHidden()
}
}
enum Mode {
case off
case loading
case on
}
}
#Preview {
@State var mode = TriStateToggle.Mode.on
return TriStateToggle(titleKey: "", mode: $mode) { _ in
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
}
}

View File

@ -16,7 +16,7 @@ struct PrefsAccountView: View {
var body: some View {
HStack {
LocalAccountAvatarView(localAccountInfo: account)
VStack(alignment: .leading) {
VStack(alignment: .prefsAvatar) {
Text(verbatim: account.username)
.foregroundColor(.primary)
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
@ -28,10 +28,23 @@ struct PrefsAccountView: View {
.font(.caption)
.foregroundColor(.primary)
}
.alignmentGuide(.prefsAvatar, computeValue: { dimension in
dimension[.leading]
})
}
}
}
private struct AvatarAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
0
}
}
extension HorizontalAlignment {
static let prefsAvatar = HorizontalAlignment(AvatarAlignment.self)
}
//#Preview {
// PrefsAccountView()
//}