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]); }