diff --git a/crates/notification-macos/swift-lib/src/NotificationInstance.swift b/crates/notification-macos/swift-lib/src/NotificationInstance.swift index 26eb01f1ff..ee1364ec79 100644 --- a/crates/notification-macos/swift-lib/src/NotificationInstance.swift +++ b/crates/notification-macos/swift-lib/src/NotificationInstance.swift @@ -112,6 +112,7 @@ class NotificationInstance { func dismissWithUserAction() { RustBridge.onDismiss(key: key) + SystemNotificationCenter.shared.removeFromNotificationCenter(key: key) dismiss() } diff --git a/crates/notification-macos/swift-lib/src/SystemNotificationCenter.swift b/crates/notification-macos/swift-lib/src/SystemNotificationCenter.swift new file mode 100644 index 0000000000..33686b23ea --- /dev/null +++ b/crates/notification-macos/swift-lib/src/SystemNotificationCenter.swift @@ -0,0 +1,99 @@ +import Foundation +import UserNotifications + +class SystemNotificationCenter: NSObject, UNUserNotificationCenterDelegate { + static let shared = SystemNotificationCenter() + + private var isAuthorized = false + + private override init() { + super.init() + UNUserNotificationCenter.current().delegate = self + } + + func requestAuthorization() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { + [weak self] granted, error in + if let error = error { + print("Notification authorization error: \(error)") + return + } + self?.isAuthorized = granted + } + } + + func postToNotificationCenter(payload: NotificationPayload) { + guard isAuthorized else { + requestAuthorization() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.postNotificationInternal(payload: payload) + } + return + } + + postNotificationInternal(payload: payload) + } + + private func postNotificationInternal(payload: NotificationPayload) { + let content = UNMutableNotificationContent() + content.title = payload.title + content.body = payload.message + content.sound = .default + content.userInfo = ["key": payload.key] + + if let eventDetails = payload.eventDetails { + var subtitle = eventDetails.what + if let location = eventDetails.location, !location.isEmpty { + subtitle += " • \(location)" + } + content.subtitle = subtitle + } + + let request = UNNotificationRequest( + identifier: payload.key, + content: content, + trigger: nil + ) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("Failed to add notification to center: \(error)") + } + } + } + + func removeFromNotificationCenter(key: String) { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [key]) + } + + func removeAllFromNotificationCenter() { + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([]) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let key = response.notification.request.identifier + + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + RustBridge.onCollapsedConfirm(key: key) + case UNNotificationDismissActionIdentifier: + RustBridge.onDismiss(key: key) + default: + break + } + + completionHandler() + } +} diff --git a/crates/notification-macos/swift-lib/src/lib.swift b/crates/notification-macos/swift-lib/src/lib.swift index d2fa612676..2dc9446962 100644 --- a/crates/notification-macos/swift-lib/src/lib.swift +++ b/crates/notification-macos/swift-lib/src/lib.swift @@ -12,6 +12,7 @@ public func _showNotification(jsonPayload: SRString) -> Bool { } NotificationManager.shared.show(payload: payload) + SystemNotificationCenter.shared.postToNotificationCenter(payload: payload) Thread.sleep(forTimeInterval: 0.1) return true @@ -20,5 +21,6 @@ public func _showNotification(jsonPayload: SRString) -> Bool { @_cdecl("_dismiss_all_notifications") public func _dismissAllNotifications() -> Bool { NotificationManager.shared.dismissAll() + SystemNotificationCenter.shared.removeAllFromNotificationCenter() return true }