import type { NotificationDto, NotificationListResult } from "@edr/types"; import { NotificationBell, NotificationDrawer, NotificationToast, type NotificationItemData, } from "@edr/ui-common"; import { useState } from "react"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; import { resolveNotificationHref, resolveNotificationVisual, } from "./notificationConfig"; import { useInfiniteNotifications, useMarkAllRead, useMarkRead, useUnreadCount, } from "./useNotifications"; import { useNotificationSocket } from "./useNotificationSocket"; /** Map a server notification into the shared presentational item shape. */ function toItem(n: NotificationDto): NotificationItemData { return { id: n.id, type: n.type, title: n.title, body: n.body, createdAt: n.createdAt, isRead: n.isRead, priority: n.priority, link: n.link, data: n.data, }; } /** Flatten an infinite query's pages into the drawer's item shape. */ function toItems( data: { pages: NotificationListResult[] } | undefined, ): NotificationItemData[] { return (data?.pages ?? []).flatMap((p) => p.items).map(toItem); } /** * Wires react-query (infinite unread/read lists) + the notification WebSocket * into the shared bell + drawer. Lists are only fetched while the drawer is * open; the badge is driven by the lightweight unread-count query + socket. */ export default function NotificationBellContainer({ enabled = true, }: { enabled?: boolean; }) { const navigate = useNavigate(); const [opened, setOpened] = useState(false); const unreadQ = useInfiniteNotifications(false, enabled && opened); const readQ = useInfiniteNotifications(true, enabled && opened); const unread = useUnreadCount(enabled); const markRead = useMarkRead(); const markAllRead = useMarkAllRead(); const unreadItems = toItems(unreadQ.data); const readItems = toItems(readQ.data); const unreadCount = unread.data ?? 0; const handleItemClick = (item: NotificationItemData) => { if (!item.isRead) markRead.mutate(item.id); const href = resolveNotificationHref(item); setOpened(false); if (href) navigate(href); }; // Live push → rich toast that reuses the same registry + click action. useNotificationSocket(enabled, (n) => { const item = toItem(n); toast.custom( (t) => ( { toast.dismiss(t.id); handleItemClick(item); }} onDismiss={() => toast.dismiss(t.id)} /> ), { duration: 6000 }, ); }); return ( <> setOpened(true)} /> setOpened(false)} unread={unreadItems} read={readItems} unreadCount={unreadCount} loading={opened && (unreadQ.isLoading || readQ.isLoading)} hasMoreUnread={unreadQ.hasNextPage} hasMoreRead={readQ.hasNextPage} loadingMoreUnread={unreadQ.isFetchingNextPage} loadingMoreRead={readQ.isFetchingNextPage} onLoadMoreUnread={() => { if (unreadQ.hasNextPage && !unreadQ.isFetchingNextPage) { void unreadQ.fetchNextPage(); } }} onLoadMoreRead={() => { if (readQ.hasNextPage && !readQ.isFetchingNextPage) { void readQ.fetchNextPage(); } }} onItemClick={handleItemClick} onMarkAllRead={() => markAllRead.mutate()} resolveVisual={resolveNotificationVisual} /> ); }