# Feed Upgrade — Implementation Plan

## Overview

A series of improvements to the feed page and post interaction bar, centered on:
1. **Simplified feed** — show posts without inline replies, just counts
2. **Post thread page** — click comment button to open post with full comment thread in a new tab
3. **Zap capability indicators** — show what the recipient can receive (nutzap, Lightning)
4. **Zap rail selector** — let users choose between nutzap and Lightning melt
5. **Balance check and error handling** — pre-flight checks and better error messages

The project uses **Cashu as the only payment rail** — nutzaps (NIP-61) for ecash transfers, and Cashu melt for Lightning zaps (NIP-57). No NWC, CLINK, or onchain needed.

### Current state

- [`www/feed.html`](www/feed.html) — shows posts with inline replies (toggled by "Comments" button). Replies appear under the post as they occur. This is unreliable.
- [`www/feed2.html`](www/feed2.html) — copy of feed.html, to be modified per this plan
- [`www/post.html`](www/post.html) — posting/composer page (NOT a thread viewer). Accepts `?event=`, `?npub=`, `?profile=` params.
- [`www/note.html`](www/note.html) — long-form note editor (kind 30023/30024), NOT for kind 1 threads
- [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) — renders the interaction bar (like, comment, quote, zap buttons) and handles zap routing
- [`www/js/zaps.mjs`](www/js/zaps.mjs) — NIP-57 Lightning zap protocol

### URL schema (existing convention)

The project uses `URLSearchParams` with query parameters:
- `?npub=<npub1...>` — Nostr public key (bech32)
- `?pubkey=<hex>` — Nostr public key (hex)
- `?event=<hex|nevent|note>` — Event identifier
- `?auth=required|optional|none` — Auth mode

The new `post-feed.html` page will follow this schema: `post-feed.html?event=<hex|nevent|note>`

### CSS theme

