Push configuration on iOS

This commit is contained in:
Jackson Harper
2024-03-08 17:15:59 +08:00
parent 6bc100942f
commit 7d77291f11
5 changed files with 212 additions and 31 deletions

View File

@ -4,47 +4,120 @@
import SwiftUI
import Utils
import Views
import Transmission
@MainActor final class PushNotificationSettingsViewModel: ObservableObject {
@Published var isLoading = false
@Published var emails = [NewsletterEmail]()
@Published var desiredNotificationsEnabled = false
@AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false
@MainActor final class PushNotificationSettingsViewModel: ObservableObject {
@Published var isLoading = false
@Published var isLoadingRules = true
func checkPushNotificationsStatus() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled
}
@Published var emails = [NewsletterEmail]()
@Published var desiredNotificationsEnabled = false
@Published var allSubscriptionsNotificationRule: Rule?
@Published var hasSubscriptionsNotifyRule = false
@Published var showOperationToast = false
@Published var operationStatus: OperationStatus = .none
@Published var operationMessage: String?
let subscriptionRuleName = "system.autoNotify.subscriptions"
@AppStorage(UserDefaultKey.notificationsEnabled.rawValue) var notificationsEnabled = false
func checkPushNotificationsStatus() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.desiredNotificationsEnabled = settings.alertSetting == UNNotificationSetting.enabled
}
}
}
func tryUpdateToDesired(dataService: DataService) {
UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue)
func tryUpdateToDesired(dataService: DataService) {
UserDefaults.standard.set(desiredNotificationsEnabled, forKey: UserDefaultKey.notificationsEnabled.rawValue)
if desiredNotificationsEnabled {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
DispatchQueue.main.async {
self.desiredNotificationsEnabled = granted
Task {
if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) {
_ = try? await dataService.syncDeviceToken(
deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken))
}
NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil)
if desiredNotificationsEnabled {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
DispatchQueue.main.async {
self.desiredNotificationsEnabled = granted
Task {
if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) {
_ = try? await dataService.syncDeviceToken(
deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken))
}
NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil)
}
}
} else {
if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) {
Task {
try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
}
}
} else {
if let tokenID = UserDefaults.standard.string(forKey: UserDefaultKey.deviceTokenID.rawValue) {
Task {
try? await Services().dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
}
}
}
}
func loadRule(dataService: DataService) async {
do {
let rule = try await dataService.rules().filter { $0.name == subscriptionRuleName }.first
setAllSubscriptionRule(rule: rule)
} catch {
print("error fetching", error)
setAllSubscriptionRule(rule: nil)
}
}
func setAllSubscriptionRule(rule: Rule?) {
allSubscriptionsNotificationRule = rule
hasSubscriptionsNotifyRule = allSubscriptionsNotificationRule != nil
isLoadingRules = false
}
func createSubscriptionNotificationRule(dataService: DataService) {
if allSubscriptionsNotificationRule != nil {
return
}
operationMessage = "Creating notification rule..."
operationStatus = .isPerforming
showOperationToast = true
Task {
do {
let rule = try await dataService.createNotificationRule(
name: subscriptionRuleName,
filter: "in:all has:subscription"
)
setAllSubscriptionRule(rule: rule)
operationMessage = "Rule created"
operationStatus = .success
} catch {
print("error creating notification rule: ", error)
operationMessage = "Failed to create notification rule"
operationStatus = .failure
}
}
}
func deleteSubscriptionNotificationRule(dataService: DataService) {
operationMessage = "Creating label rule..."
operationStatus = .isPerforming
showOperationToast = true
Task {
do {
if let allSubscriptionsNotificationRule = allSubscriptionsNotificationRule {
_ = try await dataService.deleteRule(ruleID: allSubscriptionsNotificationRule.id)
setAllSubscriptionRule(rule: nil)
operationMessage = "Notification rule deleted"
operationStatus = .success
}
} catch {
operationMessage = "Failed to create label rule"
operationStatus = .failure
}
}
}
}
struct PushNotificationSettingsView: View {
@EnvironmentObject var dataService: DataService
@Environment(\.dismiss) private var dismiss
@ -54,6 +127,12 @@
var body: some View {
Group {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) {
OperationToast(operationMessage: $viewModel.operationMessage, showOperationToast: $viewModel.showOperationToast, operationStatus: $viewModel.operationStatus)
} label: {
EmptyView()
}.buttonStyle(.plain)
#if os(iOS)
Form {
innerBody
@ -68,7 +147,10 @@
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in
dismiss()
}
.task { viewModel.checkPushNotificationsStatus() }
.task {
viewModel.checkPushNotificationsStatus()
await viewModel.loadRule(dataService: dataService)
}
}
private var notificationsText: some View {
@ -84,6 +166,14 @@
.accentColor(.blue)
}
private var rulesSection: some View {
if viewModel.isLoadingRules {
AnyView(EmptyView())
} else {
AnyView(Toggle("Notify me when new items arrive from my subscriptions", isOn: $viewModel.hasSubscriptionsNotifyRule))
}
}
private var innerBody: some View {
Group {
Section {
@ -96,12 +186,35 @@
notificationsText
}
Section {
rulesSection
}
Section {
NavigationLink("Devices") {
PushNotificationDevicesView()
}
}
}
.onChange(of: viewModel.hasSubscriptionsNotifyRule) { newValue in
print("has notification rule: \(newValue)")
if viewModel.isLoadingRules {
return
}
if newValue {
viewModel.createSubscriptionNotificationRule(dataService: dataService)
} else {
viewModel.deleteSubscriptionNotificationRule(dataService: dataService)
}
}
.onChange(of: viewModel.operationStatus) { newValue in
if newValue == .success || newValue == .failure {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
viewModel.showOperationToast = false
}
}
}
.navigationTitle(LocalText.pushNotificationsGeneric)
}
}

