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

12 KiB
Raw Permalink Blame History

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.

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:

  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).

// 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 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:

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