import { useEffect, useState } from 'react'; import { nextVisitBoundary } from '../utils/visitInvitationWords'; // A timer longer than this is clamped by browsers; a later boundary is reached by waking again. const LONGEST_WAIT_MS = 2 ** 31 - 1; /** * The current time for showing visits. Read afresh on every render, so a visit that arrives late or whose time changed * is placed against the time it is now, not the time the component first mounted. A timer re-renders at the next start * or end among the visits shown: an open page stops offering Accept at a visit's start and moves it to history at its * end without a reload. */ export const useVisitClock = (visits: ReadonlyArray<{ start?: string; end?: string }>): number => { const [tick, setTick] = useState(0); const now = Date.now(); const key = visits.map((visit) => `${visit.start ?? ''}|${visit.end ?? ''}`).join(','); useEffect(() => { const current = Date.now(); const next = visits .map((visit) => nextVisitBoundary(visit, current)) .filter((at): at is number => at !== undefined) .sort((a, b) => a - b)[0]; if (next === undefined) return undefined; const timer = setTimeout(() => setTick((value) => value + 1), Math.min(Math.max(next - current, 0), LONGEST_WAIT_MS)); return () => clearTimeout(timer); // `key` stands for the visits' times; `tick` re-arms the timer after each boundary. // eslint-disable-next-line react-hooks/exhaustive-deps }, [key, tick]); return now; };