# Event Rendering System - Comprehensive Analysis & Improvement Plan **Date**: 2025-12-11 **Context**: Grimoire Nostr Protocol Explorer **Scope**: Deep architectural analysis of event rendering system covering 150+ registered event kinds with ~20 custom renderers --- ## Executive Summary The current event rendering system has a **solid foundation** with good architectural patterns (registry-based routing, component reuse, type safety), but suffers from **inconsistencies in application** and **missing abstractions** that limit scalability, maintainability, and extensibility. **Key Findings:** - ✅ **Strengths**: Registry pattern, BaseEventContainer, applesauce integration, type safety - ❌ **Critical Issues**: Hardcoded detail renderers, inconsistent depth tracking, no error boundaries, missing threading abstraction - 🎯 **Opportunity**: Transform from "working prototype" to "production-grade framework" with systematic improvements --- ## Part 1: Current State Analysis ### Architecture Overview ``` Current System Layers: ┌─────────────────────────────────────┐ │ Renderer Layer │ Kind1Renderer, Kind6Renderer, etc. │ (~20 custom, 130+ using default) │ ├─────────────────────────────────────┤ │ Component Layer │ BaseEventContainer, EventAuthor, EventMenu │ (Reusable UI components) │ RichText, EmbeddedEvent, MediaEmbed ├─────────────────────────────────────┤ │ Registry Layer │ kindRenderers map, KindRenderer router │ (Routing & fallback) │ DefaultKindRenderer ├─────────────────────────────────────┤ │ Data Layer │ EventStore (applesauce), useNostrEvent hook │ (Reactive state) │ RelayPool, Dexie cache └─────────────────────────────────────┘ ``` ### Event Kind Categories Analysis of 150+ registered kinds reveals **7 fundamental patterns**: 1. **Content-Primary** (1, 30023, 9802) - Main payload in `content` field - Rich text rendering, markdown, media embeds - Examples: Notes, articles, highlights 2. **Reference Events** (6, 7, 9735) - Point to other events via e/a tags - Embed referenced content - Examples: Reposts, reactions, zaps 3. **Metadata Events** (0, 3, 10002) - Structured data in content JSON - Key-value pairs, lists, configurations - Examples: Profiles, contacts, relay lists 4. **List Events** (30000-39999 replaceable) - Arrays of items in tags - Follow sets, mute lists, bookmarks - Addressable/replaceable nature 5. **Media Events** (20, 21, 22, 1063) - Content is URLs with metadata - Images, videos, files - Thumbnails, dimensions, MIME types 6. **Action Events** (5, 1984) - Represent operations on other events - Deletions, reports, moderation - Usually invisible to end users 7. **Communication Events** (4, 14, 1111) - Threaded messaging - DMs, comments, chat messages - Multiple threading models (NIP-10, NIP-22, NIP-28) --- ## Part 2: What's Common to All Events ### Universal Requirements Every event, regardless of kind, needs: 1. **Author Context** - WHO created this - Profile info (name, avatar, NIP-05) - Clickable to open profile - Badge/verification indicators 2. **Temporal Context** - WHEN was this created - Relative timestamps ("2h ago") - Absolute time on hover (ISO format) - Locale-aware formatting 3. **Event Identity** - WHAT is this - Kind badge with icon and name - Event ID (bech32 format: nevent/naddr) - Copy/share capabilities 4. **Actions** - User operations - Open in detail view - Copy event ID - View raw JSON - (Future: Reply, React, Zap, Share) 5. **Relay Context** - WHERE was this seen - List of relays that served the event - Relay health indicators - Relay preferences for publishing 6. **Addressability** - HOW to reference - Regular events: nevent (id + relays + author) - Addressable events: naddr (kind + pubkey + identifier + relays) - note1 (deprecated but still supported) ### Current Implementation **Well-Handled:** - ✅ Author, Temporal, Identity, Actions (1-4) via `BaseEventContainer` + `EventMenu` - ✅ Addressability (6) logic in EventDetailViewer **Missing Universally:** - ❌ Signature verification indicator - ❌ Edit history (NIP-09 deletion event tracking) - ❌ Engagement preview (reply count, zap total, reaction summary) - ❌ Related events indicator - ❌ Community/context badges (NIP-72 communities, NIP-29 groups) **Recommendation:** Extend `BaseEventContainer` with optional engagement footer and verification indicator. --- ## Part 3: Rendering Context Analysis ### Three Primary Contexts 1. **Feed/Timeline** - Compact, scannable view - Emphasis on density, quick scanning - Show summary/preview, not full content - Inline media thumbnails - Minimal interaction chrome 2. **Detail** - Expansive, full-content view - Emphasis on readability, completeness - Full markdown rendering, full-size media - Show relationships (replies, zaps, reactions) - Additional metadata and actions 3. **Embedded** - Nested preview within another event - Context-aware depth limiting - Minimal chrome (no duplicate headers if already in context) - Click to expand/navigate - Performance-conscious (lazy load) ### Current Implementation **Feed Rendering:** - ✅ Works well with `KindRenderer` + `BaseEventContainer` - ✅ Consistent pattern across all kinds - ⚠️ No virtualization for performance **Detail Rendering:** - ❌ **CRITICAL**: Hardcoded switch statement in `EventDetailViewer.tsx`: ```tsx event.kind === kinds.Metadata ? : event.kind === kinds.Contacts ? : event.kind === kinds.LongFormArticle ? : ``` - ❌ Breaks registry pattern - not extensible - ❌ Only 5 kinds have detail renderers, rest fallback to feed **Embedded Rendering:** - ⚠️ Uses same as feed (via `EmbeddedEvent` → `KindRenderer`) - ⚠️ No context awareness - ⚠️ Depth tracking inconsistent ### Recommended Architecture **Unified Registry Pattern:** ```tsx // Proposed structure export const kindRenderers: KindRendererRegistry = { 1: { feed: Kind1Renderer, detail: Kind1DetailRenderer, // optional, fallback to feed embed: Kind1EmbedRenderer, // optional, fallback to feed }, // Or simplified: 1: Kind1Renderer, // if no variants needed 30023: { feed: Kind30023Renderer, // Compact: title + summary detail: Kind30023DetailRenderer, // Full markdown + relationships } }; // Usage function KindRenderer({ event, context = 'feed' }) { const registry = kindRenderers[event.kind]; const Renderer = registry?.[context] || registry?.feed || registry || DefaultKindRenderer; return ; } ``` **Benefits:** - ✅ Consistent pattern for all contexts - ✅ Extensible - add detail renderers without modifying router - ✅ Self-documenting - registry shows available variants - ✅ Type-safe - validate registry at compile time --- ## Part 4: Depth Tracking & Nesting ### The Problem Events can reference other events infinitely: - Kind 6 (repost) of Kind 6 of Kind 6... → infinite loop - Kind 1 (note) replying to Kind 1 replying to Kind 1... → deep nesting - Kind 9735 (zap) of article containing zaps... → exponential expansion ### Current State - ✅ `Kind1Renderer` passes `depth` to `RichText` - ✅ `RichText` uses depth to limit nesting - ❌ `Kind6Renderer` (repost) doesn't track depth → infinite loop possible - ❌ `Kind9735Renderer` (zap) embeds without depth → can nest infinitely - ❌ `EmbeddedEvent` doesn't auto-increment depth ### Solution: Systematic Depth Management ```tsx // 1. Universal depth constant export const MAX_EMBED_DEPTH = 3; // 2. All renderers receive and honor depth export interface BaseEventProps { event: NostrEvent; depth?: number; context?: 'feed' | 'detail' | 'embed'; } // 3. EmbeddedEvent auto-increments export function EmbeddedEvent({ eventId, depth = 0, ...props }) { const event = useNostrEvent(eventId); if (!event) return ; if (depth >= MAX_EMBED_DEPTH) { return ; } return ; } // 4. Depth-aware rendering export function Kind6Renderer({ event, depth = 0 }) { if (depth >= MAX_EMBED_DEPTH) { return
Repost of
; } return
Reposted
; } ``` **Benefits:** - ✅ Prevents infinite loops - ✅ Improves performance (limits cascade fetching) - ✅ Better UX (collapsed deep threads with expand option) - ✅ Consistent behavior across all renderers --- ## Part 5: Threading & Reply Abstraction ### The Challenge Multiple threading models exist in Nostr: 1. **NIP-10** (Kind 1 notes) - `e` tags with markers: `["e", id, relay, "root"|"reply"]` - Root = original post, Reply = immediate parent - Mentions = other referenced events 2. **NIP-22** (Kind 1111 comments) - **Uppercase tags** = root scope: `K`, `E`, `A`, `I`, `P` - **Lowercase tags** = parent item: `k`, `e`, `a`, `i`, `p` - Can thread on events OR external identifiers (URLs, podcasts, etc.) - MUST NOT reply to kind 1 notes (use kind 1 instead) 3. **NIP-28** (Kind 42 channel messages) - Replies within channel context - Different tag structure 4. **NIP-29** (Kinds 10, 11, 12 group messages) - Group-specific threading - Additional permissions layer ### Current Implementation - ✅ Kind1Renderer shows NIP-10 reply indicator - ✅ Uses `getNip10References` from applesauce - ❌ No support for NIP-22 (Kind 1111 not implemented) - ❌ No support for other threading models - ❌ No generic threading components ### Proposed Abstraction **Helper Layer:** ```tsx // src/lib/threading.ts export interface ThreadReference { type: 'nip10' | 'nip22' | 'nip28' | 'nip29'; root?: EventPointer | AddressPointer | string; // string for external (NIP-22) parent?: EventPointer | AddressPointer | string; mentions?: Array; rootAuthor?: string; parentAuthor?: string; } export function getThreadReferences(event: NostrEvent): ThreadReference | null { // Detect threading model by kind and tags if (event.kind === 1) return getNip10Thread(event); if (event.kind === 1111) return getNip22Thread(event); if (event.kind === 42) return getNip28Thread(event); // ... etc return null; } ``` **Component Layer:** ```tsx // Generic thread indicator export function ThreadIndicator({ event, depth = 0 }) { const refs = getThreadReferences(event); if (!refs) return null; const parentEvent = useNostrEvent(refs.parent); return (
Replying to {parentEvent ? ( ) : refs.type === 'nip22' && typeof refs.parent === 'string' ? ( ) : ( )}
); } // Generic thread tree (for detail view) export function ThreadTree({ rootEvent }) { const replies = useReplies(rootEvent); return (
{replies.map(reply => ( ))}
); } ``` **Benefits:** - ✅ Single component works across all threading models - ✅ Extensible to new NIPs - ✅ Reusable across different renderers - ✅ Consistent UX for users - ✅ Easier to maintain (one place to fix threading bugs) --- ## Part 6: Metadata Extraction Patterns ### Current Approaches 1. **Applesauce Helpers** ✅ (GOOD) - `getArticleTitle`, `getZapAmount`, `getNip10References` - Well-tested, consistent, handles edge cases - Examples: `Kind30023Renderer`, `Kind9735Renderer` 2. **Manual Tag Parsing** ⚠️ (INCONSISTENT) - `event.tags.find(t => t[0] === "e")` - Error-prone, repeated code, misses edge cases - Examples: `Kind6Renderer`, various places 3. **JSON Parsing** ❌ (ERROR-PRONE) - `JSON.parse(event.content)` without try/catch - Can crash entire app if malformed - Examples: Profile metadata, relay lists ### What Applesauce Provides Currently has helpers for: - ✅ Articles (30023): title, summary, published, image - ✅ Zaps (9735): amount, sender, request, pointers - ✅ Threading (1): NIP-10 references - ✅ Profiles (0): metadata parsing - ✅ Relays: seen relays, relay hints ### What's Missing Need helpers for: - ❌ File metadata (1063): url, hash, size, mime, dimensions - ❌ Media events (20, 21, 22): URLs, thumbnails, dimensions - ❌ List events (30000+): systematic list item extraction - ❌ Comments (1111): NIP-22 uppercase/lowercase tag parsing - ❌ Reactions (7): emoji normalization (+ → ❤️) - ❌ Reposts (6, 16, 18): reposted event extraction - ❌ Highlights (9802): context, highlight text - ❌ Calendar events (31922-31925): date/time parsing - ❌ Polls (1068): options, votes, tally - ❌ Communities (34550): community info extraction ### Recommendation **Architecture Principle:** Renderers should NEVER parse tags/content directly. ```tsx // BAD ❌ const eTag = event.tags.find(t => t[0] === "e")?.[1]; // GOOD ✅ import { getRepostedEvent } from '@/lib/helpers/repost'; const repostPointer = getRepostedEvent(event); ``` **Action Items:** 1. Contribute missing helpers to applesauce-core (if generic) 2. Create local helper library for Grimoire-specific needs 3. Audit all renderers, replace manual parsing with helpers 4. Enforce via ESLint rule: no direct `event.tags.find` --- ## Part 7: Performance & Scalability ### Current Bottlenecks 1. **No Virtualization** - All events in feed render immediately - 1000 events = 1000 DOM nodes = slow scroll - Wastes memory on off-screen content 2. **No Memoization** - RichText parses content on every render - Profile lookups happen repeatedly - JSON.parse re-runs unnecessarily - Expensive computations not cached 3. **No Lazy Loading** - All renderer code loaded upfront - ~20 renderer components = large initial bundle - Could code-split by kind 4. **Heavy Base Components** - Every event has `BaseEventContainer` overhead - Profile fetch for every `EventAuthor` - Could batch profile fetches 5. **Cascading Fetches** - Embedded event triggers fetch - That event might embed another - Exponential growth without depth limiting ### Good News - ✅ EventStore handles deduplication - ✅ Dexie provides offline caching - ✅ Reactive system (RxJS) is efficient ### Solutions **1. Virtual Scrolling** ```tsx import { Virtuoso } from 'react-virtuoso'; function EventFeed({ events }) { return ( ( )} /> ); } ``` **2. Memoization** ```tsx // Wrap all renderers export const Kind1Renderer = React.memo(({ event, depth }) => { const refs = useMemo(() => getNip10References(event), [event.id]); const handleClick = useCallback(() => {...}, [event.id]); return ; }); ``` **3. Code Splitting** ```tsx // Lazy load detail renderers const Kind30023DetailRenderer = lazy(() => import('./kinds/Kind30023DetailRenderer') ); // Use with Suspense }> ``` **4. Batch Profile Fetches** ```tsx // Instead of individual useProfile in every EventAuthor // Batch load all visible profiles function EventFeed({ events }) { const pubkeys = useMemo(() => events.map(e => e.pubkey), [events] ); useBatchProfiles(pubkeys); // Prefetch return events.map(e => ); } ``` **Performance Targets:** - Feed with 10,000 events: Smooth 60fps scroll - Initial render: < 100ms - Event interaction: < 50ms response - Bundle size: < 300KB for core, lazy load rest --- ## Part 8: Error Handling & Resilience ### Current Error Scenarios 1. **Malformed Events** - Invalid JSON in content - Missing required tags - Incorrect tag structure 2. **Network Failures** - Relays timeout - Event not found - Incomplete data 3. **Parsing Failures** - Markdown rendering errors - NIP-19 decode failures - Media load failures 4. **Rendering Errors** - Component crashes - Infinite loops (depth issue) - Out of memory ### Current Handling - ⚠️ Some try/catch in parsers (inconsistent) - ⚠️ EmbeddedEvent shows "Loading..." forever if fetch fails - ❌ No error boundaries around renderers - ✅ DefaultKindRenderer for unknown kinds (good!) ### Solution: Error Boundaries ```tsx // Per-event error boundary export function EventErrorBoundary({ children, event }) { return ( (

