/** * Times the fade-out of dumps leaving an upvoted list, so that a vote withdrawn * by mistake can be cast again before the dump is gone. * * @module */ import { useCallback, useEffect, useRef, useState } from "react"; /** * The phase of a fading item, which the list maps to a CSS class. `cooldown` * lasts 2 seconds, and `dismissing` lasts the 350ms of the CSS collapse. */ export type FadeState = "cooldown" | "dismissing"; /** * Returned by {@link useFading} to `UserUpvoted` and the profile's upvoted * section. The list keeps showing an item while it is in `fading`, then drops * it. */ export interface UseFadingResult { fading: Record; /** Does nothing when `id` is already fading. */ startFading: (id: string) => void; /** Called when the vote is cast again: the item stays. */ cancelFading: (id: string) => void; /** Called when the list changes to another user's. */ cancelAll: () => void; } /** * Returns the fading state of a list's items and the calls that drive it. The * timers are cleared on unmount. */ export function useFading(): UseFadingResult { const [fading, setFading] = useState>({}); const cancels = useRef void>>(new Map()); useEffect(() => () => { cancels.current.forEach((c) => c()); }, []); const startFading = useCallback((id: string) => { if (cancels.current.has(id)) return; // `kill` is replaced as each timer is set, so it always clears the one // running; `dead` stops a timer that fired before it was cleared. let dead = false; let kill = () => {}; kill = () => { dead = true; setFading((f) => { const n = { ...f }; delete n[id]; return n; }); cancels.current.delete(id); }; cancels.current.set(id, () => kill()); setFading((f) => ({ ...f, [id]: "cooldown" })); const t1 = setTimeout(() => { if (dead) return; setFading((f) => ({ ...f, [id]: "dismissing" })); const t2 = setTimeout(() => { if (!dead) kill(); }, 350); kill = () => { dead = true; clearTimeout(t2); setFading((f) => { const n = { ...f }; delete n[id]; return n; }); cancels.current.delete(id); }; cancels.current.set(id, () => kill()); }, 2000); kill = () => { dead = true; clearTimeout(t1); setFading((f) => { const n = { ...f }; delete n[id]; return n; }); cancels.current.delete(id); }; cancels.current.set(id, () => kill()); }, []); const cancelFading = useCallback((id: string) => { cancels.current.get(id)?.(); }, []); const cancelAll = useCallback(() => { cancels.current.forEach((c) => c()); }, []); return { fading, startFading, cancelFading, cancelAll }; }