import { listen } from "@tauri-apps/api/event"; import { isPermissionGranted, requestPermission, sendNotification, } from "@tauri-apps/plugin-notification"; import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { listNotifications, markAllNotificationsRead, markNotificationRead, } from "../../lib/api"; import type { OrchaiNotification } from "../../lib/types"; import { buttonClass, cardClass, pillClass } from "../ui/primitives"; type NewNotificationEvent = { notification: OrchaiNotification; }; async function showSystemNotification(notification: OrchaiNotification) { try { let permissionGranted = await isPermissionGranted(); if (!permissionGranted) { const permission = await requestPermission(); permissionGranted = permission === "granted"; } if (permissionGranted) { sendNotification({ title: notification.title, body: notification.message, }); } } catch { // Best effort only } } export default function NotificationCenter() { const navigate = useNavigate(); const { projectId } = useParams(); const containerRef = useRef(null); const [open, setOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [filter, setFilter] = useState<"all" | "unread" | "errors" | "fixes">( "all" ); async function loadNotifications() { if (!projectId) { setNotifications([]); return; } try { const items = await listNotifications(projectId, false); setNotifications(items); } catch { // Ignore load errors in layout chrome } } useEffect(() => { loadNotifications(); }, [projectId]); useEffect(() => { let unlisten: (() => void) | null = null; const setup = async () => { unlisten = await listen("new-notification", (event) => { const incoming = event.payload.notification; if (projectId && incoming.project_id !== projectId) { return; } setNotifications((prev) => { const withoutDuplicate = prev.filter((n) => n.id !== incoming.id); return [incoming, ...withoutDuplicate]; }); void showSystemNotification(incoming); }); }; void setup(); return () => { if (unlisten) { unlisten(); } }; }, [projectId]); useEffect(() => { if (!open) { return; } function handleOutsideClick(event: MouseEvent) { const target = event.target as Node | null; if (!containerRef.current || !target) { return; } if (!containerRef.current.contains(target)) { setOpen(false); } } function handleEscape(event: KeyboardEvent) { if (event.key === "Escape") { setOpen(false); } } document.addEventListener("mousedown", handleOutsideClick); document.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handleOutsideClick); document.removeEventListener("keydown", handleEscape); }; }, [open]); const unreadCount = useMemo( () => notifications.filter((n) => !n.read).length, [notifications] ); const filteredNotifications = useMemo(() => { switch (filter) { case "unread": return notifications.filter((n) => !n.read); case "errors": return notifications.filter((n) => n.notification_type === "Error"); case "fixes": return notifications.filter((n) => n.notification_type === "FixReady"); default: return notifications; } }, [filter, notifications]); async function handleOpenNotification(notification: OrchaiNotification) { if (!notification.read) { try { await markNotificationRead(notification.id); setNotifications((prev) => prev.map((n) => (n.id === notification.id ? { ...n, read: true } : n)) ); } catch { // ignore } } setOpen(false); if (notification.ticket_id) { navigate(`/tickets/${notification.ticket_id}`); return; } navigate(`/projects/${notification.project_id}`); } async function handleMarkAllRead() { if (!projectId) { return; } try { await markAllNotificationsRead(projectId); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); } catch { // ignore } } return (
{open && (

Notifications

{[ { id: "all", label: "All" }, { id: "unread", label: "Unread" }, { id: "errors", label: "Errors" }, { id: "fixes", label: "Fixes" }, ].map((item) => ( ))}
{filteredNotifications.length === 0 ? (
No notifications.
) : ( filteredNotifications.map((notification) => ( )) )}
)}
); }