import AppKit import Foundation import UserNotifications final class NotificationService: NSObject, UNUserNotificationCenterDelegate { static let shared = NotificationService() private let center = UNUserNotificationCenter.current() private var isAuthorized = false private override init() { super.init() center.delegate = self } func requestAuthorizationIfNeeded() async { guard !isAuthorized else { return } let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false isAuthorized = granted } func handleChanges(_ changes: [BugChange]) async { await requestAuthorizationIfNeeded() guard isAuthorized else { return } let significant = changes .filter { $0.type == .newBug || $0.type == .priorityChanged } .prefix(3) for change in significant { deliver(change) } let statusChanges = changes.filter { $0.type == .statusChanged } if statusChanges.count > 1 { deliverBatchStatusChange(statusChanges) } else if let single = statusChanges.first { deliver(single) } } private func deliver(_ change: BugChange) { let content = UNMutableNotificationContent() switch change.type { case .newBug: content.title = "New Bug Assigned" content.body = "[\(change.bug.priority.rawValue)] \(change.bug.title)" content.sound = .default case .statusChanged: content.title = "Bug Status Changed" content.body = "\(change.bug.title) → \(change.bug.status.rawValue)" case .priorityChanged: content.title = "Bug Priority Changed" content.body = "\(change.bug.title) → \(change.bug.priority.rawValue)" case .assigneeChanged: return } var userInfo: [String: Any] = ["bugId": change.bug.id] if let url = change.bug.feishuURL { userInfo["feishuURL"] = url.absoluteString } content.userInfo = userInfo let request = UNNotificationRequest( identifier: "bugger-\(change.bug.id)-\(Date().timeIntervalSince1970)", content: content, trigger: nil ) center.add(request) } private func deliverBatchStatusChange(_ changes: [BugChange]) { let content = UNMutableNotificationContent() content.title = "\(changes.count) Bugs Updated" content.body = changes.prefix(3).map { "• \($0.bug.title)" }.joined(separator: "\n") content.sound = .default let request = UNNotificationRequest( identifier: "bugger-batch-\(Date().timeIntervalSince1970)", content: content, trigger: nil ) center.add(request) } func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.banner, .sound]) } func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { if let urlString = response.notification.request.content.userInfo["feishuURL"] as? String, let url = URL(string: urlString) { NSWorkspace.shared.open(url) } completionHandler() } }