Mark notification as read when accepting via toast

Toast accept/reject now calls markRead on the corresponding notification
so it clears from unread in the notifications tab. Uses markReadRef to
avoid stale closure in Sonner toast callbacks. Covers both success and
409 (already resolved) paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-05 18:32:54 +08:00
parent 1e736eb333
commit 053c2ae85e

View File

@ -9,7 +9,7 @@ import { getErrorMessage } from '@/lib/api';
import type { AppNotification } from '@/types'; import type { AppNotification } from '@/types';
export default function NotificationToaster() { export default function NotificationToaster() {
const { notifications, unreadCount } = useNotifications(); const { notifications, unreadCount, markRead } = useNotifications();
const { respond } = useConnections(); const { respond } = useConnections();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const maxSeenIdRef = useRef(0); const maxSeenIdRef = useRef(0);
@ -20,9 +20,11 @@ export default function NotificationToaster() {
// Always call the latest respond — Sonner toasts capture closures at creation time // Always call the latest respond — Sonner toasts capture closures at creation time
const respondRef = useRef(respond); const respondRef = useRef(respond);
respondRef.current = respond; respondRef.current = respond;
const markReadRef = useRef(markRead);
markReadRef.current = markRead;
const handleConnectionRespond = useCallback( const handleConnectionRespond = useCallback(
async (requestId: number, action: 'accept' | 'reject', toastId: string | number) => { async (requestId: number, action: 'accept' | 'reject', toastId: string | number, notificationId: number) => {
// Guard against double-clicks (Sonner toasts are static, no disabled prop) // Guard against double-clicks (Sonner toasts are static, no disabled prop)
if (respondingRef.current.has(requestId)) return; if (respondingRef.current.has(requestId)) return;
respondingRef.current.add(requestId); respondingRef.current.add(requestId);
@ -37,11 +39,13 @@ export default function NotificationToaster() {
await respondRef.current({ requestId, action }); await respondRef.current({ requestId, action });
toast.dismiss(loadingId); toast.dismiss(loadingId);
toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined'); toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined');
markReadRef.current([notificationId]).catch(() => {});
} catch (err) { } catch (err) {
toast.dismiss(loadingId); toast.dismiss(loadingId);
// 409 means the request was already resolved (e.g. accepted via notification center) // 409 means the request was already resolved (e.g. accepted via notification center)
if (axios.isAxiosError(err) && err.response?.status === 409) { if (axios.isAxiosError(err) && err.response?.status === 409) {
toast.success(action === 'accept' ? 'Connection already accepted' : 'Request already resolved'); toast.success(action === 'accept' ? 'Connection already accepted' : 'Request already resolved');
markReadRef.current([notificationId]).catch(() => {});
} else { } else {
toast.error(getErrorMessage(err, 'Failed to respond to request')); toast.error(getErrorMessage(err, 'Failed to respond to request'));
} }
@ -119,14 +123,14 @@ export default function NotificationToaster() {
</p> </p>
<div className="flex items-center gap-2 mt-3"> <div className="flex items-center gap-2 mt-3">
<button <button
onClick={() => handleConnectionRespond(requestId, 'accept', id)} onClick={() => handleConnectionRespond(requestId, 'accept', id, notification.id)}
className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md bg-accent text-accent-foreground hover:bg-accent/90 transition-colors" className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md bg-accent text-accent-foreground hover:bg-accent/90 transition-colors"
> >
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
Accept Accept
</button> </button>
<button <button
onClick={() => handleConnectionRespond(requestId, 'reject', id)} onClick={() => handleConnectionRespond(requestId, 'reject', id, notification.id)}
className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md text-muted-foreground hover:bg-card-elevated transition-colors" className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md text-muted-foreground hover:bg-card-elevated transition-colors"
> >
<X className="h-3.5 w-3.5" /> <X className="h-3.5 w-3.5" />