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

10 KiB
Raw Permalink Blame History

Post Page Social Interactions Plan (Revised)

Overview

Add likes, reposts, comments, zaps display, inline comment threads, and real-time "time ago" updates to www/post.html. Build as a reusable module so www/feed.html (future) can share the same interaction bar and comment rendering.

The interaction icons will use HamburgerMorphing instances with new custom shapes (heart, repost-arrows, speech-bubble, lightning-bolt), giving the satisfying animated morph effect when users click to like/repost/zap/comment.

Architecture Overview

flowchart TD
    subgraph Reusable Module
        A[post-interactions.mjs] --> B[renderInteractionBar]
        A --> C[subscribeToInteractions]
        A --> D[handleInteractionEvent]
        A --> E[renderCommentThread]
        A --> F[publishReaction]
        A --> G[publishRepost]
        A --> H[publishComment]
    end

    subgraph Pages
        P1[post.html] --> A
        P2[feed.html - future] --> A
    end

    subgraph HamburgerMorphing
        HM[hamburger.mjs] --> S1[heart shape - new]
        HM --> S2[repost shape - new]
        HM --> S3[comment shape - new]
        HM --> S4[bolt shape - new]
    end

    A --> HM
    A --> NDK[init-ndk.mjs subscribe + publishEvent]

Making HamburgerMorphing Work for Repeated Icons

The Problem

Each HamburgerMorphing instance creates an SVG.js drawing with 3 animated lines. With 20 posts × 4 icons = 80 instances, this could be heavy.

The Solution: Lazy Instantiation + Viewport Optimization

  1. Render static SVG initially — On first render, each interaction icon is a lightweight static SVG string (no SVG.js, no animation). This is just an innerHTML set.

  2. Instantiate HamburgerMorphing on first interaction — When a user clicks an icon (like, repost, etc.), we create the HamburgerMorphing instance for that specific icon only, animate the morph (e.g., from empty heart to filled heart), then optionally destroy it after the animation completes and replace with the new static SVG state.

  3. Result: At most 1-2 HamburgerMorphing instances alive at any time, not 80.

This gives us the satisfying morph animation on click without the overhead of 80 persistent instances.

Implementation Pattern

function onInteractionClick(iconContainer, fromShape, toShape) {
    // Create temporary HamburgerMorphing instance
    const hm = new HamburgerMorphing(iconContainer, {
        size: 20,
        foreground: 'var(--muted-color)',
        background: 'var(--secondary-color)',
        hover: 'var(--accent-color)'
    });
    
    // Start at the from shape, then animate to the to shape
    hm.animateTo(fromShape);
    requestAnimationFrame(() => {
        hm.animateTo(toShape);
    });
    
    // After animation completes, destroy instance and set static SVG
    setTimeout(() => {
        const staticSvg = hm.getSvgCode();
        hm.destroy();
        iconContainer.innerHTML = staticSvg;
    }, 600); // slightly longer than ANIMATION_DURATION of 500ms
}

New HamburgerMorphing Shapes

Add 4 new shapes to www/hamburger_morphing/hamburger.mjs in the animateTo() switch statement:

Shape Name Visual 3-Line Approximation
heart ❤️ Line1: left-top diagonal down-right; Line2: right-top diagonal down-left; Line3: bottom V-shape
heart_filled Filled ❤️ Same as heart but with thick stroke to appear solid
repost_arrows 🔄 Line1: top horizontal arrow-right; Line2: connecting diagonal; Line3: bottom horizontal arrow-left
comment_bubble 💬 Line1: top arc; Line2: bottom with tail; Line3: side
bolt Line1: top-right to center-left; Line2: center horizontal; Line3: center-right to bottom-left

Nostr Event Kinds

