Add live countdown to Blobbi overstimulation toast
Show remaining seconds in the blocked toast (e.g. 'calm down for 4s') and update it every second via the toast update mechanism. The toast is automatically dismissed when the blocked phase ends. All timer state is owned by useOverstimulationReaction so the UI layer stays simple.
This commit is contained in:
@@ -162,6 +162,8 @@ export function useOverstimulationReaction({
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const blockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const toastShownRef = useRef(false);
|
||||
const toastHandleRef = useRef<ReturnType<typeof toast> | null>(null);
|
||||
const countdownIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const prevTimeRef = useRef(0);
|
||||
|
||||
// Keep profile in a ref so the rAF loop doesn't need it as a dep
|
||||
@@ -184,6 +186,17 @@ export function useOverstimulationReaction({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearCountdown = useCallback(() => {
|
||||
if (countdownIntervalRef.current !== null) {
|
||||
clearInterval(countdownIntervalRef.current);
|
||||
countdownIntervalRef.current = null;
|
||||
}
|
||||
if (toastHandleRef.current) {
|
||||
toastHandleRef.current.dismiss();
|
||||
toastHandleRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Push visible state if the delta is meaningful.
|
||||
*
|
||||
@@ -257,25 +270,37 @@ export function useOverstimulationReaction({
|
||||
phaseRef.current = 'blocked';
|
||||
setPhase('blocked');
|
||||
|
||||
const duration = BLOCK_MIN_MS + Math.random() * (BLOCK_MAX_MS - BLOCK_MIN_MS);
|
||||
const totalSeconds = Math.ceil(duration / 1000);
|
||||
let remaining = totalSeconds;
|
||||
|
||||
if (!toastShownRef.current) {
|
||||
toastShownRef.current = true;
|
||||
toast({
|
||||
const handle = toast({
|
||||
title: 'Too many clicks!',
|
||||
description: 'Blobbi is overwhelmed and blocked clicks for a few seconds.',
|
||||
description: `Blobbi is overwhelmed\u2026 calm down for ${remaining}s`,
|
||||
});
|
||||
}
|
||||
toastHandleRef.current = handle;
|
||||
|
||||
const duration = BLOCK_MIN_MS + Math.random() * (BLOCK_MAX_MS - BLOCK_MIN_MS);
|
||||
clearCountdown();
|
||||
countdownIntervalRef.current = setInterval(() => {
|
||||
remaining -= 1;
|
||||
if (remaining > 0 && toastHandleRef.current) {
|
||||
toastHandleRef.current.update({ id: toastHandleRef.current.id, title: 'Too many clicks!', description: `Blobbi is overwhelmed\u2026 calm down for ${remaining}s` });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
clearBlockTimer();
|
||||
blockTimerRef.current = setTimeout(() => {
|
||||
blockTimerRef.current = null;
|
||||
clearCountdown();
|
||||
phaseRef.current = 'cooling';
|
||||
setPhase('cooling');
|
||||
prevTimeRef.current = performance.now();
|
||||
startRafLoopRef.current();
|
||||
}, duration);
|
||||
}, [clearBlockTimer]);
|
||||
}, [clearBlockTimer, clearCountdown]);
|
||||
|
||||
// ── Global click handler ──
|
||||
|
||||
@@ -284,6 +309,7 @@ export function useOverstimulationReaction({
|
||||
// Reset everything
|
||||
clearRaf();
|
||||
clearBlockTimer();
|
||||
clearCountdown();
|
||||
clicksRef.current = [];
|
||||
levelRef.current = 0;
|
||||
phaseRef.current = 'idle';
|
||||
@@ -341,15 +367,16 @@ export function useOverstimulationReaction({
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
};
|
||||
}, [isActive, clearRaf, clearBlockTimer, pushVisible, enterBlocked]);
|
||||
}, [isActive, clearRaf, clearBlockTimer, clearCountdown, pushVisible, enterBlocked]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearRaf();
|
||||
clearBlockTimer();
|
||||
clearCountdown();
|
||||
};
|
||||
}, [clearRaf, clearBlockTimer]);
|
||||
}, [clearRaf, clearBlockTimer, clearCountdown]);
|
||||
|
||||
// ── Resolve level + phase → recipe ──
|
||||
|
||||
|
||||
Reference in New Issue
Block a user