Refactor user blocking to prioritize standard NIP-51 Mute Lists

- Updates `js/userBlocks.js` to write block entries to a standard Kind 10000 Mute List event.
- Implements a hybrid storage model: unencrypted public mutes are stored in `p` tags, while private blocks are stored in the encrypted event content.
- Maintains read compatibility with legacy `d=user-blocks` lists (both Kind 10000 and Kind 30002) by merging and prioritizing the newest event timestamp.
- Updates local caching to persist both public mutes and private blocks, preventing data loss or "zombie" entries on reload.
- Updates `js/subscriptions.js` to correctly detect NIP-44 capabilities on `Nip07Adapter` signers, fixing a regression where NIP-04 fallback was skipped.
- Updates `js/nostrEventSchemas.js` to mark `USER_BLOCK_LIST` as legacy and define `MUTE_LIST`.
- Updates unit tests to verify the new multi-kind fetching and merged publishing logic.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-01 22:10:42 +00:00
1 parent 8383a4f18b
commit de5e453b34
3 files changed
+43 -16

No files matched your search

+7 -2
View File
@@ -887,8 +887,13 @@ class SubscriptionsManager {
signer = await nostrClient.ensureActiveSignerForPubkey(userPubkey);
}
const signerHasNip04 = typeof signer?.nip04Decrypt === "function";
const signerHasNip44 = typeof signer?.nip44Decrypt === "function";
const signerCapabilities = signer?.capabilities;
const signerHasNip04 =
typeof signer?.nip04Decrypt === "function" &&
(!signerCapabilities || signerCapabilities.nip04 !== false);
const signerHasNip44 =
typeof signer?.nip44Decrypt === "function" &&
(!signerCapabilities || signerCapabilities.nip44 !== false);
const nostrApi = allowPermissionPrompt
? typeof window !== "undefined"
+33 -11
View File
@@ -934,9 +934,9 @@ class UserBlockListManager {
const hasLocalData = !!localData;
if (localData) {
this._privateBlocks = localData.blockedPubkeys;
this._publicMutes = new Set(); // Local cache structure currently only stores 'blockedPubkeys'
this.blockedPubkeys = new Set(this._privateBlocks);
this._privateBlocks = localData.privateBlocks || localData.blockedPubkeys || new Set();
this._publicMutes = localData.publicMutes || new Set();
this.blockedPubkeys = new Set([...this._privateBlocks, ...this._publicMutes]);
if (Number.isFinite(localData.createdAt)) {
this.blockEventCreatedAt = localData.createdAt;
@@ -1467,8 +1467,8 @@ class UserBlockListManager {
? this.blockEventCreatedAt
: 0;
// Concurrent incremental fetch for both kinds
const [muteEvents, blockEvents] = await Promise.all([
// Concurrent incremental fetch for all variants
const [muteEvents, blockEvents, legacyEvents] = await Promise.all([
nostrClient.fetchListIncrementally({
kind: 10000,
pubkey: normalized,
@@ -1484,9 +1484,17 @@ class UserBlockListManager {
since: fetchSince,
timeoutMs: 12000,
}),
nostrClient.fetchListIncrementally({
kind: 30002,
pubkey: normalized,
dTag: BLOCK_LIST_IDENTIFIER,
relayUrls: relays,
since: fetchSince,
timeoutMs: 12000,
}),
]);
const combined = [...muteEvents, ...blockEvents];
const combined = [...muteEvents, ...blockEvents, ...legacyEvents];
// If combined is empty, check if we have loaded state
if (!combined.length) {
@@ -1518,10 +1526,21 @@ class UserBlockListManager {
return { blockedPubkeys: new Set(cached), createdAt: null };
}
if (cached && typeof cached === "object") {
const privateBlocks = new Set(
Array.isArray(cached.privateBlocks) ? cached.privateBlocks : [],
);
const publicMutes = new Set(
Array.isArray(cached.publicMutes) ? cached.publicMutes : [],
);
// Fallback for legacy cache format
const blockedPubkeys = new Set(
Array.isArray(cached.blockedPubkeys) ? cached.blockedPubkeys : [],
);
return {
blockedPubkeys: new Set(
Array.isArray(cached.blockedPubkeys) ? cached.blockedPubkeys : [],
),
privateBlocks,
publicMutes,
blockedPubkeys,
createdAt: Number.isFinite(cached.createdAt) ? cached.createdAt : null,
};
}
@@ -1534,9 +1553,12 @@ class UserBlockListManager {
return;
}
profileCache.setProfileData(normalizedActorHex, "blocks", {
blockedPubkeys: Array.from(this._privateBlocks), // Local cache historically stores private blocks
privateBlocks: Array.from(this._privateBlocks),
publicMutes: Array.from(this._publicMutes),
// Keep blockedPubkeys for backward compatibility if needed, or just relying on new keys
blockedPubkeys: Array.from(this.blockedPubkeys),
createdAt: this.blockEventCreatedAt,
version: 1,
version: 2,
});
}
+3 -3
View File
@@ -100,8 +100,8 @@ await (async () => {
assert.deepEqual(
calls,
[...relays, ...relays],
"loadBlocks should initiate queries for all relays concurrently (twice, for block list and mute list)"
[...relays, ...relays, ...relays],
"loadBlocks should initiate queries for all relays concurrently (thrice, for block list, mute list, and legacy kind 30002)"
);
// Reverting to original test data:
@@ -254,7 +254,7 @@ await (async () => {
assert.deepEqual(
calls,
[...relays, ...relays],
[...relays, ...relays, ...relays],
"loadBlocks should query fast and background relays concurrently",
);