68fed087-9049-446f-bfc5-3f8.../Sources/engine/NotificationCenter.swift

78 lines
3.2 KiB
Swift
Raw Permalink Normal View History

//
// NotificationCenter.swift
// plugin v0.1
//
// Thin wrapper around UNUserNotificationCenter. Plugin bundles inherit
// the host app's bundle identifier for notification authorization, so
// the host (Mio Island) needs to have notifications allowed in System
// Settings. We request once on activate; if it's denied we fall back
// to the in-panel red-dot pulse pattern.
//
import Foundation
@preconcurrency import UserNotifications
import AppKit
@MainActor
final class WorkerNotificationCenter {
static let shared = WorkerNotificationCenter()
private(set) var isAuthorized: Bool = false
private var didRequest: Bool = false
private init() {}
/// Asks the user once. Subsequent calls are no-ops.
func requestAuthorizationIfNeeded() {
guard !didRequest else { return }
didRequest = true
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
let status = settings.authorizationStatus
switch status {
case .authorized, .provisional, .ephemeral:
Task { @MainActor in WorkerNotificationCenter.shared.isAuthorized = true }
case .denied:
Task { @MainActor in WorkerNotificationCenter.shared.isAuthorized = false }
WorkerDebugLog.write("notifications denied — falling back to in-panel dot")
case .notDetermined:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error {
WorkerDebugLog.write("notif auth error: \(error)")
}
Task { @MainActor in WorkerNotificationCenter.shared.isAuthorized = granted }
WorkerDebugLog.write("notif auth granted=\(granted)")
}
@unknown default:
Task { @MainActor in WorkerNotificationCenter.shared.isAuthorized = false }
}
}
}
/// Fire-and-forget local notification. Returns true if scheduled
/// (best effort auth status may flip between scheduling and firing).
@discardableResult
func notify(title: String, body: String, identifier: String = UUID().uuidString) -> Bool {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
// Trigger immediately. nil means deliver right away. We use a
// 0.1s threshold trigger because some macOS builds are flaky
// with truly-instant local notifications from plugin bundles.
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let req = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(req) { err in
if let err = err {
WorkerDebugLog.write("notif schedule error: \(err)")
}
}
// Always bounce the dock as a backup signal works even when
// notifications are denied.
NSApp.requestUserAttention(.criticalRequest)
return isAuthorized
}
}