View File

@ -273,7 +273,7 @@ struct SubscriptionsView: View {
await viewModel.cancelSubscription(dataService: dataService, subscription: subscription)
}
}
)
).background(Color.systemBackground)
} label: {
SubscriptionCell(subscription: subscription)
}
@ -526,6 +526,25 @@ struct SubscriptionSettingsView: View {
}
}
}
//
// var notificationRuleRow: some View {
// HStack {
// Text("Add Labels")
// Spacer()
// if isLoadingRule || viewModel.rules != nil {
// Button(action: { showLabelsSelector = true }, label: {
// if let ruleLabels = ruleLabels {
// let labelNames = ruleLabels.map(\.unwrappedName)
// Text("[\(labelNames.joined(separator: ","))]")
// } else {
// Text("Create Rule")
// }
// }).tint(Color.blue)
// } else {
// ProgressView()
// }
// }
// }
var body: some View {
VStack {

View File

@ -89,4 +89,51 @@ public extension DataService {
}
}
}
func createNotificationRule(name: String, filter: String) async throws -> Rule {
enum MutationResult {
case result(rule: Rule)
case error(errorMessage: String)
}
let selection = Selection<MutationResult, Unions.SetRuleResult> {
try $0.on(
setRuleError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") },
setRuleSuccess: .init { .result(rule: try $0.rule(selection: ruleSelection)) }
)
}
let mutation = Selection.Mutation {
try $0.setRule(
input: InputObjects.SetRuleInput(
actions: [InputObjects.RuleActionInput(params: [], type: .sendNotification)],
enabled: true,
eventTypes: [.pageCreated],
filter: filter,
id: OptionalArgument(nil),
name: name
),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(mutation, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
continuation.resume(throwing: BasicError.message(messageText: "network error"))
return
}
switch payload.data {
case let .result(rule: rule):
continuation.resume(returning: rule)
case let .error(errorMessage: errorMessage):
continuation.resume(throwing: BasicError.message(messageText: errorMessage))
}
}
}
}
}

View File

@ -55,7 +55,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let userInfo = notification.request.content.userInfo
UIApplication.shared.applicationIconBadgeNumber = 0
print(userInfo) // extract data sent along with PN
print("push data", userInfo) // extract data sent along with PN
completionHandler([[.banner, .sound]])
}

View File

@ -70,8 +70,10 @@ const sendNotification = async (obj: RuleActionObj) => {
const message = {
title: item.author || item.siteName || 'Omnivore',
body: item.title,
image: item.thumbnail,
}
const data = {
libraryItemId: item.id,
thumbnail: item.thumbnail,
}
return sendPushNotifications(obj.userId, message, 'rule')