The project uses CSS variables (never hardcode colors):
- `--primary-color` (black in light mode, white in dark mode) — main text/icons
- `--secondary-color` (white in light mode, black in dark mode) — background
- `--accent-color` (#ff0000 red, both modes) — the **only** highlight color
- `--muted-color` (#dddddd light / #777777 dark) — dimmed/disabled

All indicators must use these variables. "Colored" = `var(--accent-color)`, "dimmed" = `var(--muted-color)`.

---

## Phase 1: Simplified Feed (feed2.html)

Remove inline replies from the feed. Show only the original post with interaction counts (likes, comments, zaps, nutzaps). Clicking the comment button opens the post thread page in a **new tab**.

### Tasks

- [x] **1.1** In `feed2.html`, remove the inline comment rendering and the "Comments" toggle button. The feed should only show top-level posts.

- [x] **1.2** Change the comment button behavior — instead of calling `onCommentIntentFn` to show inline comments, open `post-feed.html?event=<eventId>` in a **new tab** using `window.open('./post-feed.html?event=' + encodeURIComponent(postId), '_blank')`.

- [x] **1.3** Keep the interaction bar functional in the feed:
  - ✅ Like button — works inline (no navigation)
  - ✅ Zap button — works inline (opens zap dialog, sends nutzap or Lightning melt)
  - ✅ Nutzap count display — shows total nutzaps received
  - ✅ Comment button — opens `post-feed.html?event=<eventId>` in a new tab
  - ✅ Quote button — opens quote composer (no navigation)

- [x] **1.4** Keep real-time updates for counts (likes, zaps, comments, nutzaps) — the feed should still update these counts as events arrive, just not render the reply content inline.

### Files to modify

| File | Changes |
|------|---------|
| [`www/feed2.html`](www/feed2.html) | Remove inline comment rendering, comment button opens post-feed.html in new tab |

---

## Phase 2: Post Thread Page (post-feed.html)

Create a new `www/post-feed.html` page that shows a post with its full comment thread — all replies, with the ability to reply inline.

### URL schema

```
post-feed.html?event=<hex|nevent|note>
```

Accepts the same event identifier formats as the rest of the project:
- Hex event ID: `post-feed.html?event=abc123...`
- nevent: `post-feed.html?event=nevent1q...`
- note: `post-feed.html?event=note1...`

### Tasks

- [x] **2.1** Create `www/post-feed.html` based on the [`www/template.html`](www/template.html) pattern — standard header, hamburger, sidenav, footer.

- [x] **2.2** Parse the `event` URL parameter using `URLSearchParams` (same pattern as `post.html` line 248). Decode nevent/note to hex if needed.

- [x] **2.3** Fetch the original post event via the worker's `ndkFetchEvents` or `queryCache` function. Display it using `renderPostItem` from `post-interactions.mjs`.

- [x] **2.4** Fetch all replies — kind 1 events with an `e` tag pointing to the post ID. Use `ndkFetchEvents` with a filter like `{ kinds: [1], '#e': [postId] }`. **Updated:** Now uses recursive fetching (`fetchRepliesRecursive`) to find nested replies up to 5 levels deep.

- [x] **2.5** Render the comment thread below the original post:
  - Threaded with vertical indent lines (Phase 2.5)
  - Each reply rendered using `renderPostItem` from `post-interactions2.mjs`
  - Depth-first ordering with level computation from NIP-10 e tags

- [x] **2.6** Add an inline composer at the **top** (under the main post, before replies) for posting replies. The composer:
  - Tags the original post: `['e', postId, '', 'reply']`
  - Tags the original author: `['p', postPubkey]`
  - Uses the existing post composer component (`post-composer.mjs`)
  - Styled to match `post.html`'s composer (`.topPostComposerInput` CSS)

- [x] **2.7** Real-time updates — subscribe to new replies via the worker's subscription system. Append new replies to the thread as they arrive. New replies trigger a 1-level recursive fetch for their sub-replies.

- [x] **2.8** Each reply should have its own interaction bar (like, zap, quote) via `post-interactions2.mjs`. The comment button on a reply opens `post-feed.html?event=<replyId>` in a new tab for that reply's sub-thread.

### Files to create

| File | Purpose |
|------|---------|
| `www/post-feed.html` | New post thread page — shows post + all replies + inline composer |

---

## Phase 2.5: Threaded Replies with Vertical Lines (post-feed.html)

Upgrade the flat reply list in `post-feed.html` to show **threaded replies with vertical indent lines**, matching Amethyst's thread view.

### How Amethyst does it

Amethyst uses two key components:

1. **`ThreadLevelCalculator.replyLevel(note, cachedLevels)`** — computes the depth of each reply recursively:
   - Root post (no `e` tag reply) → level 0
   - A reply's level = `max(parent's level for each parent) + 1`
   - Parents are determined by NIP-10 `e` tags in the event

2. **`Modifier.drawReplyLevel(level, color, selected)`** — draws vertical lines to the left of each post:
   - Draws `level` vertical lines, each 2px wide, spaced 3px apart
   - The last line (closest to the post) uses the "selected" color
   - All other lines use the "muted" color
   - The post content is padded left by `2 + (level * 3)px`

### Visual design

```
Original post (level 0)
│ Reply A (level 1) — direct reply to original
│ │ Reply A1 (level 2) — reply to Reply A
│ │ Reply A2 (level 2) — another reply to Reply A
│ Reply B (level 1) — another direct reply to original
│ │ Reply B1 (level 2) — reply to Reply B
│ │ │ Reply B1a (level 3) — reply to Reply B1
│ Reply C (level 1) — yet another direct reply
```

Each `│` is a vertical line drawn with CSS `border-left` on a container div. The lines use:
- `var(--muted-color)` for non-selected levels
- `var(--accent-color)` for the current post's level (the post being viewed)

### Tasks

- [x] **2.5.1** Add a `computeReplyLevels(events, rootEventId)` function to `post-feed.html`:
  - Build a map of `eventId → parentEventId` from the `e` tags (NIP-10 marked reply tags)
  - For each event, compute its level by walking up the parent chain to the root
  - Root post = level 0, direct reply = level 1, reply to reply = level 2, etc.
  - Handle missing parents gracefully (if a parent isn't in the fetched set, treat as level 1)
  - Return a `Map<eventId, level>`

- [x] **2.5.2** Sort replies in depth-first order (like Amethyst's `replyLevelSignature`):
  - Root post first
  - Then replies grouped by parent, in chronological order within each group
  - This makes the vertical lines visually contiguous — a reply's children appear right below it

- [x] **2.5.3** Render each reply with vertical indent lines:
  - Wrap each reply in a container div with `padding-left: calc(level * 12px)`
  - For each level 0..N-1, add a vertical line using positioned divs with `background: var(--muted-color)`
  - The last line (level N-1) uses `var(--accent-color)` if this is the focused post, otherwise `var(--muted-color)`

- [x] ~~**2.5.4** Add collapse/expand functionality~~ — **Removed per user request.** All replies are shown, no collapse.

- [x] **2.5.5** Highlight the focused post:
  - If the URL has a `focus=<eventId>` param, scroll to and highlight that post
  - The highlighted post's vertical line uses `var(--accent-color)` and card gets `var(--accent-color)` outline

### CSS approach (CSS variables only)

```css
.reply-thread-line {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--muted-color);
}

