Files
client/plans/mute-list-block-page.md
2026-04-17 16:52:51 -04:00

7.6 KiB

Mute List / Block Page Implementation Plan

Overview

Implement NIP-51 kind:10000 mute list support with a dedicated block.html management page and client-side filtering across feed, notifications, and post pages.

Architecture

flowchart TD
    A[block.html] -->|manage| B[mute-list.mjs]
    B -->|fetch kind:10000| C[init-ndk.mjs ndkFetchEvents]
    B -->|publish kind:10000| D[init-ndk.mjs publishEvent]
    B -->|encrypt private entries| E[NIP-44 via worker signer]
    B -->|decrypt private entries| F[NIP-44 via worker signer]
    
    G[feed.html] -->|filter posts| B
    H[notifications.html] -->|filter notifs| B
    I[post.html] -->|filter posts| B
    J[post-interactions2.mjs] -->|mute action| B
    
    B -->|isMuted pubkey| K{Check public + private sets}
    K -->|yes| L[Hide event]
    K -->|no| M[Show event]

NIP-51 kind:10000 Event Structure

{
  "kind": 10000,
  "tags": [
    ["p", "<pubkey-hex>"],
    ["t", "<hashtag>"],
    ["word", "<lowercase-string>"],
    ["e", "<event-id>"]
  ],
  "content": "<NIP-44 encrypted JSON array of private tags>"
}
  • Public tags: visible to everyone in the tags array
  • Private tags: NIP-44 encrypted to self in .content field
  • Replaceable: each new publish overwrites the previous list

Files to Create

1. www/js/mute-list.mjs — Core Mute List Module

Shared singleton module that all pages import. Responsibilities:

  • loadMuteList(pubkey) — Fetch the user's kind:10000 event via ndkFetchEvents, parse public tags, decrypt private tags via NIP-44
  • isMutedPubkey(pubkey) — Check if a pubkey is in the mute set
  • isMutedHashtag(tag) — Check if a hashtag is muted
  • isMutedWord(word) — Check if content contains a muted word
  • isMutedThread(eventId) — Check if a thread is muted
  • isEventMuted(event) — Combined check: pubkey + hashtags + words + thread
  • addMute(type, value, isPrivate) — Add a mute entry (p/t/word/e), rebuild and publish
  • removeMute(type, value) — Remove a mute entry, rebuild and publish
  • getMuteList() — Return current parsed mute list for UI display
  • onMuteListChanged(callback) — Event listener for cross-page reactivity

Internal state:

let muteListEvent = null;        // raw kind:10000 event
let publicMutes = { p: Set, t: Set, word: Set, e: Set };
let privateMutes = { p: Set, t: Set, word: Set, e: Set };
let configured = false;

The module needs references to ndkFetchEvents, publishEvent, and the NIP-44 encrypt/decrypt functions. These will be injected via a configure() call from each page after NDK init.

NIP-44 encryption for private entries: Use the existing worker signer pattern. The init-ndk.mjs already supports nip44Encrypt and nip44Decrypt via handleSignRequest. The mute-list module will need a helper that calls these through the worker. Since the mute list encrypts to self, both sender and recipient pubkey are the user's own pubkey.

2. www/block.html — Block/Mute List Management Page

Following the same page structure as people.html and notifications.html:

Layout:

  • Standard header with hamburger menu, title "BLOCK", avatar
  • Controls section: input field for adding pubkeys/npubs, hashtags, words; toggle for public/private
  • Status bar showing count of muted items
  • List of muted items grouped by type (Pubkeys, Hashtags, Words, Threads)
  • Each item shows: display name/value, public/private badge, remove button
  • For muted pubkeys: resolve and show profile name + avatar via fetchCachedProfile
  • Standard sidenav with relay section, blossom section, version bar

Imports from init-ndk.mjs:

  • initNDKPage, getPubkey, injectHeaderAvatar, ndkFetchEvents, publishEvent, fetchCachedProfile, storeProfile, getRelayData, disconnect, getVersion

Imports from mute-list.mjs:

  • configure, loadMuteList, getMuteList, addMute, removeMute, onMuteListChanged

Features:

  • Add pubkey by hex or npub (auto-detect and convert using NostrTools.nip19)
  • Add hashtag (strip leading # if present, lowercase)
  • Add word (lowercase)
  • Toggle public/private for each new entry
  • Remove any entry with confirmation
  • Show profile cards for muted pubkeys (name, avatar, npub snippet)
  • Real-time update when mute list changes

3. Integration Points

feed.html — Filter muted content from feed

In the existing upsertFeedPost(evt) function around line 410, add a mute check:

function upsertFeedPost(evt) {
  if (!evt?.id || postIds.has(evt.id) || !isOriginalPost(evt)) return false;
  if (isEventMuted(evt)) return false;  // NEW: skip muted events
  postIds.add(evt.id);
  posts.push(evt);
  return true;
}

Also filter the live subscription events and the cached events in fetchFeedWindow.

notifications.html — Filter muted notifications

Add mute check when processing incoming notification events. Skip events from muted pubkeys.

post.html — Filter muted content

Add mute check when displaying posts. Skip events from muted pubkeys.

post-interactions2.mjs — Add "Mute" action button

Add a "Mute" option to the post card action menu. When clicked:

  1. Call addMute('p', event.pubkey, false) (default to public)
  2. Show brief confirmation toast
  3. Optionally hide the post immediately

4. index.html — Add BLOCK app button

Add to the arrApps array around line 518:

{
  id: 'block',
  svg: `<path d="M5,2 A3,3 0 1,0 5,8 A3,3 0 1,0 5,2 M3,3 L7,7" />`,
  name: 'BLOCK',
},

This renders as a circle with a diagonal line through it (universal "block" symbol).

Implementation Order

  1. www/js/mute-list.mjs — Core module with all mute list logic
  2. www/block.html — Management page UI
  3. index.html — Add BLOCK to app grid
  4. feed.html — Integrate mute filtering
  5. notifications.html — Integrate mute filtering
  6. post.html — Integrate mute filtering
  7. post-interactions2.mjs — Add mute action to post cards

Key Design Decisions

Singleton pattern for mute-list.mjs

The module maintains internal state as module-level variables. Each page calls configure() once after NDK init to inject the needed functions, then calls loadMuteList() to fetch and parse. This avoids duplicate fetches across components on the same page.

NIP-44 encryption for private entries

Private mute entries are encrypted to the user's own pubkey using NIP-44. The existing worker signer infrastructure handles this — the page calls nip44Encrypt(ownPubkey, plaintext) and nip44Decrypt(ownPubkey, ciphertext) through the standard sign request flow.

Backward compatibility with NIP-04

Per NIP-51 spec: "Clients can automatically discover if the encryption is NIP-04 or NIP-44 by searching for 'iv' in the ciphertext and decrypting accordingly." The module should check for ?iv= in the content to detect NIP-04 and fall back appropriately.

Replaceable event semantics

kind:10000 is a replaceable event. Every publish must include ALL current entries (both public and private). The module must always rebuild the complete event from its internal state before publishing.

No worker-side filtering

Mute filtering happens client-side in each page's JavaScript. This keeps the worker simple and avoids adding mute-list state management to the shared worker. The mute list is small enough that client-side filtering is negligible overhead.

Profile resolution for muted pubkeys

On block.html, muted pubkeys are resolved to display names using fetchCachedProfile. If not cached, the page falls back to showing the truncated hex pubkey and listens for profile events.