import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Bell, Eye, RotateCcw, Search, Trash2 } from "lucide-react"; import { api, type FeedFilters } from "../lib/api"; import { clearAll, dismiss, getNotifications, getUnreadCount, markAllRead, notify, subscribe, type VideoHiddenMeta, type VideoWatchedMeta, } from "../lib/notifications"; import { LEVEL_STYLE } from "./Toaster"; function relTime(ts: number): string { const s = Math.round((Date.now() - ts) / 1000); if (s < 60) return "just now"; const m = Math.round(s / 60); if (m < 60) return `${m}m ago`; const h = Math.round(m / 60); if (h < 24) return `${h}h ago`; const d = Math.round(h / 24); return `${d}d ago`; } export default function NotificationCenter({ filters, setFilters, }: { filters: FeedFilters; setFilters: (f: FeedFilters) => void; }) { const notifications = useSyncExternalStore(subscribe, getNotifications, getNotifications); const unread = useSyncExternalStore(subscribe, getUnreadCount, getUnreadCount); const [open, setOpen] = useState(false); const wrap = useRef(null); const qc = useQueryClient(); // Close when clicking anywhere outside the panel. useEffect(() => { if (!open) return; function onDown(e: MouseEvent) { if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false); } document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [open]); // Opening the center marks everything read. useEffect(() => { if (open) markAllRead(); }, [open, notifications.length]); function locateHidden(meta: VideoHiddenMeta) { setFilters({ ...filters, show: "hidden", channelId: meta.channelId || undefined, channelName: meta.channelName || undefined, }); setOpen(false); } function revertState(meta: VideoHiddenMeta | VideoWatchedMeta, verb: string) { api .setState(meta.videoId, "new") .then(() => { qc.invalidateQueries({ queryKey: ["feed"] }); qc.invalidateQueries({ queryKey: ["feed-count"] }); notify({ level: "success", message: `${verb} “${meta.title}”` }); }) .catch(() => {}); } return (
{open && (
Notifications
{notifications.length > 0 && ( )}
{notifications.length === 0 ? (
No notifications yet.
) : ( notifications.map((n) => { const { icon: Icon, color } = LEVEL_STYLE[n.level]; const needsAction = n.requiresInteraction && !n.dismissed; const hidden = n.meta?.kind === "video-hidden" ? n.meta : null; const watched = n.meta?.kind === "video-watched" ? n.meta : null; return (
{n.title &&
{n.title}
}
{n.message}
{relTime(n.ts)} {needsAction && ( Action needed )}
{hidden ? (
) : watched ? (
) : ( n.action && !n.dismissed && ( ) )}
); }) )}
)}
); }