.reply-thread-line.selected {
    background: var(--accent-color);
}

.reply-item {
    position: relative;
    padding-left: 12px; /* per level */
}
```

### Files to modify

| File | Changes |
|------|---------|
| [`www/post-feed.html`](www/post-feed.html) | Add level computation, depth-first sorting, vertical line rendering, collapse/expand |

---

## Phase 3: Zap Capability Indicators in the Feed

Show zap capability badges on each post's interaction bar so users can see at a glance whether the recipient can receive nutzaps, Lightning zaps, or both.

### Design

Compact indicators next to the existing zap button, using the project's CSS variables:

| Indicator | CSS | Meaning |
|-----------|-----|---------|
| 🥜 (accent color) | `color: var(--accent-color)` | Recipient can receive nutzaps — has kind 10019 with a shared mint |
| 🥜 (muted) | `color: var(--muted-color)` | Recipient has kind 10019 but no shared mint with your wallet |
| ⚡ (accent color) | `color: var(--accent-color)` | Recipient can receive Lightning zaps — has lud16 |
| ⚡ (muted) | `color: var(--muted-color)` | Recipient has no lud16 — cannot receive Lightning zaps |

Tooltip on hover shows details: "Can nutzap via mint.minibits.cash/Bitcoin" or "No shared mints — Lightning only" or "No zap capability".

### When and where to fetch the data

Two-tier strategy based on whether the author is a followed user or not:

**Tier 1: Followed users (proactive fetch on startup)**
- The worker already fetches the user's kind 3 (contact list) on startup (line 1799 of `ndk-worker.js`)
- After the kind 3 is loaded, proactively fetch kind 10019 for all followed pubkeys in a batch
- This happens once on startup, results cached in the worker's Dexie/IndexedDB
- Followed users are the most likely zap recipients, and their 10019 rarely changes
- Add a subscription for kind 10019 updates for followed pubkeys (so changes are picked up)

**Tier 2: Non-followed users (lazy fetch on first encounter)**
- When a post by a non-followed user appears in the feed, lazily fetch their kind 10019
- Use the existing `walletFetchMintList` worker function
- Cache the result per pubkey in an in-memory Map with TTL (5 minutes)
- Only fetch once per pubkey per session (unless cache expires)

**What's already available (no fetch needed):**
- lud16 (Lightning address) — already cached in `profile-cache.mjs` when post headers are rendered. The profile fetch already happens for every post in the feed.
- The sender's own wallet mints — already in the worker's `directProofStore`, accessible via `walletGetMints`

### Tasks

- [ ] **3.1** Create a `resolveZapCapabilities(pubkey)` function in [`www/js/zaps.mjs`](www/js/zaps.mjs) that checks:
  - Does the recipient have a lud16 (Lightning address)? → check `profile-cache.mjs` cache (already fetched for post rendering) → `canLightning: true`
  - Does the recipient have a kind 10019 (nutzap mint list)? → check local cache first, then fetch via `walletFetchMintList` if not cached
  - If yes to 10019, which mints? Do any overlap with the sender's wallet mints (from `walletGetMints`)? → `canNutzap: true`, `sharedMints: [...]`
  - Return `{ canLightning, canNutzap, sharedMints, nutzapMints, lud16 }`

- [ ] **3.2** Add a proactive kind 10019 batch fetch for followed users in the worker:
  - After the kind 3 (contact list) is loaded on startup, collect all followed pubkeys
  - Fetch kind 10019 for all followed pubkeys: `ndk.fetchEvents({ kinds: [10019], authors: [...followedPubkeys] })`
  - Cache the results in the worker's IndexedDB so they persist across sessions
  - Add a subscription for kind 10019 updates for followed pubkeys

- [ ] **3.3** For non-followed users, lazily fetch kind 10019 via `walletFetchMintList`:
  - Cache results in an in-memory Map with TTL (5 minutes) in `zaps.mjs`
  - Only fetch if not already in the worker's IndexedDB cache or the in-memory cache
  - Non-blocking: render the interaction bar without badges, then update when the fetch completes

- [ ] **3.4** Update the zap button rendering in `post-interactions.mjs` to show capability indicators:
  - Add a small badge element next to the zap button
  - Show 🥜 in `var(--accent-color)` if `canNutzap` is true
  - Show 🥜 in `var(--muted-color)` if recipient has 10019 but no shared mint
  - Show ⚡ in `var(--accent-color)` if `canLightning` is true
  - Add tooltip with shared mint details

- [ ] **3.5** Make the capability check async and non-blocking — render the interaction bar immediately, then update the badges when the capability check completes. The lud16 check is instant (from profile cache); the 10019 check may take a moment for non-followed users.

### Files to modify

| File | Changes |
|------|---------|
| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Add `resolveZapCapabilities()` with caching, lazy fetch for non-followed users |
| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add capability badges to zap button rendering |
| [`www/ndk-worker.js`](www/ndk-worker.js) | Add proactive kind 10019 batch fetch for followed users on startup |

---

## Phase 4: Zap Rail Selector in the Zap Dialog

When the user taps the zap button, show which rails are available and let them choose.

```mermaid
flowchart TD
    A[User taps zap button] --> B[Show zap dialog with amount input]
    B --> C{What rails are available?}
    C -->|Nutzap + Lightning| D[Show rail toggle: 🥜 Nutzap | ⚡ Lightning]
    C -->|Lightning only| E[Show Lightning only]
    C -->|Nutzap only| F[Show Nutzap only]
    C -->|Neither| G[Show error: recipient cannot receive zaps]
    D --> H[User selects rail + amount]
    H --> I{Rail selected}
    I -->|Nutzap| J[Send NIP-61 nutzap via walletSendNutzap]
    I -->|Lightning| K[Create LN invoice, melt Cashu via walletPayInvoice]
    E --> K
    F --> J
