Refresh watch history when snapshots change

This commit is contained in:
thePR0M3TH3AN committed 2025-09-30 13:04:56 -04:00
1 parent befa1ad5ae
commit 2351f9e126
3 files changed
+249 -29

No files matched your search

+1 -4
View File
@@ -3955,10 +3955,7 @@ class bitvidApp {
sentinelSelector: "#profileHistorySentinel",
scrollContainerSelector: "#profileHistoryScroll",
emptyCopy: "You haven’t watched any videos yet.",
beforeInitialLoad: async () => {
const actor = this.pubkey || undefined;
await nostrClient.fetchWatchHistory(actor);
},
getActor: async () => this.pubkey || window.app?.pubkey || undefined,
});
}
this.adminModeratorsSection =
+209 -25
View File
@@ -16,22 +16,47 @@ function toNumber(value, fallback) {
}
export function createWatchHistoryRenderer(config = {}) {
const fallbackGetActor = async () => {
const candidate =
typeof window?.app?.pubkey === "string" && window.app.pubkey.trim()
? window.app.pubkey.trim()
: "";
return candidate || undefined;
};
const mergedConfig = {
viewSelector: "#watchHistoryView",
gridSelector: "#watchHistoryGrid",
loadingSelector: "#watchHistoryLoading",
emptySelector: "#watchHistoryEmpty",
sentinelSelector: "#watchHistorySentinel",
scrollContainerSelector: null,
emptyCopy: WATCH_HISTORY_EMPTY_COPY,
batchSize: BATCH_SIZE,
getActor: fallbackGetActor,
beforeInitialLoad: null,
getSnapshotFingerprint: null,
resolveBatch: (size) => nostrClient.resolveWatchHistory(size),
renderGrid: null,
...config,
};
const {
viewSelector = "#watchHistoryView",
gridSelector = "#watchHistoryGrid",
loadingSelector = "#watchHistoryLoading",
emptySelector = "#watchHistoryEmpty",
sentinelSelector = "#watchHistorySentinel",
scrollContainerSelector = null,
emptyCopy = WATCH_HISTORY_EMPTY_COPY,
beforeInitialLoad = async () => {
const actor = window.app?.pubkey || undefined;
await nostrClient.fetchWatchHistory(actor);
},
resolveBatch = (size) => nostrClient.resolveWatchHistory(size),
renderGrid = null,
batchSize = BATCH_SIZE,
} = config;
viewSelector,
gridSelector,
loadingSelector,
emptySelector,
sentinelSelector,
scrollContainerSelector,
emptyCopy,
batchSize,
getActor,
resolveBatch,
renderGrid,
} = mergedConfig;
const providedBeforeInitialLoad = mergedConfig.beforeInitialLoad;
const providedGetSnapshotFingerprint = mergedConfig.getSnapshotFingerprint;
const selectors = {
view: viewSelector,
@@ -56,6 +81,64 @@ export function createWatchHistoryRenderer(config = {}) {
}
};
const resolveActor = async (hint = null) => {
if (typeof hint === "string" && hint.trim()) {
return hint.trim();
}
if (typeof getActor === "function") {
try {
const candidate = await getActor();
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
} catch (error) {
debugLog("failed to resolve actor via getActor", error);
}
}
return null;
};
const callBeforeInitialLoad = async (context = {}) => {
let actor = await resolveActor(context.actor ?? null);
const finalContext = { ...context, actor };
if (typeof providedBeforeInitialLoad === "function") {
return providedBeforeInitialLoad(finalContext);
}
const snapshot = await nostrClient.fetchWatchHistory(actor || undefined);
if (!actor) {
try {
const ensured = await nostrClient.ensureSessionActor();
if (typeof ensured === "string" && ensured.trim()) {
actor = ensured.trim();
}
} catch (error) {
debugLog("failed to ensure session actor while resolving history", error);
}
}
return { actor: actor || null, snapshot };
};
const computeSnapshotFingerprint = async (context = {}) => {
const actor = await resolveActor(context.actor ?? null);
const finalContext = { ...context, actor };
if (typeof providedGetSnapshotFingerprint === "function") {
return providedGetSnapshotFingerprint(finalContext);
}
if (finalContext.refresh) {
await nostrClient.fetchWatchHistory(actor || undefined);
}
return nostrClient.getWatchHistoryFingerprint(actor || undefined);
};
const state = {
isLoading: false,
hasMore: true,
@@ -65,6 +148,8 @@ export function createWatchHistoryRenderer(config = {}) {
initialized: false,
emptyCopy,
batchSize: Math.max(1, toNumber(batchSize, BATCH_SIZE)),
actor: null,
snapshotFingerprint: null,
};
const query = (selector) => {
@@ -313,29 +398,75 @@ export function createWatchHistoryRenderer(config = {}) {
});
};
const runInitialLoad = async () => {
const runInitialLoad = async (options = {}) => {
const { actor: actorHint = null, prefetched = null, fingerprintOverride } =
typeof options === "object" && options !== null ? options : {};
const { view } = getElements();
if (!view) {
debugLog("runInitialLoad aborted: view not found");
return;
}
const resolvedActor = await resolveActor(actorHint ?? state.actor);
state.actor = resolvedActor || null;
cleanupObservers();
state.resolvedVideos = [];
state.hasMore = true;
state.isLoading = false;
state.initialized = false;
state.snapshotFingerprint = null;
resetUiState();
debugLog("starting initial load");
debugLog("starting initial load", { actor: state.actor });
try {
await beforeInitialLoad?.();
} catch (error) {
console.error("[historyView] Failed to fetch watch history list:", error);
showEmptyState(
"We couldn't load your watch history. Please try again later."
);
return;
let beforeResult;
const hasPrefetched =
prefetched &&
Object.prototype.hasOwnProperty.call(prefetched, "beforeResult");
if (hasPrefetched) {
beforeResult = prefetched.beforeResult;
} else {
try {
beforeResult = await callBeforeInitialLoad({ actor: state.actor });
} catch (error) {
console.error("[historyView] Failed to fetch watch history list:", error);
showEmptyState(
"We couldn't load your watch history. Please try again later."
);
return;
}
}
if (
beforeResult &&
typeof beforeResult === "object" &&
typeof beforeResult.actor === "string" &&
beforeResult.actor.trim()
) {
state.actor = beforeResult.actor.trim();
}
let fingerprint;
if (typeof fingerprintOverride !== "undefined") {
fingerprint = fingerprintOverride;
} else {
try {
fingerprint = await computeSnapshotFingerprint({
actor: state.actor,
beforeResult,
refresh: false,
});
} catch (error) {
debugLog("failed to compute snapshot fingerprint", error);
}
}
if (typeof fingerprint === "string") {
state.snapshotFingerprint = fingerprint;
} else {
state.snapshotFingerprint = null;
}
await loadNextBatch({ initial: true });
@@ -348,6 +479,7 @@ export function createWatchHistoryRenderer(config = {}) {
debugLog("initial load complete", {
hasMore: state.hasMore,
resolvedCount: state.resolvedVideos.length,
actor: state.actor,
});
};
@@ -369,6 +501,56 @@ export function createWatchHistoryRenderer(config = {}) {
return;
}
state.actor = (await resolveActor(state.actor)) || null;
if (!state.isLoading) {
try {
const beforeResult = await callBeforeInitialLoad({
actor: state.actor,
reason: "ensureInitialLoad",
});
if (
beforeResult &&
typeof beforeResult === "object" &&
typeof beforeResult.actor === "string" &&
beforeResult.actor.trim()
) {
state.actor = beforeResult.actor.trim();
}
const fingerprint = await computeSnapshotFingerprint({
actor: state.actor,
beforeResult,
refresh: false,
});
if (typeof fingerprint === "string") {
if (fingerprint !== state.snapshotFingerprint) {
debugLog("ensureInitialLoad detected new snapshot", {
previous: state.snapshotFingerprint,
next: fingerprint,
actor: state.actor,
});
await runInitialLoad({
actor: state.actor,
prefetched: { beforeResult },
fingerprintOverride: fingerprint,
});
return;
}
state.snapshotFingerprint = fingerprint;
}
} catch (error) {
console.warn(
"[historyView] Failed to refresh watch history snapshot:",
error
);
}
} else {
debugLog("ensureInitialLoad skipping snapshot refresh (still loading)", {
actor: state.actor,
});
}
if (!state.resolvedVideos.length && !state.isLoading) {
if (!state.hasMore) {
debugLog("ensureInitialLoad resetting hasMore before retry");
@@ -413,6 +595,8 @@ export function createWatchHistoryRenderer(config = {}) {
state.resolvedVideos = [];
state.hasMore = true;
state.isLoading = false;
state.actor = null;
state.snapshotFingerprint = null;
resetUiState();
setLoadingVisible(false);
},
+39
View File
@@ -969,6 +969,45 @@ class NostrClient {
}
}
getWatchHistoryFingerprint(pubkeyOrSession) {
let actor =
typeof pubkeyOrSession === "string" && pubkeyOrSession.trim()
? pubkeyOrSession.trim()
: "";
if (!actor) {
if (this.sessionActor?.pubkey) {
actor = this.sessionActor.pubkey.trim();
} else if (typeof this.pubkey === "string" && this.pubkey.trim()) {
actor = this.pubkey.trim();
}
}
if (!actor) {
return "";
}
const entry = this.watchHistoryCache.get(actor);
if (!entry) {
return "";
}
const pointerId =
entry.pointerEvent && typeof entry.pointerEvent.id === "string"
? entry.pointerEvent.id
: "";
const pointerCreated =
entry.pointerEvent && Number.isFinite(entry.pointerEvent.created_at)
? entry.pointerEvent.created_at
: 0;
const itemKeys = entry.items
.map((item) => pointerKey(item))
.filter(Boolean)
.join("|");
return `${pointerId}:${pointerCreated}:${itemKeys}`;
}
async encryptWatchHistoryPayload(actorPubkey, payload) {
const normalizedActor =
typeof actorPubkey === "string" && actorPubkey.trim()