Files
client/plans/fips-directory-page.md
T

151 lines
8.9 KiB
Markdown

# FIPS Directory Page Plan
## Goal
Turn [`www/fips-directory.html`](../www/fips-directory.html:1) (currently a copy of the template) into a community FIPS link directory where:
- Anyone can **view** FIPS links (no login required).
- Logged-in users can **add / remove** their own links.
- There is **no admin** — every person maintains their own **blocklist** (NIP-51 mute list) and decides what to hide from their own view.
- The site owner publishes their curated FIPS links the same way everyone else does (no hardcoded seed data).
## Standards Used
### Links — [NIP-B0: Web Bookmarking](../nips/B0.md:1) (`kind:39701`)
Each FIPS link is a **separate replaceable event**, one per URL:
- `kind`: `39701`
- `d` tag: the FIPS URL (scheme prefix omitted for `https://`, per NIP-B0; for `http://` and `ws://` FIPS URLs the full URL is used)
- `title` tag: the link name
- `t` tag: `fips-directory` (so the page can subscribe to just FIPS-directory bookmarks)
- `content`: markdown description of the link (can be empty)
- `published_at` tag: unix seconds string
**Add a link** = publish a new `kind:39701` event with `d = <url>`.
**Remove a link** = publish a [NIP-09](../nips/09.md:1) deletion request (`kind:5`) referencing the event id, OR publish an empty/blank replacement (NIP-B0 is replaceable by `d`, so republishing with empty content effectively removes it). We'll use NIP-09 deletion for a clean removal.
### Blocklist — [NIP-51: Lists](../nips/51.md:1) (`kind:10000` mute list)
The standard Nostr mute list. Each user publishes one replaceable event:
- `kind`: `10000`
- `p` tags: pubkeys the user wants hidden from their own view
- `content`: optionally NIP-44-encrypted private items (we only use public `p` tags)
**Block a publisher** = re-publish `kind:10000` with their `p` tag added.
**Unblock** = re-publish without that `p` tag.
> **Sovereign moderation:** blocking only affects the blocking user's own view. It does not delete or hide content for anyone else. This is exactly how every NIP-51-compatible Nostr client already works.
## Page Behavior
### Public (not logged in)
- Subscribe to all `kind:39701` events with `#t = ['fips-directory']`.
- Render every publisher's links as cards grouped by publisher.
- Show a "Sign in to add your links" prompt.
- No blocklist controls (no identity to attach a blocklist to).
### Logged in
- Load the user's own `kind:10000` mute list and filter blocked pubkeys out of the rendered list.
- Show an **Add Link** form (name, URL, description).
- Show **remove** buttons on the user's own links (publishes a NIP-09 deletion).
- Show a **block / unblock** button on each publisher's section (for publishers other than yourself).
- Publishing uses [`publishEvent()`](../www/js/init-ndk.mjs:653) from `init-ndk.mjs` (auto-signs via the worker).
### Auth mode
- `authMode = 'optional'` (matches [`www/app-stacks.html`](../www/app-stacks.html:235)) — public load, login on demand via the sidenav logout/login button or the "Sign in" prompt.
## UI Layout (inside `#divBody`)
```
┌─────────────────────────────────────────────┐
│ FIPS DIRECTORY │ (header text)
├─────────────────────────────────────────────┤
│ [Sign in to add your links] (if anon) │
│ │
│ ┌─ Add Link form ─────────────────────────┐ │ (only if logged in)
│ │ Name / URL / Description │ │
│ │ [Add Link] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ Publisher: laantungir ─────────────────┐ │
│ │ [block] (if logged in & not you) │ │
│ │ • My Relay — ws://....fips/relay/ │ │
│ │ [remove] (if yours) │ │
│ │ • My Client — http://....fips/client │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ Publisher: someone-else ───────────────┐ │
│ │ [block] [unblock] │ │
│ │ • Their Thing — http://....fips/thing │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
- Cards use the existing `client.css` variables (`--border-color`, `--border-radius`, `--color`, `--muted-color`, `--font-family`, etc.) — same inline-style pattern as [`www/app-stacks.html`](../www/app-stacks.html:564).
- Links open in a new tab (`target="_blank" rel="noopener"`).
- FIPS URLs are clickable as-is (the server is FIPS-enabled, per the user).
## Data Structures (in-page state)
```js
// Map: eventId -> { id, pubkey, url, title, description, createdAt }
let bookmarks = new Map();
// Set of blocked pubkeys (from current user's kind 10000)
let blocklist = new Set();
// The current user's kind 10000 event id (for re-publishing)
let myMuteListEventId = null;
// Load flags
let bookmarksLoaded = false;
let blocklistLoaded = false;
```
## Implementation Steps
1. **Set page title & header text** — change `<title>TEMPLATE</title>` to `FIPS DIRECTORY` and set `.divHeaderText` to "FIPS DIRECTORY".
2. **Set `authMode = 'optional'`** as the default (like app-stacks.html).
3. **Add state variables** — `bookmarks` (Map), `blocklist` (Set), `myMuteListEventId`, load flags.
4. **Add `esc()` helper** — prevent XSS from relay content (same as app-stacks.html).
5. **Add subscription + listener for `kind:39701` with `#t=['fips-directory']`** — `subscribeFipsBookmarks()` + `initBookmarkListener()`. Parse each event into the bookmarks Map, keyed by event id. Re-render on each event / EOSE.
6. **Add subscription + listener for `kind:10000` (mute list)** — `subscribeMuteLists()` + `initMuteListListener()`. Only the current user's mute list matters for filtering; subscribe broadly, pick out the logged-in user's. Store blocked pubkeys in the `blocklist` Set.
7. **Add `renderDirectory()`** — builds the directory HTML (add-link form if logged in, publisher sections grouped by pubkey, link cards with block/remove buttons), writes to `#divBody`. Filters out blocked pubkeys.
8. **Add `doAddLink()`** — reads the form, publishes a `kind:39701` event with `d=<url>`, `title=<name>`, `t=fips-directory`, `content=<description>`.
9. **Add `doRemoveLink()`** — publishes a `kind:5` (NIP-09 deletion) event referencing the bookmark's event id, then removes it from the local Map and re-renders.
10. **Add `doToggleBlock()`** — adds/removes a `p` tag in the user's `kind:10000` mute list, re-publishes, updates the `blocklist` Set, re-renders.
11. **Wire up subscriptions in `main()`** — after `initializeAuthenticatedPageFeatures()`, call the subscribe/listener init functions (bookmarks subscription runs for both public and logged-in; mute list subscription only matters when logged in but can run always).
12. **Footer UX note** — "Public mode / Sign in from side menu to add your links" when anonymous.
## Architecture Diagram
```mermaid
flowchart LR
A[User loads fips-directory.html] --> B[authMode = optional]
B --> C{Logged in?}
C -- no --> D[Show links + Sign in prompt]
C -- yes --> E[Show links + Add/Remove form + Block controls]
D --> F[Subscribe kind 39701 t:fips-directory]
E --> F
E --> G[Subscribe kind 10000 mute list]
F --> H[Render directory cards grouped by pubkey]
G --> H
H --> I[User clicks link -> opens FIPS URL]
E --> J[Add -> publishEvent kind 39701]
E --> K[Remove -> publishEvent kind 5 NIP-09 deletion]
E --> L[Block/Unblock -> publishEvent kind 10000]
J --> H
K --> H
L --> H
```
## NIP References
- [NIP-B0: Web Bookmarking](../nips/B0.md:1) — `kind:39701` for links
- [NIP-51: Lists](../nips/51.md:1) — `kind:10000` mute list for blocklist
- [NIP-09: Event Deletion Request](../nips/09.md:1) — `kind:5` for removing a link
## Notes
- No new JS modules needed — everything is inline in the HTML, matching the app-stacks.html pattern.
- No CSS file changes — uses existing `client.css` variables via inline styles.
- The `subscribe()` / `publishEvent()` / `getPubkey()` APIs from [`www/js/init-ndk.mjs`](../www/js/init-ndk.mjs:1) are already imported by the template.
- Fully standards-based: other Nostr clients that support NIP-B0 bookmarks and NIP-51 mute lists will interoperate with the data we publish.