# post-interactions2.mjs — Self-Contained PostCard Module

## Goal

Create a new `post-interactions2.mjs` that provides a **fully self-contained PostCard** object. A page creates a card, appends it to the DOM, and the card handles everything else: profile resolution, interaction counts, live subscriptions, comment threads, inline reply composers, and click handlers.

The page's only job is:
1. Initialize the module with NDK dependencies
2. Call `createCard(event)` to get a DOM element
3. Append it to a container

No interaction wiring callbacks. No copy-pasted `onUpdate` handlers. No page-level state management for interactions.

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│  feed.html (or any page)                                │
│                                                         │
│  const cards = initPostCards({ subscribe, publishEvent, │
│    fetchEventsFromAllRelays, ... });                     │
│                                                         │
│  const el = cards.createCard(event, { currentPubkey }); │
│  divFeed.appendChild(el);                               │
│                                                         │
│  // That's it. Card is now self-updating.               │
└─────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│  post-interactions2.mjs                                 │
│                                                         │
│  initPostCards(deps) → CardFactory                      │
│    ├── createCard(event, opts) → HTMLElement             │
│    │     ├── renderAuthorHeader(pubkey)                  │
│    │     ├── renderContent(content)                      │
│    │     ├── renderFooterRow(id, event)                  │
│    │     │     ├── Zap / Like / Comment / Quote buttons  │
│    │     │     └── Time-ago display                      │
│    │     └── auto-wires interactions on creation         │
│    │                                                     │
│    ├── createCompactCard(event, opts) → HTMLElement      │
│    │     (same as createCard but compact styling)        │
│    │                                                     │
│    ├── wireInteractions(postIds)                         │
│    │     (batch-wire for multiple cards at once)         │
│    │                                                     │
│    ├── getCommentsVisible() / setCommentsVisible()       │
│    └── destroy()                                         │
│          (cleanup all subscriptions and listeners)       │
└─────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│  Shared dependencies (unchanged)                        │
│  ├── profile-cache.mjs                                  │
│  ├── utilities.mjs (htmlFormatText)                     │
│  ├── init-ndk.mjs (subscribe, publishEvent, etc.)       │
│  └── post-composer.mjs (mountComposer — optional)       │
└─────────────────────────────────────────────────────────┘
```

## API Design

### `initPostCards(deps)` → CardFactory

Initialize the module. Called once per page.

```javascript
import { initPostCards } from './js/post-interactions2.mjs';

