Files
luma/Luma/LocalNotifier.swift
Ole André Vadla Ravnås 9c65fc2fc9 Notify when a mission action awaits approval
Hook the existing LocalNotifier (macOS) and DesktopNotifier
(GTK) to observeAllPendingMissionActions so the user sees a
desktop notification when Luma needs an approval and the app
isn't frontmost. Coalesces multiple arrivals in one observe
tick into a single notification with "(+N more pending)".
2026-05-22 12:44:57 +02:00

87 lines
3.2 KiB
Swift

#if os(macOS)
import AppKit
import LumaCore
import UserNotifications
/// Shows native local notifications for incoming collaboration events —
/// a new member joining the lab, a new notebook entry, or a chat message.
/// Push notifications already cover offline users; this class closes the
/// blind spot where the Mac app is still connected to the portal but not
/// frontmost (backgrounded, hidden, or covered by other windows), so the
/// server never routes a push to APNs.
///
/// Notifications are suppressed while Luma is the active application (the
/// user is already looking at the event live).
@MainActor
final class LocalNotifier {
init() {}
static func requestAuthorization() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { _, _ in }
}
func notifyMemberAdded(_ member: LumaCore.CollaborationSession.Member, labID: String?) {
let name = displayName(member.user)
deliver(
kind: "member-added",
title: "\(name) joined",
body: "",
labID: labID
)
}
func notifyChatMessage(_ message: LumaCore.CollaborationSession.ChatMessage, labID: String?) {
let name = displayName(message.sender)
deliver(
kind: "chat-message",
title: name,
body: message.text,
labID: labID
)
}
func notifyActionAwaitingApproval(_ action: MissionAction, additionalPending: Int) {
let body: String
if additionalPending > 0 {
body = "\(action.toolName) (+\(additionalPending) more pending)"
} else {
body = action.toolName
}
deliver(
kind: "mission-action-pending",
title: "Luma needs approval",
body: body,
labID: nil
)
}
func notifyEntryAdded(_ entry: NotebookEntry, labID: String?) {
let author = entry.author?.name ?? "Someone"
let title = entry.title.isEmpty ? "(untitled)" : entry.title
deliver(
kind: "entry-added",
title: "\(author) added an entry",
body: title,
labID: labID
)
}
private func displayName(_ user: LumaCore.CollaborationSession.UserInfo) -> String {
user.name.isEmpty ? "@\(user.id)" : user.name
}
private func deliver(kind: String, title: String, body: String, labID: String?) {
if NSApplication.shared.isActive { return }
let content = UNMutableNotificationContent()
content.title = title
if !body.isEmpty {
content.body = body
}
content.sound = .default
let id = labID.map { "\(kind)-\($0)" } ?? kind
let request = UNNotificationRequest(identifier: id, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}
#endif