```

### Tasks

- [ ] **4.1** Update `promptZapDetails` in [`www/js/zaps.mjs`](www/js/zaps.mjs) to accept and display rail options:
  - If nutzap is available → show "🥜 Nutzap" option
  - If Lightning is available → show "⚡ Lightning" option
  - Let the user pick which rail to use via a toggle/selector
  - Selected rail uses `var(--accent-color)`, unselected uses `var(--muted-color)`

- [ ] **4.2** Show balance info in the dialog: "Cashu balance: 261 sats". If amount > balance → show warning in `var(--accent-color)`.

- [ ] **4.3** Default rail selection:
  - If nutzap is available and amount < 1000 sats → default to nutzap (lower fees)
  - If nutzap is available and amount ≥ 1000 sats → default to Lightning
  - If nutzap is not available → default to Lightning

- [ ] **4.4** Pass the selected rail back to `handleZapClick` in `post-interactions.mjs` so it uses the right send function.

- [ ] **4.5** Show shared mint info when nutzap is selected: "Will send via mint.minibits.cash/Bitcoin"

### Files to modify

| File | Changes |
|------|---------|
| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Update `promptZapDetails` with rail selector UI |
| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Update `handleZapClick` to respect user's rail choice |

---

## Phase 5: Balance Check and Error Handling

- [ ] **5.1** Before sending a zap, check the user's Cashu balance (via `walletGetBalance`):
  - If balance < zap amount → show "Insufficient balance. Current: X sats, needed: Y sats" in `var(--accent-color)`
  - If balance is sufficient but barely → show confirmation "This will use most of your balance. Continue?"

- [ ] **5.2** Improve error messages for melt failures:
  - "Mint couldn't route Lightning payment" (mint liquidity issue)
  - "Mint returned an error: [details]" (mint API error)
  - "Proofs were spent but payment failed — try again or contact the mint"

- [ ] **5.3** After a successful zap, refresh the balance display.

### Files to modify

| File | Changes |
|------|---------|
| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add balance check before zap, improve error handling |

---

## Implementation Order

1. **Phase 1** (simplified feed) — remove inline replies from feed2.html, comment button opens post-feed.html in new tab
2. **Phase 2** (post thread page) — create post-feed.html to show full post + comment thread
3. **Phase 3** (zap capability indicators) — show what the recipient can receive
4. **Phase 4** (rail selector) — let users choose nutzap vs Lightning melt
5. **Phase 5** (balance + errors) — pre-flight checks and better error messages

Phases 1-2 are the feed restructuring. Phases 3-5 are zap improvements.
