forked from shadowfacts/Tusker
More reliable registering/unregistering
This commit is contained in:
parent
3efa017942
commit
9fad2a882a
|
@ -123,6 +123,7 @@
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
D64B967C2BC19C28002C8990 /* NotificationsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */; };
|
D64B967C2BC19C28002C8990 /* NotificationsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */; };
|
||||||
D64B967F2BC1D447002C8990 /* PushManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967E2BC1D447002C8990 /* PushManager.swift */; };
|
D64B967F2BC1D447002C8990 /* PushManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967E2BC1D447002C8990 /* PushManager.swift */; };
|
||||||
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
||||||
|
@ -525,6 +526,7 @@
|
||||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPrefsView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.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>"; };
|
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>"; };
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1112,6 +1114,7 @@
|
||||||
children = (
|
children = (
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
|
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */,
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
||||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
||||||
|
@ -2084,6 +2087,7 @@
|
||||||
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D646DCD62A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift in Sources */,
|
D646DCD62A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import OSLog
|
||||||
#if canImport(Sentry)
|
#if canImport(Sentry)
|
||||||
import Sentry
|
import Sentry
|
||||||
#endif
|
#endif
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PushManager")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PushManager")
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ struct PushManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol _PushManager {
|
protocol _PushManager: ObservableObject {
|
||||||
var enabled: Bool { get }
|
var enabled: Bool { get }
|
||||||
var pushProxyRegistration: PushProxyRegistration? { get }
|
var pushProxyRegistration: PushProxyRegistration? { get }
|
||||||
|
|
||||||
|
@ -111,9 +112,23 @@ private class PushManagerImpl: _PushManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
objectWillChange.send()
|
||||||
defaults.setValue(newValue?.defaultsDict, forKey: "PushProxyRegistration")
|
defaults.setValue(newValue?.defaultsDict, forKey: "PushProxyRegistration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private(set) var pushSubscriptions: [PushSubscription] {
|
||||||
|
get {
|
||||||
|
if let array = defaults.array(forKey: "PushSubscriptions") as? [[String: Any]] {
|
||||||
|
return array.compactMap(PushSubscription.init(defaultsDict:))
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objectWillChange.send()
|
||||||
|
defaults.setValue(newValue.map(\.defaultsDict), forKey: "PushSubscriptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(endpoint: URL) {
|
init(endpoint: URL) {
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
|
@ -123,8 +138,8 @@ private class PushManagerImpl: _PushManager {
|
||||||
guard remoteNotificationsRegistrationContinuation == nil else {
|
guard remoteNotificationsRegistrationContinuation == nil else {
|
||||||
throw PushRegistrationError.alreadyRegistering
|
throw PushRegistrationError.alreadyRegistering
|
||||||
}
|
}
|
||||||
let deviceToken = try await getDeviceToken()
|
let deviceToken = try await getDeviceToken().hexEncodedString()
|
||||||
logger.debug("Got device token: \(deviceToken.hexEncodedString())")
|
logger.debug("Got device token: \(deviceToken)")
|
||||||
let registration: PushProxyRegistration
|
let registration: PushProxyRegistration
|
||||||
do {
|
do {
|
||||||
registration = try await register(deviceToken: deviceToken)
|
registration = try await register(deviceToken: deviceToken)
|
||||||
|
@ -145,10 +160,15 @@ private class PushManagerImpl: _PushManager {
|
||||||
url.path = "/app/v1/registrations/\(pushProxyRegistration.id)"
|
url.path = "/app/v1/registrations/\(pushProxyRegistration.id)"
|
||||||
var request = URLRequest(url: url.url!)
|
var request = URLRequest(url: url.url!)
|
||||||
request.httpMethod = "DELETE"
|
request.httpMethod = "DELETE"
|
||||||
let (_, resp) = try await URLSession.shared.data(for: request)
|
let (data, resp) = try await URLSession.shared.data(for: request)
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
let status = (resp as! HTTPURLResponse).statusCode
|
||||||
if !(200...299).contains(status) {
|
if (200...299).contains(status) {
|
||||||
|
self.pushProxyRegistration = nil
|
||||||
|
logger.debug("Unregistered from proxy")
|
||||||
|
} else {
|
||||||
logger.error("Unregistering: unexpected status \(status)")
|
logger.error("Unregistering: unexpected status \(status)")
|
||||||
|
let error = (try? JSONDecoder().decode(ProxyRegistrationError.self, from: data)) ?? ProxyRegistrationError(error: "Unknown error", fields: nil)
|
||||||
|
throw PushRegistrationError.unregistering(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,8 +178,11 @@ private class PushManagerImpl: _PushManager {
|
||||||
}
|
}
|
||||||
logger.debug("Push proxy registration: \(pushProxyRegistration.id, privacy: .public)")
|
logger.debug("Push proxy registration: \(pushProxyRegistration.id, privacy: .public)")
|
||||||
do {
|
do {
|
||||||
let token = try await getDeviceToken()
|
let token = try await getDeviceToken().hexEncodedString()
|
||||||
|
guard token != pushProxyRegistration.deviceToken else {
|
||||||
|
// already up-to-date, nothing to do
|
||||||
|
return
|
||||||
|
}
|
||||||
let newRegistration = try await update(registration: pushProxyRegistration, deviceToken: token)
|
let newRegistration = try await update(registration: pushProxyRegistration, deviceToken: token)
|
||||||
if pushProxyRegistration.endpoint != newRegistration.endpoint {
|
if pushProxyRegistration.endpoint != newRegistration.endpoint {
|
||||||
// TODO: update subscriptions if the endpoint's changed
|
// TODO: update subscriptions if the endpoint's changed
|
||||||
|
@ -190,13 +213,13 @@ private class PushManagerImpl: _PushManager {
|
||||||
remoteNotificationsRegistrationContinuation?.resume(throwing: PushRegistrationError.registeringForRemoteNotifications(error))
|
remoteNotificationsRegistrationContinuation?.resume(throwing: PushRegistrationError.registeringForRemoteNotifications(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func register(deviceToken: Data) async throws -> PushProxyRegistration {
|
private func register(deviceToken: String) async throws -> PushProxyRegistration {
|
||||||
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
||||||
url.path = "/app/v1/registrations"
|
url.path = "/app/v1/registrations"
|
||||||
var request = URLRequest(url: url.url!)
|
var request = URLRequest(url: url.url!)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
||||||
request.httpBody = try! JSONEncoder().encode(PushRegistrationParams(transactionID: "TODO", environment: apnsEnvironment, deviceToken: deviceToken.hexEncodedString(), pushVersion: 1))
|
request.httpBody = try! JSONEncoder().encode(PushRegistrationParams(transactionID: "TODO", environment: apnsEnvironment, deviceToken: deviceToken, pushVersion: 1))
|
||||||
let (data, resp) = try await URLSession.shared.data(for: request)
|
let (data, resp) = try await URLSession.shared.data(for: request)
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
let status = (resp as! HTTPURLResponse).statusCode
|
||||||
guard (200...299).contains(status) else {
|
guard (200...299).contains(status) else {
|
||||||
|
@ -207,13 +230,13 @@ private class PushManagerImpl: _PushManager {
|
||||||
return try JSONDecoder().decode(PushProxyRegistration.self, from: data)
|
return try JSONDecoder().decode(PushProxyRegistration.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(registration: PushProxyRegistration, deviceToken: Data) async throws -> PushProxyRegistration {
|
private func update(registration: PushProxyRegistration, deviceToken: String) async throws -> PushProxyRegistration {
|
||||||
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
var url = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
||||||
url.path = "/app/v1/registrations/\(registration.id)"
|
url.path = "/app/v1/registrations/\(registration.id)"
|
||||||
var request = URLRequest(url: url.url!)
|
var request = URLRequest(url: url.url!)
|
||||||
request.httpMethod = "PUT"
|
request.httpMethod = "PUT"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
||||||
request.httpBody = try! JSONEncoder().encode(PushUpdateParams(environment: apnsEnvironment, deviceToken: deviceToken.hexEncodedString(), pushVersion: 1))
|
request.httpBody = try! JSONEncoder().encode(PushUpdateParams(environment: apnsEnvironment, deviceToken: deviceToken, pushVersion: 1))
|
||||||
let (data, resp) = try await URLSession.shared.data(for: request)
|
let (data, resp) = try await URLSession.shared.data(for: request)
|
||||||
let status = (resp as! HTTPURLResponse).statusCode
|
let status = (resp as! HTTPURLResponse).statusCode
|
||||||
guard (200...299).contains(status) else {
|
guard (200...299).contains(status) else {
|
||||||
|
@ -229,6 +252,7 @@ enum PushRegistrationError: LocalizedError {
|
||||||
case alreadyRegistering
|
case alreadyRegistering
|
||||||
case registeringForRemoteNotifications(any Error)
|
case registeringForRemoteNotifications(any Error)
|
||||||
case registeringWithProxy(any Error)
|
case registeringWithProxy(any Error)
|
||||||
|
case unregistering(any Error)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -238,6 +262,8 @@ enum PushRegistrationError: LocalizedError {
|
||||||
"Remote notifications: \(error.localizedDescription)"
|
"Remote notifications: \(error.localizedDescription)"
|
||||||
case .registeringWithProxy(let error):
|
case .registeringWithProxy(let error):
|
||||||
"Proxy: \(error.localizedDescription)"
|
"Proxy: \(error.localizedDescription)"
|
||||||
|
case .unregistering(let error):
|
||||||
|
"Unregistering: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,22 +316,31 @@ private struct PushUpdateParams: Encodable {
|
||||||
struct PushProxyRegistration: Decodable {
|
struct PushProxyRegistration: Decodable {
|
||||||
let id: String
|
let id: String
|
||||||
let endpoint: URL
|
let endpoint: URL
|
||||||
|
let deviceToken: String
|
||||||
|
|
||||||
fileprivate var defaultsDict: [String: String] {
|
fileprivate var defaultsDict: [String: String] {
|
||||||
[
|
[
|
||||||
"id": id,
|
"id": id,
|
||||||
"endpoint": endpoint.absoluteString
|
"endpoint": endpoint.absoluteString,
|
||||||
|
"deviceToken": deviceToken,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate init?(defaultsDict: [String: String]) {
|
fileprivate init?(defaultsDict: [String: String]) {
|
||||||
guard let id = defaultsDict["id"],
|
guard let id = defaultsDict["id"],
|
||||||
let endpoint = defaultsDict["endpoint"],
|
let endpoint = defaultsDict["endpoint"].flatMap(URL.init(string:)),
|
||||||
let endpointURL = URL(string: endpoint) else {
|
let deviceToken = defaultsDict["deviceToken"] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
self.id = id
|
self.id = id
|
||||||
self.endpoint = endpointURL
|
self.endpoint = endpoint
|
||||||
|
self.deviceToken = deviceToken
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case endpoint
|
||||||
|
case deviceToken = "device_token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,3 +357,49 @@ private extension Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PushSubscription {
|
||||||
|
let accountID: String
|
||||||
|
let endpoint: URL
|
||||||
|
let alerts: Alerts
|
||||||
|
let policy: Policy
|
||||||
|
|
||||||
|
fileprivate var defaultsDict: [String: Any] {
|
||||||
|
[
|
||||||
|
"accountID": accountID,
|
||||||
|
"endpoint": endpoint.absoluteString,
|
||||||
|
"alerts": alerts.rawValue,
|
||||||
|
"policy": policy.rawValue
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(defaultsDict: [String: Any]) {
|
||||||
|
guard let accountID = defaultsDict["accountID"] as? String,
|
||||||
|
let endpoint = (defaultsDict["endpoint"] as? String).flatMap(URL.init(string:)),
|
||||||
|
let alerts = defaultsDict["alerts"] as? Int,
|
||||||
|
let policy = (defaultsDict["policy"] as? String).flatMap(Policy.init(rawValue:)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.accountID = accountID
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.alerts = Alerts(rawValue: alerts)
|
||||||
|
self.policy = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Policy: String {
|
||||||
|
case all, followed, followers
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Alerts: OptionSet {
|
||||||
|
static let mention = Alerts(rawValue: 1 << 0)
|
||||||
|
static let status = Alerts(rawValue: 1 << 1)
|
||||||
|
static let reblog = Alerts(rawValue: 1 << 2)
|
||||||
|
static let follow = Alerts(rawValue: 1 << 3)
|
||||||
|
static let followRequest = Alerts(rawValue: 1 << 4)
|
||||||
|
static let favorite = Alerts(rawValue: 1 << 5)
|
||||||
|
static let poll = Alerts(rawValue: 1 << 6)
|
||||||
|
static let update = Alerts(rawValue: 1 << 7)
|
||||||
|
|
||||||
|
let rawValue: Int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,16 +8,20 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
import UserAccounts
|
||||||
|
|
||||||
struct NotificationsPrefsView: View {
|
struct NotificationsPrefsView: View {
|
||||||
@State private var error: NotificationsSetupError?
|
@State private var error: NotificationsSetupError?
|
||||||
@State private var isSetup = false
|
@State private var isSetup = TriStateToggle.Mode.off
|
||||||
@State private var working = false
|
|
||||||
@State private var pushProxyRegistration: PushProxyRegistration?
|
@State private var pushProxyRegistration: PushProxyRegistration?
|
||||||
|
@ObservedObject private var userAccounts = UserAccountsManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
enableSection
|
enableSection
|
||||||
|
if isSetup == .on {
|
||||||
|
accountsSection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||||
|
@ -31,19 +35,10 @@ struct NotificationsPrefsView: View {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if working {
|
TriStateToggle(titleKey: "Push Notifications Enabled", mode: $isSetup, onChange: isSetupChanged(newValue:))
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
Toggle("Push Notifications Enabled", isOn: Binding(get: {
|
|
||||||
isSetup
|
|
||||||
}, set: { newValue in
|
|
||||||
isSetup = newValue
|
|
||||||
isSetupChanged(newValue: newValue)
|
|
||||||
}))
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
.alertWithData("An Error Occurred", data: $error) { error in
|
.alertWithData("An Error Occurred", data: $error) { error in
|
||||||
Button("OK") {}
|
Button("OK") {}
|
||||||
} message: { error in
|
} message: { error in
|
||||||
|
@ -51,26 +46,32 @@ struct NotificationsPrefsView: View {
|
||||||
}
|
}
|
||||||
.task { @MainActor in
|
.task { @MainActor in
|
||||||
pushProxyRegistration = PushManager.shared.pushProxyRegistration
|
pushProxyRegistration = PushManager.shared.pushProxyRegistration
|
||||||
isSetup = pushProxyRegistration != nil
|
isSetup = pushProxyRegistration != nil ? .on : .off
|
||||||
if !UIApplication.shared.isRegisteredForRemoteNotifications {
|
if !UIApplication.shared.isRegisteredForRemoteNotifications {
|
||||||
_ = await registerForRemoteNotifications()
|
_ = await registerForRemoteNotifications()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isSetupChanged(newValue: Bool) {
|
private var accountsSection: some View {
|
||||||
working = true
|
Section {
|
||||||
Task {
|
ForEach(userAccounts.accounts) { account in
|
||||||
defer {
|
PrefsAccountView(account: account)
|
||||||
working = false
|
|
||||||
}
|
}
|
||||||
let success = if newValue {
|
|
||||||
await startRegistration()
|
|
||||||
} else {
|
|
||||||
await unregister()
|
|
||||||
}
|
}
|
||||||
|
.appGroupedListRowBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isSetupChanged(newValue: Bool) async {
|
||||||
|
if newValue {
|
||||||
|
let success = await startRegistration()
|
||||||
if !success {
|
if !success {
|
||||||
isSetup = !newValue
|
isSetup = .off
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let success = await unregister()
|
||||||
|
if !success {
|
||||||
|
isSetup = .on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +112,52 @@ 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 {
|
private enum NotificationsSetupError: LocalizedError {
|
||||||
case requestingAuthorization(any Error)
|
case requestingAuthorization(any Error)
|
||||||
case registering(any Error)
|
case registering(any Error)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct PreferencesView: View {
|
struct PreferencesView: View {
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
@ -34,24 +33,12 @@ struct PreferencesView: View {
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(userAccounts.accounts, id: \.accessToken) { (account) in
|
ForEach(userAccounts.accounts) { (account) in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
LocalAccountAvatarView(localAccountInfo: account)
|
PrefsAccountView(account: account)
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(verbatim: account.username)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
|
|
||||||
domain.render(.uncheckedUnicodeString)
|
|
||||||
} else {
|
|
||||||
account.instanceURL.host!
|
|
||||||
}
|
|
||||||
Text(verbatim: instance)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if account == mastodonController.accountInfo! {
|
if account == mastodonController.accountInfo! {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// PrefsAccountView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UserAccounts
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
struct PrefsAccountView: View {
|
||||||
|
let account: UserAccountInfo
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
LocalAccountAvatarView(localAccountInfo: account)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(verbatim: account.username)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
|
||||||
|
domain.render(.uncheckedUnicodeString)
|
||||||
|
} else {
|
||||||
|
account.instanceURL.host!
|
||||||
|
}
|
||||||
|
Text(verbatim: instance)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// PrefsAccountView()
|
||||||
|
//}
|
Loading…
Reference in New Issue