101 lines
2.8 KiB
TypeScript
101 lines
2.8 KiB
TypeScript
/**
|
|
* 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<string, FadeState>;
|
|
/** 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<Record<string, FadeState>>({});
|
|
const cancels = useRef<Map<string, () => 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 };
|
|
}
|