267 lines
12 KiB
Markdown
267 lines
12 KiB
Markdown
# 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 |
|