mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-22 15:16:36 +00:00
Push notifications: Per account settings
This commit is contained in:
parent
36a9eefe21
commit
d1ed8e962b
8 changed files with 201 additions and 156 deletions
|
@ -214,12 +214,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
PushNotificationsService.shared.pushToken = deviceToken
|
||||
#if !DEBUG
|
||||
Task {
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
#endif
|
||||
Task {
|
||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(forceCreate: false)
|
||||
}
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||
|
|
|
@ -33,15 +33,20 @@ struct AccountSettingsView: View {
|
|||
isEditingFilters = true
|
||||
}
|
||||
}
|
||||
if let subscription = pushNotifications.subscriptions.first(where: { $0.account.token == appAccount.oauthToken }) {
|
||||
NavigationLink(destination: PushNotificationsView(subscription: subscription)) {
|
||||
Label("settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
if let token = appAccount.oauthToken {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: [.init(server: appAccount.server,
|
||||
token: token,
|
||||
accountName: appAccount.accountName)])
|
||||
if let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
||||
await sub.deleteSubscription()
|
||||
}
|
||||
appAccountsManager.delete(account: appAccount)
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
@ -231,7 +231,8 @@ struct AddAccountView: View {
|
|||
accountName: "\(account.acct)@\(client.server)",
|
||||
oauthToken: oauthToken))
|
||||
Task {
|
||||
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
pushNotifications.setAccounts(accounts: appAccountsManager.pushAccounts)
|
||||
await pushNotifications.updateSubscriptions(forceCreate: true)
|
||||
}
|
||||
isSigninIn = false
|
||||
dismiss()
|
||||
|
|
|
@ -12,85 +12,99 @@ struct PushNotificationsView: View {
|
|||
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||
@EnvironmentObject private var pushNotifications: PushNotificationsService
|
||||
|
||||
@State private var subscriptions: [PushSubscription] = []
|
||||
@StateObject public var subscription: PushNotificationSubscriptionSettings
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: $pushNotifications.isPushEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isEnabled = newValue
|
||||
if newValue {
|
||||
updateSubscription()
|
||||
} else {
|
||||
deleteSubscription()
|
||||
}
|
||||
})) {
|
||||
Text("settings.push.main-toggle")
|
||||
}
|
||||
} footer: {
|
||||
Text("settings.push.main-toggle.description")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
if pushNotifications.isPushEnabled {
|
||||
|
||||
if subscription.isEnabled {
|
||||
Section {
|
||||
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isMentionNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isMentionNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.mentions", systemImage: "at")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isFollowNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isFollowNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isFollowNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.follows", systemImage: "person.badge.plus")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isFavoriteNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isFavoriteNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isFavoriteNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.favorites", systemImage: "star")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isReblogNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isReblogNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isReblogNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.boosts", systemImage: "arrow.left.arrow.right.circle")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isPollNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isPollNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isPollNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.polls", systemImage: "chart.bar")
|
||||
}
|
||||
Toggle(isOn: $pushNotifications.isNewPostsNotificationEnabled) {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isNewPostsNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isNewPostsNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Label("settings.push.new-posts", systemImage: "bubble.right")
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
}
|
||||
.navigationTitle("settings.push.navigation-title")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.onAppear {
|
||||
Task {
|
||||
await pushNotifications.fetchSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotifications.isPushEnabled) { newValue in
|
||||
pushNotifications.isUserPushEnabled = newValue
|
||||
if !newValue {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
}
|
||||
} else {
|
||||
updateSubscriptions()
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotifications.isFollowNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isPollNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isReblogNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isMentionNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isFavoriteNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
}
|
||||
.onChange(of: pushNotifications.isNewPostsNotificationEnabled) { _ in
|
||||
updateSubscriptions()
|
||||
.task {
|
||||
await subscription.fetchSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSubscriptions() {
|
||||
private func updateSubscription() {
|
||||
Task {
|
||||
await pushNotifications.updateSubscriptions(accounts: appAccountsManager.pushAccounts)
|
||||
await subscription.updateSubscription(forceCreate: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteSubscription() {
|
||||
Task {
|
||||
await subscription.deleteSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,11 +73,10 @@ struct SettingsTabs: View {
|
|||
.onDelete { indexSet in
|
||||
if let index = indexSet.first {
|
||||
let account = appAccountsManager.availableAccounts[index]
|
||||
if let token = account.oauthToken {
|
||||
if let token = account.oauthToken,
|
||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
||||
Task {
|
||||
await pushNotifications.deleteSubscriptions(accounts: [.init(server: account.server,
|
||||
token: token,
|
||||
accountName: account.accountName)])
|
||||
await sub.deleteSubscription()
|
||||
}
|
||||
}
|
||||
appAccountsManager.delete(account: account)
|
||||
|
@ -91,9 +90,6 @@ struct SettingsTabs: View {
|
|||
@ViewBuilder
|
||||
private var generalSection: some View {
|
||||
Section("settings.section.general") {
|
||||
NavigationLink(destination: PushNotificationsView()) {
|
||||
Label("settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right")
|
||||
}
|
||||
if let instanceData = currentInstance.instance {
|
||||
NavigationLink(destination: InstanceInfoView(instance: instanceData)) {
|
||||
Label("settings.general.instance", systemImage: "server.rack")
|
||||
|
|
|
@ -19,7 +19,7 @@ public class AppAccountsManager: ObservableObject {
|
|||
@Published public var availableAccounts: [AppAccount]
|
||||
@Published public var currentClient: Client
|
||||
|
||||
public var pushAccounts: [PushNotificationsService.PushAccounts] {
|
||||
public var pushAccounts: [PushAccount] {
|
||||
availableAccounts.filter { $0.oauthToken != nil }
|
||||
.map { .init(server: $0.server, token: $0.oauthToken!, accountName: $0.accountName) }
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ public struct AppAccountsSelectorView: View {
|
|||
if let avatar = currentAccount.account?.avatar {
|
||||
AvatarView(url: avatar, size: avatarSize)
|
||||
} else {
|
||||
EmptyView()
|
||||
ProgressView()
|
||||
}
|
||||
}.overlay(alignment: .topTrailing) {
|
||||
if !currentAccount.followRequests.isEmpty {
|
||||
|
|
|
@ -6,6 +6,18 @@ import Network
|
|||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
public struct PushAccount {
|
||||
public let server: String
|
||||
public let token: OauthToken
|
||||
public let accountName: String?
|
||||
|
||||
public init(server: String, token: OauthToken, accountName: String?) {
|
||||
self.server = server
|
||||
self.token = token
|
||||
self.accountName = accountName
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public class PushNotificationsService: ObservableObject {
|
||||
enum Constants {
|
||||
|
@ -14,40 +26,12 @@ public class PushNotificationsService: ObservableObject {
|
|||
static let keychainPrivateKey = "notifications_private_key"
|
||||
}
|
||||
|
||||
public struct PushAccounts {
|
||||
public let server: String
|
||||
public let token: OauthToken
|
||||
public let accountName: String?
|
||||
|
||||
public init(server: String, token: OauthToken, accountName: String?) {
|
||||
self.server = server
|
||||
self.token = token
|
||||
self.accountName = accountName
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = PushNotificationsService()
|
||||
|
||||
public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = []
|
||||
|
||||
@Published public var pushToken: Data?
|
||||
|
||||
@AppStorage("user_push_is_on") public var isUserPushEnabled: Bool = true
|
||||
@Published public var isPushEnabled: Bool = false {
|
||||
didSet {
|
||||
if !oldValue && isPushEnabled {
|
||||
requestPushNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var isFollowNotificationEnabled: Bool = true
|
||||
@Published public var isFavoriteNotificationEnabled: Bool = true
|
||||
@Published public var isReblogNotificationEnabled: Bool = true
|
||||
@Published public var isMentionNotificationEnabled: Bool = true
|
||||
@Published public var isPollNotificationEnabled: Bool = true
|
||||
@Published public var isNewPostsNotificationEnabled: Bool = true
|
||||
|
||||
private var subscriptions: [PushSubscription] = []
|
||||
|
||||
private var keychain: KeychainSwift {
|
||||
let keychain = KeychainSwift()
|
||||
#if !DEBUG
|
||||
|
@ -63,72 +47,26 @@ public class PushNotificationsService: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchSubscriptions(accounts: [PushAccounts]) async {
|
||||
|
||||
public func setAccounts(accounts: [PushAccount]) {
|
||||
subscriptions = []
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
let sub: PushSubscription = try await client.get(endpoint: Push.subscription)
|
||||
subscriptions.append(sub)
|
||||
} catch {}
|
||||
let sub = PushNotificationSubscriptionSettings(account: account,
|
||||
key: notificationsPrivateKeyAsKey.publicKey.x963Representation,
|
||||
authKey: notificationsAuthKeyAsKey,
|
||||
pushToken: pushToken)
|
||||
subscriptions.append(sub)
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
public func updateSubscriptions(accounts: [PushAccounts]) async {
|
||||
subscriptions = []
|
||||
let key = notificationsPrivateKeyAsKey.publicKey.x963Representation
|
||||
let authKey = notificationsAuthKeyAsKey
|
||||
guard let pushToken = pushToken, isUserPushEnabled else { return }
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
var listenerURL = Constants.endpoint
|
||||
listenerURL += "/push/"
|
||||
listenerURL += pushToken.hexString
|
||||
listenerURL += "/\(account.accountName ?? account.server)"
|
||||
#if DEBUG
|
||||
listenerURL += "?sandbox=true"
|
||||
#endif
|
||||
let sub: PushSubscription =
|
||||
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
||||
p256dh: key,
|
||||
auth: authKey,
|
||||
mentions: isMentionNotificationEnabled,
|
||||
status: isNewPostsNotificationEnabled,
|
||||
reblog: isReblogNotificationEnabled,
|
||||
follow: isFollowNotificationEnabled,
|
||||
favorite: isFavoriteNotificationEnabled,
|
||||
poll: isPollNotificationEnabled))
|
||||
subscriptions.append(sub)
|
||||
} catch {}
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
public func deleteSubscriptions(accounts: [PushAccounts]) async {
|
||||
for account in accounts {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
_ = try await client.delete(endpoint: Push.subscription)
|
||||
} catch {}
|
||||
}
|
||||
await fetchSubscriptions(accounts: accounts)
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
private func refreshSubscriptionsUI() {
|
||||
if let sub = subscriptions.first {
|
||||
isPushEnabled = true
|
||||
isFollowNotificationEnabled = sub.alerts.follow
|
||||
isFavoriteNotificationEnabled = sub.alerts.favourite
|
||||
isReblogNotificationEnabled = sub.alerts.reblog
|
||||
isMentionNotificationEnabled = sub.alerts.mention
|
||||
isPollNotificationEnabled = sub.alerts.poll
|
||||
isNewPostsNotificationEnabled = sub.alerts.status
|
||||
} else {
|
||||
isPushEnabled = false
|
||||
|
||||
public func updateSubscriptions(forceCreate: Bool) async {
|
||||
for subscription in subscriptions {
|
||||
await withTaskGroup(of: Void.self, body: { group in
|
||||
group.addTask {
|
||||
await subscription.fetchSubscription()
|
||||
await subscription.updateSubscription(forceCreate: forceCreate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,3 +121,96 @@ extension Data {
|
|||
return map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public class PushNotificationSubscriptionSettings: ObservableObject {
|
||||
@Published public var isEnabled: Bool = true
|
||||
@Published public var isFollowNotificationEnabled: Bool = true
|
||||
@Published public var isFavoriteNotificationEnabled: Bool = true
|
||||
@Published public var isReblogNotificationEnabled: Bool = true
|
||||
@Published public var isMentionNotificationEnabled: Bool = true
|
||||
@Published public var isPollNotificationEnabled: Bool = true
|
||||
@Published public var isNewPostsNotificationEnabled: Bool = true
|
||||
|
||||
public let account: PushAccount
|
||||
|
||||
private let key: Data
|
||||
private let authKey: Data
|
||||
|
||||
public var pushToken: Data?
|
||||
|
||||
public private(set) var subscription: PushSubscription?
|
||||
|
||||
public init(account: PushAccount, key: Data, authKey: Data, pushToken: Data?) {
|
||||
self.account = account
|
||||
self.key = key
|
||||
self.authKey = authKey
|
||||
self.pushToken = pushToken
|
||||
}
|
||||
|
||||
private func refreshSubscriptionsUI() {
|
||||
if let subscription {
|
||||
isFollowNotificationEnabled = subscription.alerts.follow
|
||||
isFavoriteNotificationEnabled = subscription.alerts.favourite
|
||||
isReblogNotificationEnabled = subscription.alerts.reblog
|
||||
isMentionNotificationEnabled = subscription.alerts.mention
|
||||
isPollNotificationEnabled = subscription.alerts.poll
|
||||
isNewPostsNotificationEnabled = subscription.alerts.status
|
||||
}
|
||||
}
|
||||
|
||||
public func updateSubscription(forceCreate: Bool) async {
|
||||
guard let pushToken = pushToken, subscription != nil || forceCreate else { return }
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
var listenerURL = PushNotificationsService.Constants.endpoint
|
||||
listenerURL += "/push/"
|
||||
listenerURL += pushToken.hexString
|
||||
listenerURL += "/\(account.accountName ?? account.server)"
|
||||
#if DEBUG
|
||||
listenerURL += "?sandbox=true"
|
||||
#endif
|
||||
subscription =
|
||||
try await client.post(endpoint: Push.createSub(endpoint: listenerURL,
|
||||
p256dh: key,
|
||||
auth: authKey,
|
||||
mentions: isMentionNotificationEnabled,
|
||||
status: isNewPostsNotificationEnabled,
|
||||
reblog: isReblogNotificationEnabled,
|
||||
follow: isFollowNotificationEnabled,
|
||||
favorite: isFavoriteNotificationEnabled,
|
||||
poll: isPollNotificationEnabled))
|
||||
isEnabled = subscription != nil
|
||||
|
||||
} catch {
|
||||
isEnabled = false
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
|
||||
public func deleteSubscription() async {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
_ = try await client.delete(endpoint: Push.subscription)
|
||||
subscription = nil
|
||||
await fetchSubscription()
|
||||
refreshSubscriptionsUI()
|
||||
while subscription != nil {
|
||||
await deleteSubscription()
|
||||
}
|
||||
isEnabled = false
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public func fetchSubscription() async {
|
||||
let client = Client(server: account.server, oauthToken: account.token)
|
||||
do {
|
||||
subscription = try await client.get(endpoint: Push.subscription)
|
||||
isEnabled = subscription != nil
|
||||
} catch {
|
||||
subscription = nil
|
||||
isEnabled = false
|
||||
}
|
||||
refreshSubscriptionsUI()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue