From 05d242fe32dd20600767fa034d51fa02e8d0b1e9 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 8 Sep 2026 10:59:08 +1000 Subject: [PATCH] chore: refactor notifications to support categories `AppDelegate` now acts as the `UNUserNotificationCenterDelegate` directly, replacing the standalone `NotifDelegate`. This lets notification response handlers reach app state, such as the menu bar controller. Notifications now carry a category. `NotificationCategory` is `CaseIterable`, and every case is registered with the notification center on launch, so a new case cannot be forgotten at registration. No user-visible change. Groundwork for sending notifications on VPN failures (#195). --- .../Coder-Desktop/Coder_DesktopApp.swift | 12 ++++++++---- Coder-Desktop/Coder-Desktop/Notifications.swift | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index 86b09893..79f653e3 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -49,11 +49,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { let state: AppState let fileSyncDaemon: MutagenDaemon let urlHandler: URLHandler - let notifDelegate: NotifDelegate let autoUpdater: UpdaterService override init() { - notifDelegate = NotifDelegate() + AppDelegate.registerNotificationCategories() vpn = CoderVPNService() autoUpdater = UpdaterService() let state = AppState(onChange: vpn.configureTunnelProviderProtocol) @@ -79,8 +78,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } self.fileSyncDaemon = fileSyncDaemon urlHandler = URLHandler(state: state, vpn: vpn) + super.init() // `delegate` is weak - UNUserNotificationCenter.current().delegate = notifDelegate + UNUserNotificationCenter.current().delegate = self } func applicationDidFinishLaunching(_: Notification) { @@ -164,7 +164,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { do { try urlHandler.handle(url) } catch let handleError { Task { do { - try await sendNotification(title: "Failed to handle link", body: handleError.description) + try await sendNotification( + title: "Failed to handle link", + body: handleError.description, + category: .uriFailure + ) } catch let notifError { logger.error("Failed to send notification (\(handleError.description)): \(notifError)") } diff --git a/Coder-Desktop/Coder-Desktop/Notifications.swift b/Coder-Desktop/Coder-Desktop/Notifications.swift index 44a2afb8..3ddf8c6e 100644 --- a/Coder-Desktop/Coder-Desktop/Notifications.swift +++ b/Coder-Desktop/Coder-Desktop/Notifications.swift @@ -1,8 +1,12 @@ import UserNotifications -class NotifDelegate: NSObject, UNUserNotificationCenterDelegate { - override init() { - super.init() +extension AppDelegate: UNUserNotificationCenterDelegate { + static func registerNotificationCategories() { + UNUserNotificationCenter.current().setNotificationCategories( + Set(NotificationCategory.allCases.map { + UNNotificationCategory(identifier: $0.rawValue, actions: [], intentIdentifiers: [], options: []) + }) + ) } // This function is required for notifications to appear as banners whilst the app is running. @@ -15,7 +19,7 @@ class NotifDelegate: NSObject, UNUserNotificationCenterDelegate { } } -func sendNotification(title: String, body: String) async throws { +func sendNotification(title: String, body: String, category: NotificationCategory) async throws { let nc = UNUserNotificationCenter.current() let granted = try await nc.requestAuthorization(options: [.alert, .badge]) guard granted else { @@ -24,5 +28,10 @@ func sendNotification(title: String, body: String) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body + content.categoryIdentifier = category.rawValue try await nc.add(.init(identifier: UUID().uuidString, content: content, trigger: nil)) } + +enum NotificationCategory: String, CaseIterable { + case uriFailure = "URI_FAILURE" +}