Files
byre/mac/Sources/Byre/Notifications.swift
T

133 lines
4.1 KiB
Swift

import AppKit
import UserNotifications
@MainActor
final class Notifier: NSObject, UNUserNotificationCenterDelegate {
static let shared = Notifier()
weak var connection: Connection?
var showWindow: (() -> Void)?
private var pending: String?
func install() {
UNUserNotificationCenter.current().delegate = self
}
private func open(_ taskId: String) {
NSApp.activate()
showWindow?()
pending = taskId
deliver()
}
func deliver() {
guard let taskId = pending, let store = connection?.store else {
return
}
pending = nil
if !taskId.isEmpty {
store.focusTask(taskId)
}
}
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.banner, .list, .sound]
}
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
let taskId = response.notification.request.content.userInfo["task_id"] as? String ?? ""
await MainActor.run {
open(taskId)
}
}
}
private func wants(_ p: NotificationPrefs, _ item: InboxItem) -> Bool {
switch item.kind {
case .question: p.questions
case .sessionError: p.errors
case .rateLimited: p.rateLimits
}
}
private func itemText(_ item: InboxItem) -> String {
let p = item.payload.value
switch item.kind {
case .question: return str(p, "question") ?? ""
case .sessionError: return "Error: \(str(p, "error") ?? "")"
case .rateLimited: return "Rate limited"
}
}
@MainActor
private var queue: Task<Void, Never>?
@MainActor
private func enqueue(_ op: @escaping @MainActor () async -> Void) {
let prev = queue
queue = Task {
await prev?.value
await op()
}
}
@MainActor
private func show(_ store: Store, _ taskId: String, _ tag: String, _ text: String) {
let line = trimmed(text).split(separator: "\n", omittingEmptySubsequences: false).first.map(String.init) ?? ""
let content = UNMutableNotificationContent()
content.title = store.findTask(taskId).map { "byre · \($0.title)" } ?? "byre"
content.body = line.count > 200 ? "\(line.prefix(199))…" : line
content.sound = .default
content.userInfo = ["task_id": taskId]
let request = UNNotificationRequest(identifier: tag, content: content, trigger: nil)
enqueue {
let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
if status != .authorized && status != .provisional {
return
}
do {
try await UNUserNotificationCenter.current().add(request)
} catch {
log.error("show notification: \(errorText(error), privacy: .public)")
}
}
}
@MainActor
func notifyInbox(_ store: Store, _ item: InboxItem) {
guard let me = store.me, wants(me.notificationPrefs, item) else {
return
}
show(store, item.taskId, item.id, itemText(item))
}
@MainActor
func clearInbox(_ item: InboxItem) {
if item.state == .open {
return
}
enqueue {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [item.id])
}
}
@MainActor
func clearTurns(_ taskId: String, through seq: Int64) {
enqueue {
let ids = await UNUserNotificationCenter.current().deliveredNotifications().map(\.request).filter { r in
guard r.content.userInfo["task_id"] as? String == taskId, r.identifier.hasPrefix("turn-"), let s = Int64(r.identifier.dropFirst(5)) else {
return false
}
return s <= seq
}.map(\.identifier)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
}
}
@MainActor
func notifyTurn(_ store: Store, _ ev: ByreEvent) {
guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty else {
return
}
show(store, taskId, "turn-\(ev.seq)", str(ev.payload.value, "result") ?? "")
}