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.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-08 00:52:57 +00:00
1 parent 05292925c8
commit 7d274bc331
2 files changed
+43 -4

No files matched your search

+15 -4
View File
@@ -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;
+28
View File
@@ -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;
}