Files
siftlode/frontend/src/features/player/useOpenLinkedPlayer.ts
T

71 lines
3.4 KiB
TypeScript

import { useEffect, useEffectEvent, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api, type Video } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { linkedPlayer, PLAYER_KEYS } from "../../lib/playerUrlState";
import { clearParams } from "../../lib/urlState";
import { notifyActionError } from "../../lib/actionErrors";
import { msg } from "../../lib/notifications";
/** Opens the player an address names (a reload, a bookmark — lib/playerUrlState) on the page that
* hosts it, once: read as the host mounts, opened once its list is in (`ready`). From the list when the
* item is in it (`find`), so the player plays on through the list; else alone, its row fetched from
* the catalog. A row the catalog no longer has is said, not swallowed.
*
* `scope` names the list the address put it over (a playlist, a live search): when the host moves to
* another before it opened, the address under it named something else, and the player is given up —
* opened later, it would start over a list nobody linked, unasked. So it is when a player opened
* meanwhile (a card clicked while the row was on its way): the address names that one now. */
export function useOpenLinkedPlayer(
ready: boolean,
find: (id: string) => Video | undefined,
open: (video: Video, inList: boolean) => void,
scope: string | number | null = null,
): void {
const [linked] = useState(() => linkedPlayer()?.watch ?? null);
const [linkedScope] = useState(scope);
// Opened, said to be gone or given up, once. A ref for the effect (a StrictMode re-run of it must
// not open it twice); the state stops the lookup and the row fetch on screen after it.
const done = useRef(linked == null);
const [settled, setSettled] = useState(linked == null);
const moved = scope !== linkedScope;
const waiting = ready && !moved && !settled && linked != null;
const listed = waiting ? find(linked) : undefined;
const row = useQuery({
queryKey: qk.videoRow(linked ?? ""),
queryFn: () => api.videoRow(linked!),
enabled: waiting && !listed,
retry: false,
});
const openIt = useEffectEvent(open);
const failed = useEffectEvent((err: unknown) => {
notifyActionError(err, msg("player.linkedFailed"));
clearParams(...PLAYER_KEYS);
});
useEffect(() => {
if (done.current) return;
const settle = () => {
done.current = true;
// State, not derived: a one-way decision taken as the list or the row arrives. Derived from
// `listed`, the lookup and the row fetch would come back whenever the video left the list.
setSettled(true);
};
// A player opened since names its own: this one is not wanted any more.
if (linkedPlayer()?.watch !== linked) return settle();
if (moved) {
// The address goes on naming it under the new list (a reload would open it there).
clearParams(...PLAYER_KEYS);
return settle();
}
// Not before the host is ready: the row's cache entry is shared, so another opener's fetch fills
// it here too (a disabled query still reads the cache) — the shell's player over a page that
// plays its own would open a second one.
if (!ready) return;
if (listed) openIt(listed, true);
else if (row.data) openIt(row.data, false);
else if (row.isError) failed(row.error);
else return;
settle();
}, [linked, moved, ready, listed, row.data, row.isError, row.error]);
}