Files
client/plans/post-interactions2-plan.md
T
2026-04-17 16:52:51 -04:00

267 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |