mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Scope watch history sync to logged users
This commit is contained in:
1 parent
b1a493f113
commit
c3ed4bc6ea
3 files changed
+241
-32
No files matched your search
+31
-8
@@ -16,7 +16,7 @@ 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.";
|
||||
"Watch history sync is unavailable. Connect a NIP-07 extension or log in to enable encrypted syncing.";
|
||||
|
||||
const WATCH_HISTORY_METADATA_PREF_KEY =
|
||||
"bitvid:watch-history:metadata-preference";
|
||||
@@ -739,11 +739,18 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
},
|
||||
} = config;
|
||||
|
||||
const syncEnabled = watchHistoryService.isEnabled?.() === true;
|
||||
const syncEnabled =
|
||||
typeof watchHistoryService.isEnabled === "function"
|
||||
? watchHistoryService.isEnabled() === true
|
||||
: false;
|
||||
const localSupported =
|
||||
typeof watchHistoryService.supportsLocalHistory === "function"
|
||||
? watchHistoryService.supportsLocalHistory() !== false
|
||||
: true;
|
||||
? watchHistoryService.supportsLocalHistory() === true
|
||||
: false;
|
||||
const localOnly =
|
||||
typeof watchHistoryService.isLocalOnly === "function"
|
||||
? watchHistoryService.isLocalOnly() === true
|
||||
: false;
|
||||
|
||||
const state = {
|
||||
initialized: false,
|
||||
@@ -771,6 +778,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
sessionFallbackActive: false,
|
||||
syncEnabled,
|
||||
localSupported,
|
||||
localOnly,
|
||||
featureEnabled: syncEnabled || localSupported,
|
||||
};
|
||||
|
||||
@@ -903,13 +911,22 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
}
|
||||
|
||||
function updateFeatureBanner() {
|
||||
const syncEnabled = watchHistoryService.isEnabled?.() === true;
|
||||
const actor = typeof state.actor === "string" ? state.actor : undefined;
|
||||
const syncEnabled =
|
||||
typeof watchHistoryService.isEnabled === "function"
|
||||
? watchHistoryService.isEnabled(actor) === true
|
||||
: false;
|
||||
const localSupported =
|
||||
typeof watchHistoryService.supportsLocalHistory === "function"
|
||||
? watchHistoryService.supportsLocalHistory() !== false
|
||||
: true;
|
||||
? watchHistoryService.supportsLocalHistory(actor) === true
|
||||
: false;
|
||||
const localOnly =
|
||||
typeof watchHistoryService.isLocalOnly === "function"
|
||||
? watchHistoryService.isLocalOnly(actor) === true
|
||||
: false;
|
||||
state.syncEnabled = syncEnabled;
|
||||
state.localSupported = localSupported;
|
||||
state.localOnly = localOnly;
|
||||
state.featureEnabled = syncEnabled || localSupported;
|
||||
if (!(elements.featureBanner instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -919,9 +936,15 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
setHidden(elements.featureBanner, true);
|
||||
return;
|
||||
}
|
||||
if (localOnly) {
|
||||
elements.featureBanner.textContent =
|
||||
"Watch history sync requires a logged-in Nostr account. This guest session is stored locally only.";
|
||||
setHidden(elements.featureBanner, false);
|
||||
return;
|
||||
}
|
||||
if (localSupported) {
|
||||
elements.featureBanner.textContent =
|
||||
"Watch history sync is disabled on this server. Local history only.";
|
||||
"Watch history sync is disabled right now. We'll keep a local copy on this device.";
|
||||
} else {
|
||||
elements.featureBanner.textContent =
|
||||
"Watch history is disabled on this server.";
|
||||
|
||||
+101
-22
@@ -36,10 +36,10 @@ const state = {
|
||||
},
|
||||
};
|
||||
|
||||
function isFeatureEnabled() {
|
||||
function resolveFlagEnabled() {
|
||||
try {
|
||||
if (typeof getWatchHistoryV2Enabled === "function") {
|
||||
return getWatchHistoryV2Enabled();
|
||||
return getWatchHistoryV2Enabled() === true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
@@ -52,6 +52,79 @@ function isFeatureEnabled() {
|
||||
return FEATURE_WATCH_HISTORY_V2 === true;
|
||||
}
|
||||
|
||||
function getLoggedInActorKey() {
|
||||
return normalizeActorKey(nostrClient?.pubkey);
|
||||
}
|
||||
|
||||
function getSessionActorKey() {
|
||||
const logged = getLoggedInActorKey();
|
||||
if (logged) {
|
||||
return "";
|
||||
}
|
||||
return normalizeActorKey(nostrClient?.sessionActor?.pubkey);
|
||||
}
|
||||
|
||||
function resolveEffectiveActorKey(actorInput) {
|
||||
const supplied =
|
||||
typeof actorInput === "string" && actorInput.trim()
|
||||
? normalizeActorKey(actorInput)
|
||||
: "";
|
||||
if (supplied) {
|
||||
return supplied;
|
||||
}
|
||||
const logged = getLoggedInActorKey();
|
||||
if (logged) {
|
||||
return logged;
|
||||
}
|
||||
const session = getSessionActorKey();
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isFeatureEnabled(actorInput) {
|
||||
const baseEnabled = resolveFlagEnabled();
|
||||
const actorKey = resolveEffectiveActorKey(actorInput);
|
||||
const logged = getLoggedInActorKey();
|
||||
const session = getSessionActorKey();
|
||||
|
||||
if (actorKey && logged && actorKey === logged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actorKey && session && actorKey === session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!actorKey) {
|
||||
if (logged) {
|
||||
return true;
|
||||
}
|
||||
if (session) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return baseEnabled;
|
||||
}
|
||||
|
||||
function isLocalOnly(actorInput) {
|
||||
const actorKey = resolveEffectiveActorKey(actorInput);
|
||||
const session = getSessionActorKey();
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
if (!actorKey) {
|
||||
return true;
|
||||
}
|
||||
return actorKey === session;
|
||||
}
|
||||
|
||||
function supportsLocalHistory(actorInput) {
|
||||
return isLocalOnly(actorInput);
|
||||
}
|
||||
|
||||
function getSessionStorage() {
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.sessionStorage) {
|
||||
@@ -557,7 +630,7 @@ function restoreQueueState() {
|
||||
queue.lastSnapshotReason = null;
|
||||
}
|
||||
|
||||
if (queue.pendingSnapshotId && isFeatureEnabled()) {
|
||||
if (queue.pendingSnapshotId && isFeatureEnabled(actorKey)) {
|
||||
scheduleRepublishForQueue(actorKey);
|
||||
}
|
||||
}
|
||||
@@ -702,7 +775,7 @@ function resolveWatchedAt(...candidates) {
|
||||
}
|
||||
|
||||
function scheduleRepublishForQueue(actorKey) {
|
||||
if (!isFeatureEnabled()) {
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
return;
|
||||
}
|
||||
const queue = state.queues.get(actorKey);
|
||||
@@ -849,6 +922,8 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
return viewResult;
|
||||
}
|
||||
|
||||
const remoteEnabled = isFeatureEnabled(actorKey);
|
||||
|
||||
const normalizedPointer = normalizePointerInput(pointer);
|
||||
if (!normalizedPointer) {
|
||||
console.warn(
|
||||
@@ -871,6 +946,7 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
pointerKey: key,
|
||||
watchedAt: normalizedPointer.watchedAt,
|
||||
sessionEvent: normalizedPointer.session === true,
|
||||
remoteSyncEnabled: remoteEnabled,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -952,9 +1028,9 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
}
|
||||
);
|
||||
|
||||
if (!isFeatureEnabled()) {
|
||||
if (!remoteEnabled) {
|
||||
console.info(
|
||||
"[watchHistoryService] Watch history sync disabled; retaining pointer in local session queue only.",
|
||||
"[watchHistoryService] Watch history sync unavailable for this actor; retaining pointer in local session queue only.",
|
||||
{
|
||||
actor: actorKey,
|
||||
pointerKey: key,
|
||||
@@ -967,22 +1043,24 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
}
|
||||
|
||||
async function snapshot(items, options = {}) {
|
||||
if (!isFeatureEnabled()) {
|
||||
console.info(
|
||||
"[watchHistoryService] Snapshot skipped because watch history feature is disabled.",
|
||||
{
|
||||
requestedItems: Array.isArray(items) ? items.length : 0,
|
||||
reason: typeof options.reason === "string" ? options.reason : "manual",
|
||||
}
|
||||
);
|
||||
return { ok: true, skipped: true, reason: "feature-disabled" };
|
||||
}
|
||||
const reason = typeof options.reason === "string" ? options.reason : "manual";
|
||||
const actorKey = resolveActorKey(options.actor);
|
||||
if (!actorKey) {
|
||||
return { ok: false, error: "missing-actor" };
|
||||
}
|
||||
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
console.info(
|
||||
"[watchHistoryService] Snapshot skipped because watch history sync is unavailable for this actor.",
|
||||
{
|
||||
actor: actorKey,
|
||||
requestedItems: Array.isArray(items) ? items.length : 0,
|
||||
reason,
|
||||
}
|
||||
);
|
||||
return { ok: true, skipped: true, reason: "feature-disabled" };
|
||||
}
|
||||
|
||||
if (state.inflightSnapshots.has(actorKey)) {
|
||||
return state.inflightSnapshots.get(actorKey);
|
||||
}
|
||||
@@ -1091,13 +1169,13 @@ async function loadLatest(actorInput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isFeatureEnabled()) {
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
if (!state.restored) {
|
||||
restoreQueueState();
|
||||
}
|
||||
const items = collectQueueItems(actorKey);
|
||||
console.info(
|
||||
"[watchHistoryService] loadLatest returning session watch history queue.",
|
||||
"[watchHistoryService] loadLatest returning local-only watch history queue.",
|
||||
{
|
||||
actor: actorKey,
|
||||
itemCount: items.length,
|
||||
@@ -1178,13 +1256,13 @@ async function loadLatest(actorInput) {
|
||||
}
|
||||
|
||||
async function getFingerprint(actorInput) {
|
||||
if (!isFeatureEnabled()) {
|
||||
return "";
|
||||
}
|
||||
const actorKey = resolveActorKey(actorInput);
|
||||
if (!actorKey) {
|
||||
return "";
|
||||
}
|
||||
if (!isFeatureEnabled(actorKey)) {
|
||||
return "";
|
||||
}
|
||||
const now = Date.now();
|
||||
const cacheEntry = state.fingerprintCache.get(actorKey);
|
||||
if (
|
||||
@@ -1261,7 +1339,8 @@ function getSettings() {
|
||||
|
||||
const watchHistoryService = {
|
||||
isEnabled: isFeatureEnabled,
|
||||
supportsLocalHistory: () => true,
|
||||
supportsLocalHistory,
|
||||
isLocalOnly,
|
||||
publishView,
|
||||
snapshot,
|
||||
loadLatest,
|
||||
|
||||
@@ -48,6 +48,9 @@ const originalWatchHistoryCache = nostrClient.watchHistoryCache;
|
||||
const originalWatchHistoryStorage = nostrClient.watchHistoryStorage;
|
||||
const originalScheduleWatchHistoryRepublish =
|
||||
nostrClient.scheduleWatchHistoryRepublish;
|
||||
const originalResolveWatchHistory = nostrClient.resolveWatchHistory;
|
||||
const originalGetWatchHistoryFingerprint =
|
||||
nostrClient.getWatchHistoryFingerprint;
|
||||
|
||||
nostrClient.watchHistoryCache = new Map();
|
||||
nostrClient.watchHistoryStorage = {
|
||||
@@ -628,7 +631,7 @@ async function testWatchHistoryPartialRelayRetry() {
|
||||
const originalRecordView = nostrClient.recordVideoView;
|
||||
|
||||
try {
|
||||
nostrClient.pubkey = "";
|
||||
nostrClient.pubkey = actor;
|
||||
nostrClient.sessionActor = { pubkey: actor, privateKey: "partial-priv" };
|
||||
nostrClient.ensureSessionActor = async () => actor;
|
||||
|
||||
@@ -986,12 +989,14 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
const actor = "local-fallback-actor";
|
||||
const originalRecordView = nostrClient.recordVideoView;
|
||||
const originalPub = nostrClient.pubkey;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
|
||||
try {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
localStorage.clear();
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = "";
|
||||
nostrClient.sessionActor = { pubkey: actor, privateKey: "local-priv" };
|
||||
nostrClient.recordVideoView = async (_pointer, options = {}) => ({
|
||||
ok: true,
|
||||
event: {
|
||||
@@ -1003,13 +1008,27 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
|
||||
const supported =
|
||||
typeof watchHistoryService.supportsLocalHistory === "function"
|
||||
? watchHistoryService.supportsLocalHistory()
|
||||
? watchHistoryService.supportsLocalHistory(actor)
|
||||
: true;
|
||||
const localOnly =
|
||||
typeof watchHistoryService.isLocalOnly === "function"
|
||||
? watchHistoryService.isLocalOnly(actor)
|
||||
: true;
|
||||
const enabled =
|
||||
typeof watchHistoryService.isEnabled === "function"
|
||||
? watchHistoryService.isEnabled(actor)
|
||||
: false;
|
||||
assert.equal(
|
||||
supported,
|
||||
true,
|
||||
"service should report local history support while sync disabled",
|
||||
);
|
||||
assert.equal(localOnly, true, "guest session should be treated as local only");
|
||||
assert.equal(
|
||||
enabled,
|
||||
false,
|
||||
"sync should be disabled for session actors when feature flag is off",
|
||||
);
|
||||
|
||||
const createdAt = 1_700_500_000;
|
||||
await watchHistoryService.publishView(
|
||||
@@ -1042,6 +1061,90 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.recordVideoView = originalRecordView;
|
||||
nostrClient.pubkey = originalPub;
|
||||
nostrClient.sessionActor = originalSession;
|
||||
}
|
||||
}
|
||||
|
||||
async function testWatchHistorySyncEnabledForLoggedInUsers() {
|
||||
console.log("Running watch history logged-in sync override test...");
|
||||
|
||||
const actor = "npub-logged-sync";
|
||||
const originalPub = nostrClient.pubkey;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
const originalResolve = nostrClient.resolveWatchHistory;
|
||||
const originalFingerprint = nostrClient.getWatchHistoryFingerprint;
|
||||
|
||||
try {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
localStorage.clear();
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.pubkey = actor;
|
||||
nostrClient.sessionActor = null;
|
||||
|
||||
let resolveCalls = 0;
|
||||
nostrClient.resolveWatchHistory = async (requestedActor, options = {}) => {
|
||||
resolveCalls += 1;
|
||||
assert.equal(
|
||||
requestedActor,
|
||||
actor,
|
||||
"resolveWatchHistory should target the logged-in actor",
|
||||
);
|
||||
assert.equal(
|
||||
options?.forceRefresh,
|
||||
true,
|
||||
"loadLatest should force a relay refresh for logged-in actors",
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "e",
|
||||
value: "remote-pointer",
|
||||
watchedAt: 1_700_600_000,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
let fingerprintCalls = 0;
|
||||
nostrClient.getWatchHistoryFingerprint = async (requestedActor, items) => {
|
||||
fingerprintCalls += 1;
|
||||
assert.equal(
|
||||
requestedActor,
|
||||
actor,
|
||||
"fingerprint lookup should use the logged-in actor",
|
||||
);
|
||||
assert(Array.isArray(items), "fingerprint helper expects item array input");
|
||||
return "fingerprint-logged";
|
||||
};
|
||||
|
||||
const enabled =
|
||||
typeof watchHistoryService.isEnabled === "function"
|
||||
? watchHistoryService.isEnabled(actor)
|
||||
: false;
|
||||
assert.equal(
|
||||
enabled,
|
||||
true,
|
||||
"sync should remain enabled for logged-in actors even when the flag is disabled",
|
||||
);
|
||||
|
||||
const items = await watchHistoryService.loadLatest(actor);
|
||||
assert.equal(
|
||||
resolveCalls > 0,
|
||||
true,
|
||||
"loadLatest should fetch from relays for logged-in actors",
|
||||
);
|
||||
assert.equal(items.length, 1, "loadLatest should return relay data for logged-in actors");
|
||||
assert.equal(items[0]?.value, "remote-pointer");
|
||||
assert.equal(
|
||||
fingerprintCalls > 0,
|
||||
true,
|
||||
"fingerprint cache should update for logged-in actors",
|
||||
);
|
||||
} finally {
|
||||
setWatchHistoryV2Enabled(true);
|
||||
watchHistoryService.resetProgress();
|
||||
nostrClient.resolveWatchHistory = originalResolve;
|
||||
nostrClient.getWatchHistoryFingerprint = originalFingerprint;
|
||||
nostrClient.pubkey = originalPub;
|
||||
nostrClient.sessionActor = originalSession;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,6 +1155,7 @@ await testWatchHistoryPartialRelayRetry();
|
||||
await testResolveWatchHistoryBatchingWindow();
|
||||
await testWatchHistoryServiceIntegration();
|
||||
await testWatchHistoryLocalFallbackWhenDisabled();
|
||||
await testWatchHistorySyncEnabledForLoggedInUsers();
|
||||
|
||||
console.log("watch-history.test.mjs completed successfully");
|
||||
|
||||
@@ -1070,6 +1174,9 @@ nostrClient.watchHistoryCache = originalWatchHistoryCache;
|
||||
nostrClient.watchHistoryStorage = originalWatchHistoryStorage;
|
||||
nostrClient.scheduleWatchHistoryRepublish =
|
||||
originalScheduleWatchHistoryRepublish;
|
||||
nostrClient.resolveWatchHistory = originalResolveWatchHistory;
|
||||
nostrClient.getWatchHistoryFingerprint =
|
||||
originalGetWatchHistoryFingerprint;
|
||||
|
||||
if (!originalFlag) {
|
||||
setWatchHistoryV2Enabled(false);
|
||||
|
||||
Reference in new issue
Block a user