const cards = initPostCards({
  // Required — NDK functions
  subscribe,
  publishEvent,
  fetchEventsFromAllRelays,
  
  // Required — for profile resolution
  fetchCachedProfile,
  storeProfile,
  
  // Optional — for inline reply composers
  mountComposer,          // from post-composer.mjs
  
  // Optional — query local cache
  queryCache
});
```

### `cards.createCard(event, opts)` → HTMLElement

Create a self-contained post card. Returns a `.divPostItem` element.

```javascript
const el = cards.createCard(event, {
  currentPubkey,          // Required — for "you liked" state
  showHeader: true,       // Show author avatar + name (default: true)
  autoWire: true,         // Auto-fetch + subscribe interactions (default: true)
  enableComposer: true,   // Enable inline reply composer (default: true if mountComposer provided)
});
divFeed.appendChild(el);
```

When `autoWire: true` (default), the card immediately:
1. Fetches existing interactions from relays
2. Subscribes to live interaction updates
3. Updates its own DOM when new likes/comments/quotes/zaps arrive

### `cards.wireInteractions(postIds, opts)` → void

Batch-wire interactions for multiple cards at once (more efficient than per-card auto-wiring for initial page loads).

```javascript
// After appending multiple cards
const postIds = events.map(e => e.id);
cards.wireInteractions(postIds, { currentPubkey });
```

### `cards.destroy()` → void

Cleanup all subscriptions, event listeners, and timers. Call on page unload.

## Internal Architecture

### State Management

Per-card state stored in a module-level `Map<postId, CardState>`:

```javascript
{
  likes: new Set(),       // pubkeys
  quotes: new Set(),      // event IDs
  reposts: new Set(),     // pubkeys
  comments: [],           // comment event objects
  zaps: [],               // zap receipt objects
  zapTotal: 0,
  userLiked: false,
  userQuoted: false,
  userReposted: false,
  commentCount: 0,
  quoteCount: 0
}
```

This is identical to the current `post-interactions.mjs` state shape — no changes needed.

### Subscription Management

The module manages its own subscription lifecycle:

- **Batched subscriptions**: Instead of one subscription per card, the module batches post IDs and creates a single subscription for all active cards. Re-batches when new cards are added.
- **Dedup/expiry**: Built-in tracking to prevent duplicate subscriptions (replaces `trackSubscription()` from post.html).
- **Cleanup**: `destroy()` removes all `ndkEvent` listeners and clears state.

### Profile Resolution

Uses `createProfileCache()` from `profile-cache.mjs` (same as current). Configured during `initPostCards()` with the NDK functions.

### Comment Threading

When the Comment button is clicked:
1. If `mountComposer` was provided → create inline composer (rich, with upload/preview)
2. If not → create simple `contenteditable` input box (fallback)
3. Comments render as compact cards (recursive `createCard()` with `isCompact: true`)

### CSS Dependencies

The card relies on these classes from `client.css`:

**Already in client.css:**
- `.divPostHeader`, `.divPostAvatar`, `.divPostAuthorName` (author header)
- `.divPostInteractions`, `.interaction-item`, `.interaction-icon`, `.interaction-count` (footer bar)
- `.divCommentThread`, `.divCommentsList`, `.divCommentInputBox`, `.comment-input`, `.comment-submit` (comments)
- `.nostr-mention`, `.nostr-embed`, `.nostr-profile-*` (entity rendering)
- `.avatar`, `.avatar--sm`, `.avatar--md` (avatar sizing)

**Must be added to client.css:**
- `.divPostItem` — the card container
- `.divPostContent` — post text content
- `.divPostFooterRow` — footer row wrapper
- `.divPostTime` — time-ago display
- `.divPostContent a` — link styling within posts
- `.divPostContent img` — image styling within posts
- `.divPostItem.is-read` / `.divPostItem.is-unread` — read/unread states (for viewed mode later)

**No inline CSS in any page for card rendering.**

## What Changes vs. Current post-interactions.mjs

| Aspect | Current (v1) | New (v2) |
|--------|-------------|----------|
| Wiring | Page must call `fetchExistingInteractions()` + `subscribeToInteractions()` with callback | Card auto-wires on creation, or batch via `wireInteractions()` |
| Callback | Page writes `onUpdate` callback (copy-pasted 3×) | Module owns the callback internally |
| Inline composer | Page owns `getOrCreateInlineComposer()` (~70 lines) | Module owns it, uses `mountComposer` dep |
| Subscription dedup | Page owns `trackSubscription()` (~50 lines) | Module owns it internally |
| CSS | Mix of client.css + inline page styles | All in client.css |
| Comments toggle | Page wires button to `setCommentsVisible()` | Module exposes same API, page just calls it |
| Time-ago updates | Module runs `setInterval(updateTimeAgos, 30000)` | Same, but cleanup in `destroy()` |
| Profile cache | Two separate instances (page + module) | Single instance, configured once |

## What Stays the Same

- `renderPostItem()` DOM structure (`.divPostItem` > `.divPostHeader` + `.divPostContent` + `.divPostFooterRow`)
- `processInteractionEvent()` state update logic
- `updateInteractionBar()` DOM update logic
- `addCommentToPost()` comment append logic
- `renderCommentThread()` / `renderCommentItem()` recursive card rendering
- `htmlFormatText()` / `hydrateNostrEntities()` content formatting
- All click handlers (like, comment, quote, zap)
- Profile fetch + apply pattern
- Time-ago formatting and registration

## feed.html Integration

The page code in `feed.html` will be minimal:

```javascript
import { initPostCards } from './js/post-interactions2.mjs';
import { initNDKPage, getPubkey, subscribe, publishEvent, 
         fetchEventsFromAllRelays, fetchCachedProfile, 
         storeProfile, queryCache } from './js/init-ndk.mjs';
import { mountComposer } from './js/post-composer.mjs';

// After auth...
const cards = initPostCards({
  subscribe, publishEvent, fetchEventsFromAllRelays,
  fetchCachedProfile, storeProfile, queryCache, mountComposer
});

// Get follows list
const contactEvents = await queryCache({ kinds: [3], authors: [currentPubkey], limit: 1 });
const followedPubkeys = contactEvents[0]?.tags
  ?.filter(t => t[0] === 'p')
  .map(t => t[1]) || [];

// Subscribe to posts from follows
subscribe({ kinds: [1], authors: followedPubkeys, limit: 20 },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });

// Render cards as events arrive
window.addEventListener('ndkEvent', (e) => {
  const evt = e.detail;
  if (evt.kind === 1 && !renderedIds.has(evt.id)) {
    renderedIds.add(evt.id);
    const el = cards.createCard(evt, { currentPubkey });
    divFeed.appendChild(el);
  }
  if (evt.kind === 3 && evt.pubkey === currentPubkey) {
    // Update follows list...
  }
});
```

~30 lines of page-specific logic. Everything else is in the module.

## File Inventory

| File | Action |
|------|--------|
| `www/js/post-interactions2.mjs` | **CREATE** — new self-contained module |
| `www/css/client.css` | **MODIFY** — add card component styles, remove duplicates |
| `www/feed.html` | **MODIFY** — wire up as first consumer |
| `www/js/post-interactions.mjs` | **NO CHANGE** — existing pages keep working |
| `www/js/post-composer.mjs` | **NO CHANGE** — used as optional dependency |
| `www/js/profile-cache.mjs` | **NO CHANGE** — used as dependency |
| `www/js/utilities.mjs` | **NO CHANGE** — used as dependency |
