diff --git a/frontend/src/components/notifications/NotificationToaster.tsx b/frontend/src/components/notifications/NotificationToaster.tsx index 0dff878..4ebd234 100644 --- a/frontend/src/components/notifications/NotificationToaster.tsx +++ b/frontend/src/components/notifications/NotificationToaster.tsx @@ -14,16 +14,30 @@ export default function NotificationToaster() { const maxSeenIdRef = useRef(0); const initializedRef = useRef(false); const prevUnreadRef = useRef(0); + // Track in-flight request IDs so repeated clicks are blocked + const respondingRef = useRef>(new Set()); const handleConnectionRespond = useCallback( async (requestId: number, action: 'accept' | 'reject', toastId: string | number) => { + // Guard against double-clicks (Sonner toasts are static, no disabled prop) + if (respondingRef.current.has(requestId)) return; + respondingRef.current.add(requestId); + + // Immediately dismiss the custom toast and show a loading indicator + toast.dismiss(toastId); + const loadingId = toast.loading( + action === 'accept' ? 'Accepting connection…' : 'Declining request…', + ); + try { await respond({ requestId, action }); - // onSuccess in useConnections already dismisses the custom toast and invalidates caches + toast.dismiss(loadingId); toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined'); } catch (err) { - toast.dismiss(toastId); + toast.dismiss(loadingId); toast.error(getErrorMessage(err, 'Failed to respond to request')); + } finally { + respondingRef.current.delete(requestId); } }, [respond],