diff --git a/config/instance-config.js b/config/instance-config.js index a439923c..695df656 100644 --- a/config/instance-config.js +++ b/config/instance-config.js @@ -63,14 +63,14 @@ export const WATCH_HISTORY_KIND = 30078; * BitVid deployments that share relays, but keep it stable once clients begin * syncing history. */ -export const WATCH_HISTORY_LIST_IDENTIFIER = "watch-history:v2:index"; +export const WATCH_HISTORY_LIST_IDENTIFIER = "watch-history"; /** * Legacy identifiers that BitVid clients should continue honoring when * fetching historical watch-history snapshots. */ export const WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS = Object.freeze([ - "watch-history", + "watch-history:v2:index", ]); /** diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index 158359b2..716726d0 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -64,10 +64,12 @@ builders inherit the same debugging knobs. ### Watch history identifiers Watch-history snapshots now publish a dedicated index event with -`#d ${WATCH_HISTORY_LIST_IDENTIFIER}` (defaults to `watch-history:v2:index`). The +`#d ${WATCH_HISTORY_LIST_IDENTIFIER}` (defaults to `watch-history`). The index advertises the active snapshot id, total chunk count, and a pointer to each chunk via `a` tags so clients can fetch the encrypted payloads without -guessing identifiers. +guessing identifiers. Deployments that previously used +`watch-history:v2:index` continue to resolve thanks to the legacy identifier +list. Every chunk event uses a unique `d` tag such as `watch-history:v2//`. Legacy snapshots created before the diff --git a/js/app.js b/js/app.js index 918b03b2..2f33999e 100644 --- a/js/app.js +++ b/js/app.js @@ -4283,6 +4283,29 @@ class bitvidApp { } button.dataset.navBound = "true"; button.addEventListener("click", () => { + if (name === "history") { + let actorCandidate; + if (typeof this.pubkey === "string" && this.pubkey.trim()) { + actorCandidate = this.pubkey.trim(); + } else if ( + typeof window?.app?.pubkey === "string" && + window.app.pubkey.trim() + ) { + actorCandidate = window.app.pubkey.trim(); + } + + void nostrClient + .fetchWatchHistory(actorCandidate, { forceRefresh: true }) + .catch((error) => { + if (isDevMode) { + console.warn( + "[profileModal] Failed to prefetch watch history on navigation:", + error + ); + } + }); + } + this.selectProfilePane(name); }); }); diff --git a/js/index.js b/js/index.js index 72fd1f8e..dfb19a99 100644 --- a/js/index.js +++ b/js/index.js @@ -2,6 +2,45 @@ import "./bufferPolyfill.js"; import { trackPageView } from "./analytics.js"; +import { nostrClient } from "./nostr.js"; + +const HISTORY_PREFETCH_DEBOUNCE_MS = 250; + +function prefetchWatchHistory(options = {}) { + if (typeof nostrClient?.fetchWatchHistory !== "function") { + return; + } + + const force = options.force === true; + const now = Date.now(); + + if (typeof window !== "undefined") { + const last = + typeof window.__bitvidLastHistoryPrefetch === "number" + ? window.__bitvidLastHistoryPrefetch + : 0; + + if (!force && now - last < HISTORY_PREFETCH_DEBOUNCE_MS) { + return; + } + + window.__bitvidLastHistoryPrefetch = now; + } + + let actor; + if (typeof window?.app?.pubkey === "string" && window.app.pubkey.trim()) { + actor = window.app.pubkey.trim(); + } + + void nostrClient + .fetchWatchHistory(actor, { forceRefresh: true }) + .catch((error) => { + console.warn( + "[index] Failed to prefetch watch history during navigation:", + error + ); + }); +} const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in"; const VIDEO_THUMBNAIL_FADE_IN_ANIMATION = "video-thumbnail-fade-in"; @@ -257,6 +296,11 @@ export function setHashView(viewName) { url.searchParams.delete("v"); const newUrl = url.pathname + url.search + `#view=${viewName}`; window.history.replaceState({}, "", newUrl); + + if (typeof viewName === "string" && viewName.toLowerCase() === "history") { + prefetchWatchHistory({ force: true }); + } + handleHashChange(); } @@ -403,6 +447,9 @@ async function handleHashChange() { } const viewName = match[1]; // only the chunk before any '&' + if (typeof viewName === "string" && viewName.toLowerCase() === "history") { + prefetchWatchHistory(); + } const viewUrl = `views/${viewName}.html`; // Now dynamically load that partial, then call its init function diff --git a/js/nostr.js b/js/nostr.js index f49c37f4..05b22ec3 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -58,7 +58,28 @@ const WATCH_HISTORY_INDEX_IDENTIFIER_LOWER = ? WATCH_HISTORY_LIST_IDENTIFIER.trim().toLowerCase() : ""; -const WATCH_HISTORY_CHUNK_V2_PREFIX = "watch-history:v2"; +const WATCH_HISTORY_CHUNK_V2_PREFIX = (() => { + const rawIdentifier = + typeof WATCH_HISTORY_LIST_IDENTIFIER === "string" + ? WATCH_HISTORY_LIST_IDENTIFIER.trim() + : ""; + + if (rawIdentifier) { + const trimmed = rawIdentifier.replace(/\/+$/, ""); + if (/[:/]index$/i.test(trimmed)) { + const base = trimmed.replace(/[:/]index$/i, "").replace(/\/+$/, ""); + return base || "watch-history:v2"; + } + + if (/:v\d+$/i.test(trimmed)) { + return trimmed; + } + + return `${trimmed}:v2`; + } + + return "watch-history:v2"; +})(); const LEGACY_WATCH_HISTORY_CHUNK_IDENTIFIER_PREFIX = (() => { const rawIdentifier = diff --git a/tests/watch-history.test.mjs b/tests/watch-history.test.mjs index 2d8a646a..2740d781 100644 --- a/tests/watch-history.test.mjs +++ b/tests/watch-history.test.mjs @@ -109,9 +109,30 @@ function createDecryptClient(actorPubkey) { } const ACTOR = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; -const WATCH_HISTORY_CHUNK_PREFIX = WATCH_HISTORY_LIST_IDENTIFIER.replace( - /[:/]index$/i, - "" +function deriveChunkPrefix(identifier) { + if (typeof identifier !== "string") { + return "watch-history:v2"; + } + + const trimmed = identifier.trim().replace(/\/+$/, ""); + if (!trimmed) { + return "watch-history:v2"; + } + + if (/[:/]index$/i.test(trimmed)) { + const base = trimmed.replace(/[:/]index$/i, "").replace(/\/+$/, ""); + return base || "watch-history:v2"; + } + + if (/:v\d+$/i.test(trimmed)) { + return trimmed; + } + + return `${trimmed}:v2`; +} + +const WATCH_HISTORY_CHUNK_PREFIX = deriveChunkPrefix( + WATCH_HISTORY_LIST_IDENTIFIER ); const indexSchema = getNostrEventSchema(NOTE_TYPES.WATCH_HISTORY_INDEX);