/** * Keeps a page's list of playlists in step with the socket's `playlist_*` * messages, and their owners' names. The user's profile, the playlists page * and the search call it. * * @module */ import { useEffect, useLayoutEffect, useRef } from "react"; import type { Playlist } from "../../../shared/playlist.ts"; import { useWS, useWSEvent } from "../ws/useWS.ts"; import { insertAt, usePositionMemory } from "../feed/usePositionMemory.ts"; import { withByDate } from "../../utils/feed/pagedList.ts"; /** Passed to {@link usePlaylistListSync} by each page, to fit its list. */ export interface PlaylistListSyncOptions { /** Keeps private playlists in the list, for their owner. */ isOwner?: boolean; /** * Adds only this user's playlists, when one is created or comes back into * view. Without it, anyone's are added. */ ownerId?: string; /** * Adds no new playlist, for a list of followed playlists, since the socket * doesn't say what the user follows, or of search results. Playlists * already listed still change, go, and come back where they were. */ noNewEntries?: boolean; } /** * Applies the socket's playlist events to `items`, the loaded list, newest * first, which `setPlaylists` updates. A created playlist goes where its date * puts it, first when new, an updated one changes in place or leaves when it * turns private (unless `isOwner`), one that left comes back where it was * when it returns, a deleted one leaves, and a playlist's `dumpCount` follows * its dumps. A renamed user's playlists take their new name. */ export function usePlaylistListSync( items: Playlist[], setPlaylists: (fn: (prev: Playlist[]) => Playlist[]) => void, options?: PlaylistListSyncOptions, ): void { const { deletedPlaylistIds } = useWS(); const takePosition = usePositionMemory(items); // A ref, so that the effect runs on new deletions only, not on new // callbacks. const setPlaylistsRef = useRef(setPlaylists); useLayoutEffect(() => { setPlaylistsRef.current = setPlaylists; }); useWSEvent("playlist", (ev) => { const { isOwner, ownerId, noNewEntries } = options ?? {}; const shown = ev.playlist && (ev.playlist.isPublic || isOwner) && (!ownerId || ev.playlist.userId === ownerId); // Read here, not in the updater, which React may call twice. const oldIdx = shown ? takePosition(ev.playlistId) : undefined; setPlaylists((prev) => { if ((ev.type === "created" || ev.type === "updated") && ev.playlist) { const playlist = ev.playlist; const idx = prev.findIndex((p) => p.id === playlist.id); if (idx !== -1) { if (!shown) return prev.filter((p) => p.id !== playlist.id); const next = [...prev]; next[idx] = playlist; return next; } if (!shown) return prev; if (oldIdx !== undefined) return insertAt(prev, playlist, oldIdx); // An update never adds a playlist the list doesn't hold. if (ev.type === "updated" || noNewEntries) return prev; return withByDate(prev, playlist); } if (ev.type === "deleted") { return prev.filter((p) => p.id !== ev.playlistId); } if (ev.type === "dumps_updated" && ev.dumpIds) { const idx = prev.findIndex((p) => p.id === ev.playlistId); if (idx === -1) return prev; const next = [...prev]; next[idx] = { ...next[idx], dumpCount: ev.dumpCount ?? ev.dumpIds.length, }; return next; } return prev; }); }); useWSEvent("user", ({ user }) => { setPlaylists((prev) => prev.some((p) => p.userId === user.id && p.ownerUsername !== user.username ) ? prev.map((p) => p.userId === user.id ? { ...p, ownerUsername: user.username } : p ) : prev ); }); useEffect(() => { if (deletedPlaylistIds.size === 0) return; setPlaylistsRef.current((prev) => prev.filter((p) => !deletedPlaylistIds.has(p.id)) ); }, [deletedPlaylistIds]); }