12 KiB
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:
- Initialize the module with NDK dependencies
- Call
createCard(event)to get a DOM element - 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.
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.
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:
- Fetches existing interactions from relays
- Subscribes to live interaction updates
- 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).
// 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>:
{
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 allndkEventlisteners 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:
- If
mountComposerwas provided → create inline composer (rich, with upload/preview) - If not → create simple
contenteditableinput box (fallback) - Comments render as compact cards (recursive
createCard()withisCompact: 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 logicupdateInteractionBar()DOM update logicaddCommentToPost()comment append logicrenderCommentThread()/renderCommentItem()recursive card renderinghtmlFormatText()/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:
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 |