From 124a2330ad043bd81bcb2150cf1dc03a488d34cf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:08:01 +0000 Subject: [PATCH] Update block list selection to prioritize tagged events - Prioritize Kind 10000 events with `d=user-blocks` over plain mute lists in `loadBlocks`. - Add `isTaggedBlockListEvent` helper for differentiation. - Add regression test case to `tests/user-blocks.test.mjs` verifying that tagged lists override newer plain lists. --- js/userBlocks.js | 31 ++++++++++++++++++-- tests/user-blocks.test.mjs | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/js/userBlocks.js b/js/userBlocks.js index a2044a10..36b840c0 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -386,6 +386,21 @@ function isUserBlockListEvent(event) { return false; } +function isTaggedBlockListEvent(event) { + if (!event || !Array.isArray(event.tags)) { + return false; + } + return event.tags.some( + (tag) => + Array.isArray(tag) && + tag.length >= 2 && + typeof tag[0] === "string" && + tag[0].trim().toLowerCase() === "d" && + typeof tag[1] === "string" && + tag[1].trim() === BLOCK_LIST_IDENTIFIER, + ); +} + const BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; const BECH32_CHARSET_MAP = (() => { const map = new Map(); @@ -1164,14 +1179,24 @@ class UserBlockListManager { } // Separate by kind to merge distinct lists (Block vs Mute) - const muteEvents = validEvents - .filter((e) => e.kind === 10000) + const muteEvents = validEvents.filter((e) => e.kind === 10000); + + // Prioritize tagged mute lists (Bitvid) over plain ones (others) + const taggedMutes = muteEvents + .filter((e) => isTaggedBlockListEvent(e)) .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0)); + + const plainMutes = muteEvents + .filter((e) => !isTaggedBlockListEvent(e)) + .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0)); + + const newestMute = + taggedMutes.length > 0 ? taggedMutes[0] : plainMutes[0] || null; + const blockEvents = validEvents .filter((e) => e.kind === 30002) .sort((a, b) => (b?.created_at || 0) - (a?.created_at || 0)); - const newestMute = muteEvents[0] || null; const newestBlock = blockEvents[0] || null; const newestMuteTime = Number.isFinite(newestMute?.created_at) diff --git a/tests/user-blocks.test.mjs b/tests/user-blocks.test.mjs index 4b87c6b0..91187114 100644 --- a/tests/user-blocks.test.mjs +++ b/tests/user-blocks.test.mjs @@ -1179,3 +1179,62 @@ await (async () => { nostrClient.pool = originalPool; } })(); + +await (async () => { + // Test: Prioritize 'd=user-blocks' tagged lists over newer plain lists + const actor = "a".repeat(64); + const taggedBlocked = "b".repeat(64); + const plainBlocked = "c".repeat(64); + + const originalRelays = nostrClient.relays; + const originalPool = nostrClient.pool; + const originalNostr = window.nostr; + + nostrClient.relays = ["wss://repro.example"]; + + window.nostr = { + nip04: { + decrypt: async (_pubkey, ciphertext) => { + if (ciphertext === "encrypted-tagged") { + return JSON.stringify({ blockedPubkeys: [taggedBlocked] }); + } + return "{}"; + } + } + }; + + nostrClient.pool = { + list: async () => [ + { + id: "event-tagged", + kind: 10000, + created_at: 1000, + pubkey: actor, + content: "encrypted-tagged", + tags: [["d", "user-blocks"]], + }, + { + id: "event-plain", + kind: 10000, + created_at: 2000, // Newer! + pubkey: actor, + content: "", + tags: [["p", plainBlocked]], + } + ] + }; + + userBlocks.reset(); + + try { + await userBlocks.loadBlocks(actor); + + const blocked = userBlocks.getBlockedPubkeys(); + assert.ok(blocked.includes(taggedBlocked), "Should include pubkey from tagged list"); + assert.ok(!blocked.includes(plainBlocked), "Should NOT include pubkey from plain list (fallback ignored)"); + } finally { + nostrClient.relays = originalRelays; + nostrClient.pool = originalPool; + window.nostr = originalNostr; + } +})();