diff --git a/ntfy/App/AppDelegate.swift b/ntfy/App/AppDelegate.swift index 7bd3577..f05b3e8 100644 --- a/ntfy/App/AppDelegate.swift +++ b/ntfy/App/AppDelegate.swift @@ -6,7 +6,8 @@ import FirebaseCore import CoreData class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { - let tag = "AppDelegate" + private let tag = "AppDelegate" + private let pollTimerTopic = "~poll" // See ntfy server if ever changed // Implements navigation from notifications, see https://stackoverflow.com/a/70731861/1440785 @Published var selectedBaseUrl: String? = nil @@ -30,9 +31,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject { // Set self as messaging delegate Messaging.messaging().delegate = self + // Register to timerkeeper topic + Messaging.messaging().subscribe(toTopic: pollTimerTopic) + return true } + /// Executed when a background notification arrives. This is used to trigger polling of local topics. + /// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + Log.d(tag, "Background notification received", userInfo) + + // Exit out early if this message is not expected + let topic = userInfo["topic"] as? String ?? "" + if topic != pollTimerTopic { + completionHandler(.noData) + return + } + + // Poll and display new messages + let store = Store.shared + let center = UNUserNotificationCenter.current() + let subscriptionManager = SubscriptionManager(store: store) + + store.getSubscriptions()?.forEach { subscription in + subscriptionManager.poll(subscription) { messages in + messages.forEach { message in + let content = UNMutableNotificationContent() + content.title = message.title ?? "" + content.body = message.message ?? "" + content.sound = .default + + let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */) + center.add(request) { (error) in + if let error = error { + Log.e(self.tag, "Unable to create notification", error) + } + } + } + } + } + completionHandler(.newData) + } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { data in String(format: "%02.2hhx", data) }.joined() Messaging.messaging().apnsToken = deviceToken diff --git a/ntfy/Persistence/Store.swift b/ntfy/Persistence/Store.swift index 93408b7..d5c36f0 100644 --- a/ntfy/Persistence/Store.swift +++ b/ntfy/Persistence/Store.swift @@ -70,6 +70,10 @@ class Store: ObservableObject { return try? context.fetch(fetchRequest).first } + func getSubscriptions() -> [Subscription]? { + return try? context.fetch(Subscription.fetchRequest()) + } + func delete(subscription: Subscription) { context.delete(subscription) try? context.save() diff --git a/ntfy/Persistence/SubscriptionManager.swift b/ntfy/Persistence/SubscriptionManager.swift index 154bb9a..f621cb9 100644 --- a/ntfy/Persistence/SubscriptionManager.swift +++ b/ntfy/Persistence/SubscriptionManager.swift @@ -25,20 +25,26 @@ struct SubscriptionManager { } func poll(_ subscription: Subscription) { + poll(subscription) { _ in } + } + + func poll(_ subscription: Subscription, completionHandler: @escaping ([Message]) -> Void) { Log.d(tag, "Polling from \(subscription.urlString())") ApiService.shared.poll(subscription: subscription) { messages, error in guard let messages = messages else { Log.e(tag, "Polling failed", error) + completionHandler([]) return } Log.d(tag, "Polling success, \(messages.count) new message(s)", messages) if !messages.isEmpty { - DispatchQueue.main.async { + DispatchQueue.main.sync { for message in messages { store.save(notificationFromMessage: message, withSubscription: subscription) } } } + completionHandler(messages) } } } diff --git a/ntfy/Views/SubscriptionAddView.swift b/ntfy/Views/SubscriptionAddView.swift index 692e2e6..e693631 100644 --- a/ntfy/Views/SubscriptionAddView.swift +++ b/ntfy/Views/SubscriptionAddView.swift @@ -7,6 +7,8 @@ struct SubscriptionAddView: View { @EnvironmentObject private var store: Store @State private var topic: String = "" + @State private var useAnother: Bool = false + @State private var baseUrl: String = "" private var subscriptionManager: SubscriptionManager { return SubscriptionManager(store: store) @@ -24,6 +26,14 @@ struct SubscriptionAddView: View { .disableAutocapitalization() .disableAutocorrection(true) } + Section { + Toggle("Use another server", isOn: $useAnother) + if useAnother { + TextField("Server URL, e.g. https://ntfy.example.com", text: $baseUrl) + .disableAutocapitalization() + .disableAutocorrection(true) + } + } } } .navigationTitle("Add subscription") @@ -61,8 +71,9 @@ struct SubscriptionAddView: View { } private func subscribeAction() { + let baseUrl = (useAnother) ? baseUrl : Config.appBaseUrl DispatchQueue.global(qos: .background).async { - subscriptionManager.subscribe(baseUrl: Config.appBaseUrl, topic: sanitize(topic: topic)) + subscriptionManager.subscribe(baseUrl: baseUrl, topic: sanitize(topic: topic)) } isShowing = false } @@ -71,3 +82,14 @@ struct SubscriptionAddView: View { isShowing = false } } + + +struct SubscriptionAddView_Previews: PreviewProvider { + @State static var isShowing = true + + static var previews: some View { + let store = Store.preview + SubscriptionAddView(isShowing: $isShowing) + .environmentObject(store) + } +}