SEC-cyBERT/labelapp/hooks/use-active-timer.ts
2026-03-29 16:37:51 -04:00

106 lines
3.0 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
const IDLE_THRESHOLD_MS = 30_000; // 30 seconds of inactivity = idle
const TICK_INTERVAL_MS = 1_000; // Update display every second
const ACTIVITY_EVENTS = [
"mousemove",
"mousedown",
"keydown",
"scroll",
"touchstart",
] as const;
export function useActiveTimer() {
const [activeMs, setActiveMs] = useState(0);
const [totalMs, setTotalMs] = useState(0);
const [isIdle, setIsIdle] = useState(false);
// Refs for values that change frequently but shouldn't trigger re-renders
const activeMsRef = useRef(0);
const totalMsRef = useRef(0);
const lastActivityRef = useRef(Date.now());
const lastTickRef = useRef(Date.now());
const idleRef = useRef(false);
const startedRef = useRef(Date.now());
const reset = useCallback(() => {
const now = Date.now();
activeMsRef.current = 0;
totalMsRef.current = 0;
lastActivityRef.current = now;
lastTickRef.current = now;
startedRef.current = now;
idleRef.current = false;
setActiveMs(0);
setTotalMs(0);
setIsIdle(false);
}, []);
// Mark activity on user interaction
useEffect(() => {
function onActivity() {
lastActivityRef.current = Date.now();
if (idleRef.current) {
idleRef.current = false;
setIsIdle(false);
// Reset the tick baseline so we don't count the idle gap as active
lastTickRef.current = Date.now();
}
}
for (const event of ACTIVITY_EVENTS) {
window.addEventListener(event, onActivity, { passive: true });
}
// Also track when the tab regains focus
window.addEventListener("focus", onActivity);
return () => {
for (const event of ACTIVITY_EVENTS) {
window.removeEventListener(event, onActivity);
}
window.removeEventListener("focus", onActivity);
};
}, []);
// Tick loop: accumulate active time and detect idle transitions
useEffect(() => {
const interval = setInterval(() => {
const now = Date.now();
const elapsed = now - lastTickRef.current;
lastTickRef.current = now;
// Always accumulate wall-clock time
totalMsRef.current = now - startedRef.current;
setTotalMs(totalMsRef.current);
const timeSinceActivity = now - lastActivityRef.current;
if (timeSinceActivity >= IDLE_THRESHOLD_MS) {
// User is idle — don't accumulate active time
if (!idleRef.current) {
idleRef.current = true;
setIsIdle(true);
}
} else {
// User is active — accumulate
activeMsRef.current += elapsed;
setActiveMs(activeMsRef.current);
}
}, TICK_INTERVAL_MS);
return () => clearInterval(interval);
}, []);
return {
/** Milliseconds the user was actively engaged (idle time excluded) */
activeMs,
/** Total wall-clock milliseconds since last reset */
totalMs,
/** Whether the user is currently idle */
isIdle,
/** Reset both counters (call when loading a new paragraph) */
reset,
};
}