From 7d274bc331fed6f6edfc60a5419ad73a25b5e27c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 00:52:57 +0000 Subject: [PATCH] feat: incorporate tag preferences into blacklist filter stage - Implements `hasDisinterestedTag` helper in `js/feedEngine/utils.js` to check for disinterested tags in both raw Nostr tags and NIP-71 metadata. - Updates `createBlacklistFilterStage` in `js/feedEngine/stages.js` to filter out videos matching `context.runtime.tagPreferences` disinterests. - Ensures existing blacklist logic remains undisturbed. - Verified with new unit test `tests/feed-engine-tag-pref.test.mjs` and existing feed engine tests. --- js/feedEngine/stages.js | 19 +++++++++++++++---- js/feedEngine/utils.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/js/feedEngine/stages.js b/js/feedEngine/stages.js index ef0fe569..5f0e78c6 100644 --- a/js/feedEngine/stages.js +++ b/js/feedEngine/stages.js @@ -10,7 +10,7 @@ import nostrService from "../services/nostrService.js"; import moderationService from "../services/moderationService.js"; import logger from "../utils/logger.js"; import { dedupeToNewestByRoot } from "../utils/videoDeduper.js"; -import { isPlainObject, toSet } from "./utils.js"; +import { isPlainObject, toSet, hasDisinterestedTag } from "./utils.js"; const FEED_HIDE_BYPASS_NAMES = new Set(["home", "recent"]); @@ -276,9 +276,9 @@ export function createBlacklistFilterStage({ const options = { blacklistedEventIds: blacklist, isAuthorBlocked }; - // TODO(tag-preferences): incorporate context.runtime.tagPreferences once the - // filtering helpers are in place so this stage can drop disinterested tags - // without disturbing existing blacklist logic. + const tagPreferences = context?.runtime?.tagPreferences; + const disinterests = toSet(tagPreferences?.disinterests); + const hasTagPreferences = disinterests.size > 0; const results = []; @@ -289,6 +289,17 @@ export function createBlacklistFilterStage({ continue; } + if (hasTagPreferences && hasDisinterestedTag(video, disinterests)) { + context?.addWhy?.({ + stage: stageName, + type: "filter", + reason: "disinterested-tag", + videoId: typeof video.id === "string" ? video.id : null, + pubkey: typeof video.pubkey === "string" ? video.pubkey : null, + }); + continue; + } + let include = false; try { include = includeFn(video, options) !== false; diff --git a/js/feedEngine/utils.js b/js/feedEngine/utils.js index 38512b73..44b94112 100644 --- a/js/feedEngine/utils.js +++ b/js/feedEngine/utils.js @@ -27,3 +27,31 @@ export function toArray(value) { } return [value]; } + +export function hasDisinterestedTag(video, disinterestsSet) { + if (!video || !disinterestsSet || !(disinterestsSet instanceof Set) || disinterestsSet.size === 0) { + return false; + } + + // Check raw tags + if (Array.isArray(video.tags)) { + for (const tag of video.tags) { + if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") { + if (disinterestsSet.has(tag[1].toLowerCase())) { + return true; + } + } + } + } + + // Check NIP-71 metadata hashtags if available + if (video.nip71 && Array.isArray(video.nip71.hashtags)) { + for (const tag of video.nip71.hashtags) { + if (typeof tag === "string" && disinterestsSet.has(tag.toLowerCase())) { + return true; + } + } + } + + return false; +}