From 511cfa25cf029cbaa24951e6cc5a870f7843335d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 02:55:55 +0000 Subject: [PATCH] fix(for-you): preload watch history and filter watched videos Preload watch history in `refreshForYouFeed` to ensure `watchHistoryService` has the latest data. Update `buildForYouFeedRuntime` to include a `watchHistory` suppression hook that checks against preloaded watch history items. Register `createWatchHistorySuppressionStage` in the "for-you" feed pipeline to filter out watched videos. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/app.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/js/app.js b/js/app.js index 3bf32daf..43cd3698 100644 --- a/js/app.js +++ b/js/app.js @@ -47,6 +47,7 @@ import { createModerationStage, createResolvePostedAtStage, createTagPreferenceFilterStage, + createWatchHistorySuppressionStage, createChronologicalSorter, createSubscriptionAuthorsSource, registerWatchHistoryFeed, @@ -120,6 +121,7 @@ import RevertModalController from "./ui/revertModalController.js"; import TagPreferenceMenuController from "./ui/tagPreferenceMenuController.js"; import ReactionController from "./ui/reactionController.js"; import { pointerArrayToKey } from "./utils/pointer.js"; +import { pointerKey } from "./nostr/watchHistory.js"; import resolveVideoPointer, { buildVideoAddressPointer, } from "./utils/videoPointer.js"; @@ -5753,6 +5755,7 @@ class Application { shouldIncludeVideo: (video, options) => this.nostrService.shouldIncludeVideo(video, options), }), + createWatchHistorySuppressionStage(), createDedupeByRootStage({ dedupe: (videos) => this.dedupeVideosByRoot(videos), }), @@ -6093,7 +6096,7 @@ class Application { } } - buildForYouFeedRuntime() { + buildForYouFeedRuntime({ watchHistoryItems = [] } = {}) { const blacklist = this.blacklistedEventIds instanceof Set ? new Set(this.blacklistedEventIds) @@ -6111,6 +6114,19 @@ class Application { const { interests = [], disinterests = [] } = preferenceSource || {}; const moderationThresholds = this.getActiveModerationThresholds(); + const watchedKeys = new Set(); + if (Array.isArray(watchHistoryItems)) { + for (const item of watchHistoryItems) { + const key = + typeof item.pointerKey === "string" && item.pointerKey + ? item.pointerKey + : pointerKey(item.pointer || item); + if (key) { + watchedKeys.add(key); + } + } + } + return { blacklistedEventIds: blacklist, isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey), @@ -6121,6 +6137,41 @@ class Application { moderationThresholds: moderationThresholds ? { ...moderationThresholds } : undefined, + watchHistory: { + shouldSuppress: (item) => { + const video = item?.video; + if (!video || typeof video !== "object") { + return false; + } + + if (video.id) { + const eKey = pointerKey({ type: "e", value: video.id }); + if (watchedKeys.has(eKey)) { + return true; + } + } + + if ( + Number.isFinite(video.kind) && + video.kind >= 30000 && + video.kind < 40000 && + typeof video.pubkey === "string" + ) { + const dTag = Array.isArray(video.tags) + ? video.tags.find((t) => Array.isArray(t) && t[0] === "d") + : null; + const dValue = dTag && typeof dTag[1] === "string" ? dTag[1] : ""; + if (dValue) { + const aValue = `${video.kind}:${video.pubkey}:${dValue}`; + const aKey = pointerKey({ type: "a", value: aValue }); + if (watchedKeys.has(aKey)) { + return true; + } + } + } + return false; + }, + }, }; } @@ -6324,8 +6375,20 @@ class Application { }; } - refreshForYouFeed({ reason, fallbackVideos } = {}) { - const runtime = this.buildForYouFeedRuntime(); + async refreshForYouFeed({ reason, fallbackVideos } = {}) { + let watchHistoryItems = []; + try { + watchHistoryItems = await watchHistoryService.loadLatest(undefined, { + allowStale: true, + }); + } catch (error) { + devLogger.warn( + "[Application] Failed to preload watch history for For You feed:", + error, + ); + } + + const runtime = this.buildForYouFeedRuntime({ watchHistoryItems }); const normalizedReason = typeof reason === "string" ? reason : undefined; const fallback = Array.isArray(fallbackVideos) ? fallbackVideos : [];