From ee4a5b46f8be568da039f757e755bc9aae10a66d Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 25 Oct 2026 13:56:40 +0100 Subject: [PATCH] refactor(player): the watched toggle is stamped at each answer, with no hold while a write is out The hold changed nothing a user could see (each answer stamps the toggle right after its own echo), and it would have outranked a mark made elsewhere after the click while the write was still out. --- frontend/src/features/player/PlayerModal.tsx | 30 +++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/frontend/src/features/player/PlayerModal.tsx b/frontend/src/features/player/PlayerModal.tsx index dc7d387c..1e57af5e 100644 --- a/frontend/src/features/player/PlayerModal.tsx +++ b/frontend/src/features/player/PlayerModal.tsx @@ -163,12 +163,14 @@ const META_OWN_LINE = "w-full [&>span:first-child>[data-meta-sep]]:hidden"; // the line wraps and the meta, or the actions after it, move down instead of squeezing. const META_MIN = "min-w-48 flex-1"; -// The watched toggle's own word on an item: what it last asked for, how many of its writes are still -// on the way, and when the last of them was answered (Infinity until then). -type OwnMark = { status: VideoStatus; seq: number; pending: number; at: number }; +// The watched toggle's own word on an item: what it last asked for (`seq`, its write), and when — its +// click, then the server's last answer to any of its writes. +type OwnMark = { status: VideoStatus; seq: number; at: number }; -// One of the toggle's writes was answered: stamped once none is left on the way; the latest refused, -// the toggle's word is dropped and the item shows what the server holds. +// One of the toggle's writes was answered. Its answer has just come back as a session mark too (the +// write marks before it resolves), so the toggle's word is stamped after it and outranks it; a later +// mark from elsewhere still outranks the toggle. The latest write refused, the toggle's word is +// dropped and the item shows what the server holds. function settled(marks: Record, id: string, seq: number, ok: boolean) { const own = marks[id]; if (!own) return marks; @@ -177,8 +179,7 @@ function settled(marks: Record, id: string, seq: number, ok: bo delete rest[id]; return rest; } - const pending = own.pending - 1; - return { ...marks, [id]: { ...own, pending, at: pending > 0 ? Infinity : Date.now() } }; + return { ...marks, [id]: { ...own, at: Date.now() } }; } export default function PlayerModal({ @@ -203,7 +204,7 @@ export default function PlayerModal({ onClose: () => void; /** The player's own marks (its watched toggle, the auto-mark at the end), for the item it is on — * named by `video`, which may be none of the host's (a related video played in place). Returns - * the write: the toggle holds its own word until the server has answered, and drops a refused one. */ + * the write: the toggle weighs its own word at the server's answer, and drops a refused one. */ onState: (id: string, status: VideoStatus, video?: Video) => Promise; /** A write the related list made itself, for a host that refetches once the player closes and its * writes have landed (the feed's player session). */ @@ -573,10 +574,10 @@ export default function PlayerModal({ // The newest word wins: this toggle's own mark, or one the session confirmed elsewhere since the // player opened (the related row's own menu, a notice's Undo — `useVideoMarks`), else the item as // it was opened. The toggle alone missed those, and its first click re-sent the same status. - // Both are weighed when the server answered them: the toggle's own writes come back as session - // marks too, stamped at their answer, so a click weighed at its own time lost to the late answer - // of the click before it. While any of its writes is on the way the toggle's word holds; a refused - // one is dropped, so the toggle does not keep showing what the server turned down. + // The toggle's own writes come back as session marks too, stamped at their answer, so a click + // weighed only at its own time lost to the late answer of the click before it: each answer stamps + // the toggle's word again (`settled`). A refused one is dropped, so the toggle does not keep showing + // what the server turned down. const [marks, setMarks] = useState>({}); const ownSeq = useRef(0); const [openedAt] = useState(() => Date.now()); @@ -593,10 +594,7 @@ export default function PlayerModal({ const id = subject.id; const next = on ? "watched" : "new"; const seq = ++ownSeq.current; - setMarks((m) => ({ - ...m, - [id]: { status: next, seq, pending: (m[id]?.pending ?? 0) + 1, at: Infinity }, - })); + setMarks((m) => ({ ...m, [id]: { status: next, seq, at: Date.now() } })); Promise.resolve(onState(id, next, subject)).then( () => setMarks((m) => settled(m, id, seq, true)), () => setMarks((m) => settled(m, id, seq, false)),