mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Gate watch history v2 behind runtime flag
This commit is contained in:
1 parent
b4f7ed1e54
commit
6cfaf38ce1
5 files changed
+148
-5
No files matched your search
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user