diff --git a/js/channelPlaylists.js b/js/channelPlaylists.js index 971670fc..2be0dcb6 100644 --- a/js/channelPlaylists.js +++ b/js/channelPlaylists.js @@ -4,14 +4,21 @@ // renders a row of cards linking to the playlist view. Kept out of // channelProfile.js (that file is at its size cap) and wired in via a one-line // hook, mirroring channelZapTotal.js. Gated by FEATURE_PLAYLISTS — with the flag -// off (default) it renders nothing and does no fetch, so it's invisible until -// the whole playlist UI ships. +// off it renders nothing and does no fetch. +// +// Also listens for `bitvid:playlists-changed` (dispatched by the "Add to +// playlist" picker) so the section refreshes right after you add/create a +// playlist, instead of needing a manual page reload. import { fetchCreatorPlaylists } from "./playlists/playlistFacade.js"; import { FEATURE_PLAYLISTS } from "./constants.js"; import { devLogger } from "./utils/logger.js"; let currentToken = 0; +let currentHex = ""; +let currentClient; +let currentDoc = null; +let listenerAttached = false; function clearChildren(el) { while (el.firstChild) { @@ -63,8 +70,71 @@ function renderPlaylistCard(doc, playlist) { return card; } +// Fetch + render the currently-wired channel's playlists. Safe to call any time +// (event refresh, initial wire); a nav token drops a stale fetch. +async function refresh() { + const doc = currentDoc || globalThis.document; + const section = + doc && typeof doc.getElementById === "function" + ? doc.getElementById("channelPlaylistsSection") + : null; + const grid = doc?.getElementById?.("channelPlaylistsGrid") || null; + if (!section || !grid) { + return; + } + + if (!FEATURE_PLAYLISTS || !currentHex) { + section.classList.add("hidden"); + return; + } + + const token = currentToken; + let playlists = []; + try { + playlists = await fetchCreatorPlaylists( + currentHex, + currentClient ? { client: currentClient } : {}, + ); + } catch (error) { + devLogger.warn("[channelPlaylists] Failed to load playlists:", error); + playlists = []; + } + + // A newer navigation superseded this fetch. + if (token !== currentToken) { + return; + } + + if (!playlists.length) { + section.classList.add("hidden"); + return; + } + + clearChildren(grid); + const frag = doc.createDocumentFragment(); + for (const playlist of playlists) { + frag.appendChild(renderPlaylistCard(doc, playlist)); + } + grid.appendChild(frag); + section.classList.remove("hidden"); +} + +function onPlaylistsChanged(event) { + const changed = (event?.detail?.pubkey || "").trim().toLowerCase(); + if (!currentHex || changed !== currentHex) { + return; + } + // Refresh now, then again shortly after — relays need a beat to serve the + // event we just published back, so the first fetch can miss it. + refresh(); + setTimeout(() => { + refresh(); + }, 2000); +} + export function teardownChannelPlaylists() { currentToken += 1; + currentHex = ""; } /** @@ -78,51 +148,16 @@ export async function wireChannelPlaylists( { document: doc = globalThis.document, client } = {}, ) { teardownChannelPlaylists(); - const token = currentToken; + currentHex = typeof pubkey === "string" ? pubkey.trim().toLowerCase() : ""; + currentClient = client; + currentDoc = doc || globalThis.document; - const section = - doc && typeof doc.getElementById === "function" - ? doc.getElementById("channelPlaylistsSection") - : null; - const grid = doc?.getElementById?.("channelPlaylistsGrid") || null; - if (!section || !grid) { - return; + if (!listenerAttached && currentDoc?.addEventListener) { + currentDoc.addEventListener("bitvid:playlists-changed", onPlaylistsChanged); + listenerAttached = true; } - const hide = () => section.classList.add("hidden"); - const show = () => section.classList.remove("hidden"); - - const hex = typeof pubkey === "string" ? pubkey.trim().toLowerCase() : ""; - if (!FEATURE_PLAYLISTS || !hex) { - hide(); - return; - } - - let playlists = []; - try { - playlists = await fetchCreatorPlaylists(hex, client ? { client } : {}); - } catch (error) { - devLogger.warn("[channelPlaylists] Failed to load playlists:", error); - playlists = []; - } - - // A newer navigation (channel switch / teardown) superseded this fetch. - if (token !== currentToken) { - return; - } - - if (!playlists.length) { - hide(); - return; - } - - clearChildren(grid); - const frag = doc.createDocumentFragment(); - for (const playlist of playlists) { - frag.appendChild(renderPlaylistCard(doc, playlist)); - } - grid.appendChild(frag); - show(); + await refresh(); } export default wireChannelPlaylists; diff --git a/js/ui/playlistPicker.js b/js/ui/playlistPicker.js index 8de5c4c1..73588fc0 100644 --- a/js/ui/playlistPicker.js +++ b/js/ui/playlistPicker.js @@ -169,13 +169,13 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) { } } - async function onSave() { - saveBtn.disabled = true; - cancelBtn.disabled = true; - saveBtn.textContent = "Saving…"; - + function onSave() { + // Compute the diff synchronously, then close the dialog IMMEDIATELY and + // publish in the background — relay publishes take a beat, and blocking the + // dialog on them made it feel stuck / "not close". Success/failure comes + // back as a toast; a `bitvid:playlists-changed` event lets the channel + // section refresh without a manual page reload. const jobs = []; - // Diff existing playlists: only publish those whose membership changed. for (const { playlist, input, wasIn } of checkboxes) { if (input.checked === wasIn) { continue; @@ -185,7 +185,6 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) { : removeVideoFromPlaylist(playlist.items, coordinate); jobs.push({ ...playlist, items }); } - // New playlist, if named. const newName = newInput.value.trim(); if (newName) { jobs.push({ @@ -196,41 +195,55 @@ export async function openPlaylistPicker({ videoCoordinate, notify } = {}) { }); } + cleanup(); if (!jobs.length) { - cleanup(); return; } - let ok = 0; - let failed = 0; - for (const job of jobs) { - try { - await publishPlaylist({ pubkey, ...job }); - ok += 1; - } catch (error) { - userLogger.warn("[playlistPicker] Failed to publish playlist:", error); - failed += 1; + (async () => { + let ok = 0; + let failed = 0; + for (const job of jobs) { + try { + await publishPlaylist({ pubkey, ...job }); + ok += 1; + } catch (error) { + userLogger.warn( + "[playlistPicker] Failed to publish playlist:", + error, + ); + failed += 1; + } } - } - cleanup(); - if (ok && !failed) { - say( - "success", - ok === 1 ? "Playlist updated." : `Updated ${ok} playlists.`, - ); - } else if (ok && failed) { - say("error", `Updated ${ok}; ${failed} could not be saved.`); - } else { - say("error", "Couldn't save your playlist changes."); - } + if (ok && !failed) { + say( + "success", + ok === 1 ? "Playlist updated." : `Updated ${ok} playlists.`, + ); + } else if (ok && failed) { + say("error", `Updated ${ok}; ${failed} could not be saved.`); + } else { + say("error", "Couldn't save your playlist changes."); + } + + if (ok) { + try { + document.dispatchEvent( + new CustomEvent("bitvid:playlists-changed", { + detail: { pubkey }, + }), + ); + } catch (error) { + // best effort + } + } + })(); } cancelBtn.addEventListener("click", cleanup); backdrop.addEventListener("click", cleanup); - saveBtn.addEventListener("click", () => { - onSave().catch(() => cleanup()); - }); + saveBtn.addEventListener("click", onSave); document.addEventListener("keydown", onKeydown, true); document.body.appendChild(overlay);