# 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

```mermaid
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

```json
{
  "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:
```js
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:

```js
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:

```js
{
  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.
