mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
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) => (
|
|
<NotificationToast
|
|
item={item}
|
|
visual={resolveNotificationVisual(item)}
|
|
visible={t.visible}
|
|
onClick={() => {
|
|
toast.dismiss(t.id);
|
|
handleItemClick(item);
|
|
}}
|
|
onDismiss={() => toast.dismiss(t.id)}
|
|
/>
|
|
),
|
|
{ duration: 6000 },
|
|
);
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<NotificationBell
|
|
unreadCount={unreadCount}
|
|
onClick={() => setOpened(true)}
|
|
/>
|
|
<NotificationDrawer
|
|
opened={opened}
|
|
onClose={() => 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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|