Gate watch history v2 behind runtime flag

This commit is contained in:
thePR0M3TH3AN committed 2025-10-02 15:19:55 -04:00
1 parent b4f7ed1e54
commit 6cfaf38ce1
5 files changed
+148 -5

No files matched your search

+11
View File
@@ -76,6 +76,17 @@ export const WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS = Object.freeze([
"watch-history:v2:index",
]);
/**
* Whether to enable the V2 encrypted watch-history service.
*
* The runtime flag that controls this feature defaults to `false` so that new
* deployments stick with the analytics-only view flow until operators opt in.
* To enable V2, set `window.__BITVID_RUNTIME_FLAGS__.FEATURE_WATCH_HISTORY_V2 = true`
* in a bootstrap script (or override the value before the app loads). When the
* flag stays off, BitVid still emits legacy view events and will honor
* existing watch-history reads per plan §12, but the sync UI will surface a
* disabled banner instead of querying relays.
*/
/**
* Maximum number of watch-history entries to retain per user.
*
+10 -1
View File
@@ -7585,7 +7585,16 @@ class bitvidApp {
(async () => {
let viewResult;
try {
viewResult = await watchHistoryService.publishView(thresholdPointer);
const watchHistoryEnabled =
watchHistoryService?.isEnabled?.() === true &&
typeof watchHistoryService.publishView === "function";
if (watchHistoryEnabled) {
viewResult = await watchHistoryService.publishView(thresholdPointer);
} else if (typeof nostrClient?.recordVideoView === "function") {
viewResult = await nostrClient.recordVideoView(thresholdPointer);
} else {
viewResult = { ok: false, error: "view-logging-unavailable" };
}
} catch (error) {
if (isDevMode) {
console.warn(
+6 -2
View File
@@ -1,6 +1,6 @@
const DEFAULT_FLAGS = Object.freeze({
URL_FIRST_ENABLED: true, // try URL before magnet in the player
ACCEPT_LEGACY_V1: true, // accept v1 magnet-only notes
URL_FIRST_ENABLED: true, // try URL before magnet in the player
ACCEPT_LEGACY_V1: true, // accept v1 magnet-only notes
VIEW_FILTER_INCLUDE_LEGACY_VIDEO: false,
FEATURE_WATCH_HISTORY_V2: false,
WSS_TRACKERS: Object.freeze([
@@ -195,6 +195,10 @@ export function setViewFilterIncludeLegacyVideo(next) {
return VIEW_FILTER_INCLUDE_LEGACY_VIDEO;
}
export function getWatchHistoryV2Enabled() {
return FEATURE_WATCH_HISTORY_V2 === true;
}
export function setWssTrackers(next) {
runtimeFlags.WSS_TRACKERS = next;
return WSS_TRACKERS;
+75 -1
View File
@@ -12,6 +12,9 @@ import { WATCH_HISTORY_BATCH_RESOLVE } from "./config.js";
export const WATCH_HISTORY_EMPTY_COPY =
"Your watch history is empty. Watch some videos to populate this list.";
export const WATCH_HISTORY_DISABLED_COPY =
"Encrypted watch history sync is disabled by this server. Local history stays on this device only.";
const WATCH_HISTORY_METADATA_PREF_KEY =
"bitvid:watch-history:metadata-preference";
const WATCH_HISTORY_PRIVACY_DISMISSED_KEY =
@@ -716,6 +719,7 @@ export function createWatchHistoryRenderer(config = {}) {
metadataLabelSelector = "#profileHistoryMetadataLabel",
metadataDescriptionSelector = "#profileHistoryMetadataDescription",
emptyCopy = WATCH_HISTORY_EMPTY_COPY,
disabledCopy = WATCH_HISTORY_DISABLED_COPY,
batchSize = WATCH_HISTORY_BATCH_SIZE,
remove = (payload) => {
const app = getAppInstance();
@@ -750,6 +754,7 @@ export function createWatchHistoryRenderer(config = {}) {
? watchHistoryService.shouldStoreMetadata() !== false
: true,
sessionFallbackActive: false,
featureEnabled: watchHistoryService.isEnabled?.() === true,
};
let elements = {
@@ -881,10 +886,12 @@ export function createWatchHistoryRenderer(config = {}) {
}
function updateFeatureBanner() {
const enabled = watchHistoryService.isEnabled?.() === true;
state.featureEnabled = enabled;
if (!(elements.featureBanner instanceof HTMLElement)) {
return;
}
if (watchHistoryService.isEnabled?.() === true) {
if (enabled) {
elements.featureBanner.textContent = "";
setHidden(elements.featureBanner, true);
return;
@@ -1018,6 +1025,31 @@ export function createWatchHistoryRenderer(config = {}) {
}
}
function showFeatureDisabledState() {
hideErrorBanner();
if (elements.loading) {
setHidden(elements.loading, true);
}
if (elements.status) {
setHidden(elements.status, true);
}
detachObserver();
if (elements.grid) {
elements.grid.innerHTML = "";
setHidden(elements.grid, true);
}
if (elements.empty) {
setTextContent(elements.empty, disabledCopy || emptyCopy);
setHidden(elements.empty, false);
}
if (elements.loadMore) {
setHidden(elements.loadMore, true);
}
if (elements.sentinel) {
setHidden(elements.sentinel, true);
}
}
function ensureObserver() {
if (!elements.sentinel || !WATCH_HISTORY_BATCH_RESOLVE) {
return;
@@ -1553,6 +1585,17 @@ export function createWatchHistoryRenderer(config = {}) {
if (state.isLoading) {
return false;
}
if (!state.featureEnabled) {
state.actor = null;
updateSessionFallbackWarning();
state.items = [];
state.fingerprint = "";
state.cursor = 0;
state.hasMore = false;
state.lastError = null;
showFeatureDisabledState();
return false;
}
setLoadingState(true);
const actor =
typeof actorOverride === "string" && actorOverride.trim()
@@ -1642,6 +1685,15 @@ export function createWatchHistoryRenderer(config = {}) {
) === "true";
updatePrivacyBanner();
state.initialized = true;
if (!state.featureEnabled) {
state.items = [];
state.fingerprint = "";
state.cursor = 0;
state.hasMore = false;
state.lastError = null;
showFeatureDisabledState();
return;
}
await this.refresh({ ...options, force: true });
},
async ensureInitialLoad(options = {}) {
@@ -1651,6 +1703,15 @@ export function createWatchHistoryRenderer(config = {}) {
}
refreshElements();
updateFeatureBanner();
if (!state.featureEnabled) {
state.items = [];
state.fingerprint = "";
state.cursor = 0;
state.hasMore = false;
state.lastError = null;
showFeatureDisabledState();
return;
}
bindMetadataToggle();
state.metadataStorageEnabled =
typeof watchHistoryService.shouldStoreMetadata === "function"
@@ -1659,6 +1720,16 @@ export function createWatchHistoryRenderer(config = {}) {
updateMetadataToggle();
},
async refresh(options = {}) {
updateFeatureBanner();
if (!state.featureEnabled) {
state.items = [];
state.fingerprint = "";
state.cursor = 0;
state.hasMore = false;
state.lastError = null;
showFeatureDisabledState();
return;
}
const { actor, force = true } = options;
const changed = await loadHistory({
force,
@@ -1671,6 +1742,9 @@ export function createWatchHistoryRenderer(config = {}) {
await renderInitial();
},
async loadMore() {
if (!state.featureEnabled) {
return [];
}
if (!state.items.length) {
return [];
}
+46 -1
View File
@@ -10,7 +10,10 @@ import {
WATCH_HISTORY_CACHE_TTL_MS,
WATCH_HISTORY_MAX_ITEMS,
} from "./config.js";
import { FEATURE_WATCH_HISTORY_V2 } from "./constants.js";
import {
FEATURE_WATCH_HISTORY_V2,
getWatchHistoryV2Enabled,
} from "./constants.js";
import { isDevMode } from "./config.js";
const SESSION_STORAGE_KEY = "bitvid:watch-history:session:v1";
@@ -34,6 +37,18 @@ const state = {
};
function isFeatureEnabled() {
try {
if (typeof getWatchHistoryV2Enabled === "function") {
return getWatchHistoryV2Enabled();
}
} catch (error) {
if (isDevMode) {
console.warn(
"[watchHistoryService] Failed to read FEATURE_WATCH_HISTORY_V2 flag:",
error,
);
}
}
return FEATURE_WATCH_HISTORY_V2 === true;
}
@@ -439,6 +454,9 @@ function getOrCreateQueue(actorKey) {
}
function restoreQueueState() {
if (!isFeatureEnabled()) {
return;
}
if (state.restored) {
return;
}
@@ -549,6 +567,9 @@ function restoreQueueState() {
}
function ensureQueue(actorKey) {
if (!isFeatureEnabled()) {
return null;
}
if (!actorKey) {
return null;
}
@@ -559,6 +580,9 @@ function ensureQueue(actorKey) {
}
function persistQueueState() {
if (!isFeatureEnabled()) {
return;
}
const storage = getSessionStorage();
if (!storage) {
return;
@@ -623,6 +647,9 @@ function resolveActorKey(actorInput) {
}
function collectQueueItems(actorKey) {
if (!isFeatureEnabled()) {
return [];
}
const queue = state.queues.get(actorKey);
if (!queue) {
return [];
@@ -648,6 +675,9 @@ function collectQueueItems(actorKey) {
}
function notifyQueueChange(actorKey) {
if (!isFeatureEnabled()) {
return;
}
emit("queue-changed", {
actor: actorKey,
items: collectQueueItems(actorKey),
@@ -655,6 +685,9 @@ function notifyQueueChange(actorKey) {
}
function clearQueue(actorKey) {
if (!isFeatureEnabled()) {
return;
}
const queue = state.queues.get(actorKey);
if (!queue) {
return;
@@ -687,6 +720,9 @@ function resolveWatchedAt(...candidates) {
}
function scheduleRepublishForQueue(actorKey) {
if (!isFeatureEnabled()) {
return;
}
const queue = state.queues.get(actorKey);
if (!queue || !queue.pendingSnapshotId || queue.republishScheduled) {
return;
@@ -1045,6 +1081,9 @@ async function getFingerprint(actorInput) {
}
function resetProgress(actorInput) {
if (!isFeatureEnabled()) {
return;
}
const actorKey = normalizeActorKey(actorInput);
if (actorKey) {
state.queues.delete(actorKey);
@@ -1062,6 +1101,9 @@ function resetProgress(actorInput) {
}
function getQueuedPointers(actorInput) {
if (!isFeatureEnabled()) {
return [];
}
const actorKey = normalizeActorKey(actorInput);
if (!actorKey) {
return [];
@@ -1073,6 +1115,9 @@ function getQueuedPointers(actorInput) {
}
function getAllQueues() {
if (!isFeatureEnabled()) {
return {};
}
if (!state.restored) {
restoreQueueState();
}