Interaction Kind NIP Filter Pattern
Reaction/Like 7 NIP-25 { kinds: [7], #e: [postIds] }
Repost 6 NIP-18 { kinds: [6], #e: [postIds] }
Reply/Comment 1 NIP-10 { kinds: [1], #e: [postIds] }
Zap Receipt 9735 NIP-57 { kinds: [9735], #e: [postIds] }

Reusable Module: www/js/post-interactions.mjs

Exports

// Initialize interaction tracking for a set of posts
export function initInteractions(posts, currentPubkey, options)

// Render the interaction bar HTML for a single post
export function renderInteractionBar(postId, counts)

// Render comment thread under a post
export function renderCommentThread(postId, comments)

// Subscribe to all interaction events for given post IDs
export function subscribeToInteractions(postIds, onUpdate)

// Publish a like reaction
export function publishLike(postId, postPubkey)

// Publish a repost
export function publishRepost(postId, postPubkey, originalEvent)

// Publish a comment/reply
export function publishComment(postId, postPubkey, content)

// Update all time-ago displays on the page
export function updateTimeAgos()

// Get static SVG string for an interaction icon
export function getIconSvg(type, active)

Data Structure

// Per-post interaction state
const interactionState = new Map();
// Key: postId
// Value: {
//   likes: Set of pubkeys,
//   reposts: Set of pubkeys,
//   comments: Array of { id, pubkey, content, created_at },
//   zaps: Array of { pubkey, amount, comment },
//   zapTotal: number (in sats),
//   userLiked: boolean,
//   userReposted: boolean
// }

Comment Thread Display

When a post has comments, they appear below the interaction bar in a collapsible section:

flowchart TD
    subgraph Post Card
        Content[Post Content]
        Footer[Time Ago]
        Bar[Interaction Bar: heart 3 | comment 2 | repost 1 | bolt 500]
        Toggle[Click comment icon to expand]
        Thread[Comment Thread]
        Reply[Reply Input - contenteditable div]
    end
    
    Content --> Footer --> Bar --> Toggle --> Thread --> Reply
  • Comments are sorted by created_at ascending
  • Each comment shows: content + time ago + its own mini interaction bar (like only)
  • Reply input appears at the bottom — press Enter to post reply (same UX as the main post box)
  • Comments on comments (nested replies) are shown flat with indentation based on the reply chain depth (max 2 levels)

Detailed Implementation Steps

Step 1: Add new shapes to HamburgerMorphing

Add heart, heart_filled, repost_arrows, comment_bubble, and bolt cases to the animateTo() switch statement in www/hamburger_morphing/hamburger.mjs.

Step 2: Create www/js/post-interactions.mjs

The reusable module containing:

  • Static SVG icon generation functions
  • Interaction state management (Map of post ID to counts/sets)
  • Subscription management (subscribe to kinds 7, 6, 1, 9735 with #e filter)
  • Event handlers that update state and trigger re-renders
  • Publish functions for like, repost, comment
  • Comment thread rendering
  • Time-ago update function

Step 3: Create www/css/post-interactions.css (or add to client.css)

Styles for:

  • .divPostInteractions — horizontal flex row, gap between items
  • .interaction-item — inline-flex, align-items center, cursor pointer
  • .interaction-item.active — accent color
  • .interaction-count — small font, muted color
  • .divCommentThread — indented, border-left
  • .divCommentItem — individual comment styling
  • .divCommentReply — reply input box styling

Step 4: Modify www/post.html to use the module

  • Import post-interactions.mjs
  • After posts render, call initInteractions(posts, currentPubkey)
  • Modify renderFeed() to include interaction bar HTML
  • Add event delegation for interaction clicks
  • Set up 30-second interval for updateTimeAgos()
  • Handle new interaction events from the ndkEvent listener

Step 5: Wire up comment thread

  • Clicking the comment icon toggles the comment thread visibility
  • Comment thread shows existing replies
  • Reply input at bottom of thread
  • Enter to submit reply (Shift+Enter for newline, same as main post)
  • New replies appear in real-time via the subscription

Step 6: Wire up all interactive actions

  • Like: Click heart → morph animation → publish kind 7 → update count
  • Repost: Click repost → morph animation → publish kind 6 → update count
  • Comment: Click comment → expand thread → focus reply input
  • Zap: Click bolt → morph animation → (future: integrate with wallet/NWC)

Real-Time Time Ago

// In post-interactions.mjs
export function updateTimeAgos() {
    document.querySelectorAll('[data-timestamp]').forEach(el => {
        const ts = parseInt(el.dataset.timestamp);
        if (!isNaN(ts)) {
            el.textContent = formatTimeAgo(ts);
        }
    });
}

// Called from page every 30 seconds
setInterval(updateTimeAgos, 30000);

The formatTimeAgo() function already exists in post.html — it should be moved into the module or into www/js/utilities.mjs for reuse.

File Changes Summary

File Action Description
www/hamburger_morphing/hamburger.mjs Modify Add heart, heart_filled, repost_arrows, comment_bubble, bolt shapes
www/js/post-interactions.mjs Create Reusable interaction bar module
www/post.html Modify Import module, modify renderFeed, add interaction handlers, add time-ago interval
www/css/client.css Modify Add interaction bar and comment thread styles
www/js/utilities.mjs Modify Move formatTimeAgo here for reuse

Future: feed.html

The same post-interactions.mjs module will be imported by feed.html with identical usage:

import { initInteractions, updateTimeAgos } from './js/post-interactions.mjs';

// After rendering feed posts...
initInteractions(posts, currentPubkey);
setInterval(updateTimeAgos, 30000);

Design Principles

  • Minimal: Small icons + numbers, no text labels
  • Animated: HamburgerMorphing morph effect on click (lazy instantiation keeps it lightweight)
  • Reusable: Module works for post.html, feed.html, and any future page with posts
  • Real-time: Counts update as events arrive; timestamps update every 30s
  • Threaded: Comments appear inline under posts with reply capability
  • Recursive: Comments on comments supported (flat with indentation, max 2 levels)