style: improve the ui of the toast for notifications

This commit is contained in:
Nathnael
2026-07-06 10:38:27 +00:00
parent abe26c2c60
commit ca3a6072d8
7 changed files with 244 additions and 38 deletions

View File

@@ -1,10 +1,12 @@
import type { NotificationListResult } from "@edr/types";
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 {
@@ -19,22 +21,25 @@ import {
} 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,
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((n) => ({
id: n.id,
type: n.type,
title: n.title,
body: n.body,
createdAt: n.createdAt,
isRead: n.isRead,
link: n.link,
data: n.data,
}));
return (data?.pages ?? []).flatMap((p) => p.items).map(toItem);
}
/**
@@ -55,7 +60,6 @@ export default function NotificationBellContainer({
const unread = useUnreadCount(enabled);
const markRead = useMarkRead();
const markAllRead = useMarkAllRead();
useNotificationSocket(enabled);
const unreadItems = toItems(unreadQ.data);
const readItems = toItems(readQ.data);
@@ -68,6 +72,26 @@ export default function NotificationBellContainer({
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

View File

@@ -4,8 +4,7 @@ import {
type NotificationDto,
} from "@edr/types";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import toast from "react-hot-toast";
import { useEffect, useRef } from "react";
import { io } from "socket.io-client";
import { API_BASE_URL } from "@/constants/apiConfig";
@@ -19,10 +18,16 @@ const SOCKET_ORIGIN = String(API_BASE_URL ?? "").replace(/\/api\/?$/, "");
/**
* Subscribes to live notification pushes for the signed-in staff user. New
* items invalidate the list + toast; unread-count pushes update the badge.
* items invalidate the cached lists + fire `onNew` (the host shows a rich
* toast); unread-count pushes update the badge.
*/
export function useNotificationSocket(enabled: boolean) {
export function useNotificationSocket(
enabled: boolean,
onNew?: (notification: NotificationDto) => void,
) {
const qc = useQueryClient();
const onNewRef = useRef(onNew);
onNewRef.current = onNew;
useEffect(() => {
if (!enabled) return;
@@ -37,7 +42,8 @@ export function useNotificationSocket(enabled: boolean) {
socket.on(NOTIFICATION_WS_EVENTS.NEW, (n: NotificationDto) => {
qc.invalidateQueries({ queryKey: NOTIFICATIONS_KEY });
toast(n.title);
qc.invalidateQueries({ queryKey: UNREAD_KEY });
onNewRef.current?.(n);
});
socket.on(NOTIFICATION_WS_EVENTS.UNREAD_COUNT, (count: number) => {