diff --git a/js/historyView.js b/js/historyView.js index 0f9b12bc..8110b213 100644 --- a/js/historyView.js +++ b/js/historyView.js @@ -739,6 +739,12 @@ export function createWatchHistoryRenderer(config = {}) { }, } = config; + const syncEnabled = watchHistoryService.isEnabled?.() === true; + const localSupported = + typeof watchHistoryService.supportsLocalHistory === "function" + ? watchHistoryService.supportsLocalHistory() !== false + : true; + const state = { initialized: false, actor: null, @@ -763,7 +769,9 @@ export function createWatchHistoryRenderer(config = {}) { ? watchHistoryService.shouldStoreMetadata() !== false : true, sessionFallbackActive: false, - featureEnabled: watchHistoryService.isEnabled?.() === true, + syncEnabled, + localSupported, + featureEnabled: syncEnabled || localSupported, }; let elements = { @@ -895,18 +903,29 @@ export function createWatchHistoryRenderer(config = {}) { } function updateFeatureBanner() { - const enabled = watchHistoryService.isEnabled?.() === true; - state.featureEnabled = enabled; + const syncEnabled = watchHistoryService.isEnabled?.() === true; + const localSupported = + typeof watchHistoryService.supportsLocalHistory === "function" + ? watchHistoryService.supportsLocalHistory() !== false + : true; + state.syncEnabled = syncEnabled; + state.localSupported = localSupported; + state.featureEnabled = syncEnabled || localSupported; if (!(elements.featureBanner instanceof HTMLElement)) { return; } - if (enabled) { + if (syncEnabled) { elements.featureBanner.textContent = ""; setHidden(elements.featureBanner, true); return; } - elements.featureBanner.textContent = - "Watch history sync is disabled on this server. Local history only."; + if (localSupported) { + elements.featureBanner.textContent = + "Watch history sync is disabled on this server. Local history only."; + } else { + elements.featureBanner.textContent = + "Watch history is disabled on this server."; + } setHidden(elements.featureBanner, false); } diff --git a/js/watchHistoryService.js b/js/watchHistoryService.js index ff5ca635..13f5e0a5 100644 --- a/js/watchHistoryService.js +++ b/js/watchHistoryService.js @@ -454,9 +454,6 @@ function getOrCreateQueue(actorKey) { } function restoreQueueState() { - if (!isFeatureEnabled()) { - return; - } if (state.restored) { return; } @@ -567,9 +564,6 @@ function restoreQueueState() { } function ensureQueue(actorKey) { - if (!isFeatureEnabled()) { - return null; - } if (!actorKey) { return null; } @@ -580,9 +574,6 @@ function ensureQueue(actorKey) { } function persistQueueState() { - if (!isFeatureEnabled()) { - return; - } const storage = getSessionStorage(); if (!storage) { return; @@ -647,9 +638,6 @@ function resolveActorKey(actorInput) { } function collectQueueItems(actorKey) { - if (!isFeatureEnabled()) { - return []; - } const queue = state.queues.get(actorKey); if (!queue) { return []; @@ -675,9 +663,6 @@ function collectQueueItems(actorKey) { } function notifyQueueChange(actorKey) { - if (!isFeatureEnabled()) { - return; - } emit("queue-changed", { actor: actorKey, items: collectQueueItems(actorKey), @@ -685,9 +670,6 @@ function notifyQueueChange(actorKey) { } function clearQueue(actorKey) { - if (!isFeatureEnabled()) { - return; - } const queue = state.queues.get(actorKey); if (!queue) { return; @@ -834,17 +816,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) { } ); - if (!isFeatureEnabled()) { - console.info( - "[watchHistoryService] Watch history feature disabled; skipping queue update.", - { - pointer: pointerInput, - createdAt, - } - ); - return viewResult; - } - const pointer = normalizePointerInput(pointerInput); if (!pointer) { console.warn( @@ -981,6 +952,17 @@ async function publishView(pointerInput, createdAt, metadata = {}) { } ); + if (!isFeatureEnabled()) { + console.info( + "[watchHistoryService] Watch history sync disabled; retaining pointer in local session queue only.", + { + actor: actorKey, + pointerKey: key, + watchedAt: normalizedPointer.watchedAt, + } + ); + } + return viewResult; } @@ -1104,18 +1086,26 @@ async function snapshot(items, options = {}) { } async function loadLatest(actorInput) { - if (!isFeatureEnabled()) { - console.info( - "[watchHistoryService] loadLatest skipped because watch history feature is disabled.", - { actor: resolveActorKey(actorInput) || null } - ); - return []; - } const actorKey = resolveActorKey(actorInput); if (!actorKey) { return []; } + if (!isFeatureEnabled()) { + if (!state.restored) { + restoreQueueState(); + } + const items = collectQueueItems(actorKey); + console.info( + "[watchHistoryService] loadLatest returning session watch history queue.", + { + actor: actorKey, + itemCount: items.length, + } + ); + return items; + } + const now = Date.now(); const cacheEntry = state.fingerprintCache.get(actorKey); console.info( @@ -1217,9 +1207,6 @@ async function getFingerprint(actorInput) { } function resetProgress(actorInput) { - if (!isFeatureEnabled()) { - return; - } const actorKey = normalizeActorKey(actorInput); if (actorKey) { state.queues.delete(actorKey); @@ -1237,9 +1224,6 @@ function resetProgress(actorInput) { } function getQueuedPointers(actorInput) { - if (!isFeatureEnabled()) { - return []; - } const actorKey = normalizeActorKey(actorInput); if (!actorKey) { return []; @@ -1251,9 +1235,6 @@ function getQueuedPointers(actorInput) { } function getAllQueues() { - if (!isFeatureEnabled()) { - return {}; - } if (!state.restored) { restoreQueueState(); } @@ -1280,6 +1261,7 @@ function getSettings() { const watchHistoryService = { isEnabled: isFeatureEnabled, + supportsLocalHistory: () => true, publishView, snapshot, loadLatest, diff --git a/tests/watch-history.test.mjs b/tests/watch-history.test.mjs index 8558c243..8f665191 100644 --- a/tests/watch-history.test.mjs +++ b/tests/watch-history.test.mjs @@ -980,12 +980,78 @@ async function testWatchHistoryServiceIntegration() { } } +async function testWatchHistoryLocalFallbackWhenDisabled() { + console.log("Running watch history local fallback test..."); + + const actor = "local-fallback-actor"; + const originalRecordView = nostrClient.recordVideoView; + const originalPub = nostrClient.pubkey; + + try { + setWatchHistoryV2Enabled(false); + localStorage.clear(); + watchHistoryService.resetProgress(); + nostrClient.pubkey = ""; + nostrClient.recordVideoView = async (_pointer, options = {}) => ({ + ok: true, + event: { + id: "local-view", + pubkey: actor, + created_at: options.created_at || 1_700_500_000, + }, + }); + + const supported = + typeof watchHistoryService.supportsLocalHistory === "function" + ? watchHistoryService.supportsLocalHistory() + : true; + assert.equal( + supported, + true, + "service should report local history support while sync disabled", + ); + + const createdAt = 1_700_500_000; + await watchHistoryService.publishView( + { type: "e", value: "local-pointer" }, + createdAt, + { actor }, + ); + + const queued = watchHistoryService.getQueuedPointers(actor); + assert.equal( + queued.length, + 1, + "local queue should retain pointer when sync is disabled", + ); + + const latest = await watchHistoryService.loadLatest(actor); + assert.equal( + latest.length, + 1, + "loadLatest should surface session queue entries when sync disabled", + ); + assert.equal(latest[0]?.value, "local-pointer"); + assert.equal(latest[0]?.type, "e"); + assert( + Number.isFinite(latest[0]?.watchedAt) && latest[0].watchedAt > 0, + "local fallback entries should carry watchedAt timestamps", + ); + } finally { + setWatchHistoryV2Enabled(true); + watchHistoryService.resetProgress(); + nostrClient.recordVideoView = originalRecordView; + nostrClient.pubkey = originalPub; + } +} + await testPublishSnapshotCanonicalizationAndChunking(); await testPublishSnapshotUsesExtensionCrypto(); await testPublishSnapshotFailureRetry(); await testWatchHistoryPartialRelayRetry(); await testResolveWatchHistoryBatchingWindow(); await testWatchHistoryServiceIntegration(); +await testWatchHistoryLocalFallbackWhenDisabled(); console.log("watch-history.test.mjs completed successfully");