# Plan: Move Mute List Filtering into SharedWorker

## Problem

The mute list (kind 10000) is currently loaded and evaluated **page-side only** in `www/js/mute-list.mjs`. The SharedWorker (`www/ndk-worker.js`) broadcasts all subscription events unfiltered. This means:

1. Every page must independently import and call `isEventMuted()` — easy to forget
2. Muted events still get cached in IndexedDB by the worker
3. Muted events still consume bandwidth and processing across all tabs
4. Pages that don't implement mute checks show muted content

## Current Architecture

```mermaid
flowchart LR
    R[Relays] -->|all events| W[SharedWorker]
    W -->|broadcast unfiltered| P1[Page 1]
    W -->|broadcast unfiltered| P2[Page 2]
    P1 -->|page-side check| ML1[mute-list.mjs]
    P2 -->|page-side check| ML2[mute-list.mjs]
    
    style W fill:#553,color:#fff
    style ML1 fill:#335,color:#fff
    style ML2 fill:#335,color:#fff
```

## Target Architecture

```mermaid
flowchart LR
    R[Relays] -->|all events| W[SharedWorker]
    W -->|filter via mute sets| F[isEventMuted check]
    F -->|clean events only| P1[Page 1]
    F -->|clean events only| P2[Page 2]
    W -->|fetch kind 10000| R
    P1 -->|updateMuteList msg| W
    
    style W fill:#553,color:#fff
    style F fill:#533,color:#fff
```

## Design Decisions

### Public mutes only in worker
The worker will only filter on **public mute entries** (the `tags` array of the kind 10000 event). Private/encrypted mute entries require NIP-44 decryption which needs the signer — the worker already has `messageSigner` for this, so we can decrypt private entries too via the existing `requestFromPage` pattern.

### Worker fetches its own mute list
During `handleInit()`, after relay connection, the worker will fetch kind 10000 for the authenticated pubkey and parse it. This parallels how it already fetches kind 3 (follows), kind 10002 (relay list), etc.

### Pages can still add/remove mutes
The page-side `addMute()`/`removeMute()` flow publishes a new kind 10000 event. After publishing, the page sends a `syncMuteList` message to the worker so it reloads.

### Backward compatibility
Keep `isEventMuted()` exported from `init-ndk.mjs` so pages that need fine-grained control (like event-management showing muted items with a badge) can still check. But the worker pre-filters subscription broadcasts so most pages don't need to.

## Files to Modify

### `www/ndk-worker.js`
- Add mute list state variables (public/private mute sets)
- Add `parseMuteListTags()` helper
- Add `isWorkerEventMuted()` check function
- Add `handleLoadMuteList()` — fetches kind 10000, parses public tags, decrypts private content via `messageSigner.requestFromPage`
- Modify `wireSubscriptionHandlers()` — filter events through `isWorkerEventMuted()` before `broadcast()`
- Add `syncMuteList` message handler in the switch statement
- Call `handleLoadMuteList()` during `handleInit()` after relay connection
- Broadcast `muteListUpdated` event to all pages when mute list changes

### `www/js/init-ndk.mjs`
- Add `syncWorkerMuteList()` export that sends `syncMuteList` message to worker
- Modify `bootstrapMuteList()` to also trigger worker-side load
- Add handler for `muteListUpdated` worker message to keep page-side mute-list.mjs in sync
- Keep all existing exports for backward compatibility

### `www/js/mute-list.mjs`
- No changes needed — it continues to work page-side for pages that need it
- Worker sync will update it via the `muteListUpdated` event path

### Pages (minimal changes)
- `www/vj.html` — can remove the `isEventMuted` check in `handleMusicNdkEvent()` since worker pre-filters
- `www/feed.html`, `www/post.html`, `www/notifications.html`, `www/skills-edit.html` — subscription events will arrive pre-filtered; existing `isEventMuted()` calls become redundant but harmless (double-check is fine)
- `www/event-management.html` — keep page-side check since it shows muted items with a visual badge
- `www/block.html` — no subscription events; manages the mute list directly

## Implementation Steps

1. **Add mute state and parsing to worker** — port the `parseTags()` and `isEventMuted()` logic into `ndk-worker.js` as standalone functions
2. **Add `handleLoadMuteList()` to worker** — fetch kind 10000, parse public tags, decrypt private content using `messageSigner.requestFromPage('nip44Decrypt', ...)`
3. **Filter in `wireSubscriptionHandlers()`** — before `broadcast()`, check `isWorkerEventMuted(event)` and skip if true
4. **Call during init** — add `handleLoadMuteList(currentPubkey)` call in `handleInit()` after relay setup
5. **Add `syncMuteList` message type** — so pages can trigger a reload after publishing mute changes
6. **Broadcast `muteListUpdated`** — after worker loads/reloads mute list, broadcast the parsed public mute data to all pages
7. **Wire `init-ndk.mjs`** — handle `muteListUpdated` message, call `syncWorkerMuteList()` after `addMute`/`removeMute`
8. **Clean up page-side checks** — remove redundant `isEventMuted()` calls from subscription handlers in pages where the worker now pre-filters

## Edge Cases

- **Worker init before mute list loads**: Events arriving before the mute list is fetched will pass through unfiltered. This is acceptable — the mute list loads quickly and the window is small.
- **Multiple tabs with different mute states**: The worker is shared, so all tabs see the same mute filtering. This is correct behavior.
- **Mute list publish race**: After `addMute()` publishes, the page sends `syncMuteList` to the worker. The worker re-fetches from cache/relays. There's a brief window where the old mute list is active. Acceptable.
- **Anonymous/unauthenticated mode**: No mute list to load. Worker skips mute filtering. No change from current behavior.
