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; +}