Fix toast accept button: instant feedback + double-click guard

Toast buttons are static Sonner elements that can't bind React state,
so clicks had no visual feedback and allowed duplicate mutations.
Now: dismiss custom toast immediately, show loading toast, and block
concurrent clicks via a ref-based Set guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-04 19:38:29 +08:00
parent 60281caa64
commit dff36f30c8

View File

@ -14,16 +14,30 @@ export default function NotificationToaster() {
const maxSeenIdRef = useRef(0); const maxSeenIdRef = useRef(0);
const initializedRef = useRef(false); const initializedRef = useRef(false);
const prevUnreadRef = useRef(0); const prevUnreadRef = useRef(0);
// Track in-flight request IDs so repeated clicks are blocked
const respondingRef = useRef<Set<number>>(new Set());
const handleConnectionRespond = useCallback( const handleConnectionRespond = useCallback(
async (requestId: number, action: 'accept' | 'reject', toastId: string | number) => { 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 { try {
await respond({ requestId, action }); 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'); toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined');
} catch (err) { } catch (err) {
toast.dismiss(toastId); toast.dismiss(loadingId);
toast.error(getErrorMessage(err, 'Failed to respond to request')); toast.error(getErrorMessage(err, 'Failed to respond to request'));
} finally {
respondingRef.current.delete(requestId);
} }
}, },
[respond], [respond],