/** * Keeps a page's list of dumps in step with the socket: the dumps deleted, * edited and posted, their comment counts, and their posters' names. * * @module */ import { useEffect, useLayoutEffect, useRef } from "react"; import type { Dump } from "../../../shared/dump.ts"; import { useWS, useWSEvent } from "../ws/useWS.ts"; import { insertAt, usePositionMemory } from "../feed/usePositionMemory.ts"; /** * Passed to {@link useDumpListSync} by the lists that need more than edits and * deletions. The index's feed adds no dump through it, since it merges * `WSContext`'s `recentDumps` itself. */ export interface DumpListSyncOptions { /** * The list holds the viewer's own private dumps, as their profile, the * index and the search do, so that one they make private stays. Only its * poster hears of a private dump's edits. */ isOwner?: boolean; /** * Which dumps the list may hold. A dump that left it, as by turning * private, comes back where it was when it returns and this accepts it. * Without it, the list never gains a dump. */ belongs?: (dump: Dump) => boolean; /** * Also adds a dump `belongs` accepts that the list never held, where the * newest-first order puts it among the dumps loaded: a new one first. One * older than all of them is left to the next pages. For the lists sorted by * `createdAt`, as a profile's dumps. */ addNew?: boolean; } /** * Returns `list` with `dump` before the first dump posted earlier, or as is * when none was: the dump belongs to a later page. */ function insertByDate(list: Dump[], dump: Dump): Dump[] { if (list.length === 0) return [dump]; const idx = list.findIndex((d) => d.createdAt.getTime() < dump.createdAt.getTime() ); return idx === -1 ? list : insertAt(list, dump, idx); } /** * Applies the socket's dump, comment and user events to `items`, the loaded * list, which `setDumps` updates. A deleted dump is removed, an edited one is * replaced in place, one gone private leaves unless `isOwner`, and a new or * returning one is added as `belongs` and `addNew` say. A new or deleted * comment moves its dump's `commentCount`, and a renamed user's dumps take * their new name. */ export function useDumpListSync( items: Dump[], setDumps: (fn: (prev: Dump[]) => Dump[]) => void, options?: DumpListSyncOptions, ): void { const { deletedDumpIds } = useWS(); const takePosition = usePositionMemory(items); // A ref, so that the effect runs on new deletions only, not on new // callbacks. const setDumpsRef = useRef(setDumps); useLayoutEffect(() => { setDumpsRef.current = setDumps; }); useEffect(() => { if (deletedDumpIds.size === 0) return; setDumpsRef.current((prev) => prev.filter((d) => !deletedDumpIds.has(d.id)) ); }, [deletedDumpIds]); useWSEvent("dump", ({ dump }) => { const { isOwner, belongs, addNew } = options ?? {}; const accepted = !!belongs?.(dump); // Read here, not in the updater, which React may call twice. const oldIdx = accepted ? takePosition(dump.id) : undefined; setDumps((prev) => { const idx = prev.findIndex((d) => d.id === dump.id); if (idx !== -1) { if (dump.isPrivate && !isOwner) { return prev.filter((d) => d.id !== dump.id); } const next = [...prev]; next[idx] = dump; return next; } if (!accepted) return prev; if (oldIdx !== undefined) return insertAt(prev, dump, oldIdx); return addNew ? insertByDate(prev, dump) : prev; }); }); // The server sends a private dump's comment events to its owner alone, who // alone lists it. useWSEvent("comment", ({ type, dumpId }) => { if (type !== "created" && type !== "deleted") return; const delta = type === "created" ? 1 : -1; setDumps((prev) => { const idx = prev.findIndex((d) => d.id === dumpId); if (idx === -1) return prev; const next = [...prev]; next[idx] = { ...next[idx], commentCount: Math.max(0, next[idx].commentCount + delta), }; return next; }); }); useWSEvent("user", ({ user }) => { setDumps((prev) => prev.some((d) => d.userId === user.id && d.ownerUsername !== user.username ) ? prev.map((d) => d.userId === user.id ? { ...d, ownerUsername: user.username } : d ) : prev ); }); }