Failed to render event

Kind {event.kind} • {event.id.slice(0, 8)}

Error details
{error.message}
)} > {children}
); } // Usage in feed {events.map(event => ( ))} ``` **Benefits:** - ✅ One broken event doesn't break entire feed - ✅ User gets actionable error info - ✅ Developer gets diagnostics - ✅ Graceful degradation --- ## Part 9: Accessibility & Internationalization ### Accessibility Gaps **Keyboard Navigation:** - ❌ No keyboard shortcuts for common actions - ❌ Can't navigate between events with Tab - ❌ Can't expand/collapse without mouse **Screen Reader Support:** - ❌ EventMenu has no aria-label - ❌ Embedded events don't announce properly - ❌ Time stamps are "2h ago" but no absolute time for SR - ⚠️ BaseEventContainer uses `
` not `
` **Visual:** - ⚠️ Muted text might not meet WCAG AA contrast - ❌ No prefers-reduced-motion support - ❌ Focus indicators inconsistent **RTL Support:** - ❌ Noted in TODO as partially implemented - ❌ Inline elements conflict with RTL alignment ### I18n Gaps - ⚠️ Timestamps use locale from state but inconsistently - ❌ Number formatting hardcoded to "en" (zap amounts) - ❌ Kind names are English-only strings - ❌ Error messages hardcoded English - ❌ No language detection for content ### Solutions **Semantic HTML:** ```tsx export function BaseEventContainer({ event, children }) { return (
{children}
); } ``` **Keyboard Navigation:** ```tsx // Arrow keys to navigate events // Enter to open detail // Escape to close // Tab to focus actions useKeyboardNavigation({ onUp: () => focusPrevEvent(), onDown: () => focusNextEvent(), onEnter: () => openEventDetail(), }); ``` **I18n:** ```tsx import { useTranslation } from 'react-i18next'; export function EventMenu({ event }) { const { t } = useTranslation(); return ( {t('event.actions.copy_id')} ); } ``` --- ## Part 10: Developer Experience ### Current DX **Good:** - ✅ Clear file structure (`src/components/nostr/kinds/`) - ✅ TypeScript types (`BaseEventProps`) - ✅ README documenting pattern - ✅ Consistent naming (`KindXRenderer`) **Friction:** - ❌ Can't hot-reload new renderer without modifying `index.tsx` - ❌ No component gallery (Storybook) - ❌ Hard to test renderers in isolation - ❌ Manual registration in multiple places - ❌ No development tooling (event inspector) - ❌ No renderer generator CLI ### Ideal DX **1. Convention-Based Registration** ```bash # Just create the file, auto-discovered src/components/nostr/kinds/Kind1111Renderer.tsx # No need to modify index.tsx ``` **2. Component Gallery** ```tsx // Visit /dev/renderers in dev mode // Browse all renderers with sample events // Test with different contexts (feed/detail/embed) // Inspect props, performance ``` **3. Testing Utilities** ```tsx import { renderKind, mockEvent } from '@/test/utils'; test('Kind1Renderer displays content', () => { const event = mockEvent({ kind: 1, content: 'Hello' }); const { getByText } = renderKind(1, event); expect(getByText('Hello')).toBeInTheDocument(); }); ``` **4. Generator CLI** ```bash npm run generate:renderer -- --kind 1111 --nip 22 # Scaffolds: # - Kind1111Renderer.tsx with boilerplate # - Kind1111Renderer.test.tsx # - Updates registry # - Adds to documentation ``` **5. Dev Tools** ```tsx // Browser extension or dev panel // Shows: props, state, performance, helper calls, errors ``` --- ## Part 11: What's Working Well (To Preserve) These patterns are **strengths** to maintain and enhance: 1. ✅ **Registry Pattern**: Centralized kind → renderer mapping 2. ✅ **BaseEventContainer**: Consistent header/footer 3. ✅ **Applesauce Integration**: Using library helpers 4. ✅ **Type Safety**: TypeScript interfaces 5. ✅ **Separation of Concerns**: Rendering separate from data fetching 6. ✅ **Recursive Rendering**: KindRenderer can nest 7. ✅ **Universal Actions**: EventMenu available everywhere 8. ✅ **Event Identity**: Good handling of regular vs addressable 9. ✅ **Default Fallback**: Unknown kinds still display 10. ✅ **Component Reuse**: EmbeddedEvent, MediaEmbed, RichText **Don't throw away these foundations - build on them!** --- ## Part 12: Comprehensive Improvement Roadmap ### Phase 1: Foundation Fixes (1-2 weeks) **Goal:** Fix critical architectural issues and quick wins **1.1 Unified Detail Renderer Registry** - Remove hardcoded switch in EventDetailViewer - Create `detailRenderers` map parallel to `kindRenderers` - Fallback logic: detail → feed → default - Files: `src/components/nostr/kinds/index.tsx`, `EventDetailViewer.tsx` - **Impact:** HIGH | **Effort:** LOW **1.2 Systematic Depth Tracking** - Add `MAX_EMBED_DEPTH` constant - Update `BaseEventProps` to require depth - Audit all renderers using `EmbeddedEvent` - Implement `CollapsedPreview` for max depth - Files: All `*Renderer.tsx` files - **Impact:** HIGH | **Effort:** MEDIUM **1.3 Error Boundaries** - Create `EventErrorBoundary` component - Wrap all events in feeds - Add diagnostic error cards - File: `src/components/EventErrorBoundary.tsx` - **Impact:** HIGH | **Effort:** LOW **1.4 Fix JSON Viewer Scrolling** - From TODO: "JSON viewer scrolling" - Add `overflow-auto` and `max-height` to JSON container - File: `src/components/JsonViewer.tsx` - **Impact:** MEDIUM | **Effort:** TRIVIAL **1.5 Renderer Memoization** - Wrap all renderer components with `React.memo` - Add `useMemo` for expensive computations - Add `useCallback` for handlers - Files: All `*Renderer.tsx` files - **Impact:** MEDIUM | **Effort:** LOW **Deliverables:** - [ ] Detail renderer registry implemented - [ ] All renderers honor depth (with tests) - [ ] Error boundaries deployed - [ ] JSON viewer scrolls properly - [ ] All renderers memoized --- ### Phase 2: Component Library (2-3 weeks) **Goal:** Build reusable abstractions for common patterns **2.1 Generic Threading Components** - `getThreadReferences()` helper supporting NIP-10, NIP-22, NIP-28 - `` component - `` for parent preview - `` for detail view reply chains - Files: `src/lib/threading.ts`, `src/components/Thread/` - **Impact:** HIGH | **Effort:** HIGH **2.2 NIP-22 Comment Support** - Implement `Kind1111Renderer` (from TODO) - NIP-22 tag parsing helpers (K/k, E/e, A/a, I/i, P/p) - External identifier display (I tags) - Nested comment threading - Files: `src/lib/helpers/nip22.ts`, `src/components/nostr/kinds/Kind1111Renderer.tsx` - **Impact:** HIGH | **Effort:** HIGH **2.3 Relationship Panels** - `` - Show replies to event - `` - Show zaps with total/list - `` - Group reactions by emoji - `` - Universal engagement indicators - Use in detail renderers - Files: `src/components/nostr/Relationships/` - **Impact:** MEDIUM | **Effort:** MEDIUM **2.4 Enhanced Media Components** - Multi-stage rendering (placeholder → thumbnail → full → error) - Lazy loading with IntersectionObserver - NSFW blur with content-warning tag support - Quality selection for videos - Accessibility improvements (alt text, captions) - Files: Enhance `src/components/nostr/MediaEmbed.tsx` - **Impact:** MEDIUM | **Effort:** MEDIUM **2.5 Context-Aware Rendering** - Add `context` prop to BaseEventProps - Renderers adapt to feed vs detail vs embed - Update all existing renderers - Files: `src/components/nostr/kinds/index.tsx`, all renderers - **Impact:** MEDIUM | **Effort:** LOW **Deliverables:** - [ ] Threading works across NIP-10, NIP-22, NIP-28 - [ ] Kind 1111 (comments) fully functional - [ ] Detail views show relationships - [ ] Media rendering has all stages - [ ] Context awareness implemented --- ### Phase 3: Architecture Evolution (3-4 weeks) **Goal:** Transform into production-grade framework **3.1 Performance Optimization** - Virtual scrolling with react-virtuoso - Code splitting for detail renderers - Batch profile fetching - Suspense boundaries - Performance monitoring - Files: `src/components/ReqViewer.tsx`, `EventDetailViewer.tsx` - **Impact:** HIGH | **Effort:** MEDIUM **3.2 Helper Library Expansion** - Audit all renderers for manual tag parsing - Create helpers for all missing NIPs: - File metadata (1063) - Media events (20, 21, 22) - Lists (30000+) - Reposts (6, 16, 18) - Highlights (9802) - Calendar (31922-31925) - Polls (1068) - Submit generic ones to applesauce-core - Files: `src/lib/helpers/` directory structure - **Impact:** HIGH | **Effort:** HIGH **3.3 Accessibility Improvements** - Semantic HTML (`
`, `