import type { NostrEvent } from '@nostrify/nostrify'; import type { ReactNode } from 'react'; import { useState } from 'react'; import { NoteCard } from '@/components/NoteCard'; import { cn } from '@/lib/utils'; /** Maximum nesting depth before collapsing the rest of the thread. */ const MAX_RENDER_DEPTH = 3; export interface ReplyNode { event: NostrEvent; children: ReplyNode[]; /** Sibling replies hidden from the inline thread chain. Revealed on demand. */ hiddenChildren?: ReplyNode[]; } /** Renders a fully threaded reply tree with collapsible deep branches. */ export function ThreadedReplyList({ roots, renderItemHeader }: { roots: ReplyNode[]; renderItemHeader?: (event: NostrEvent) => ReactNode }) { return ( // Drop the trailing border on the last comment in the list — when // the surrounding page doesn't wrap us in a card, that border // floats orphaned below the final note. Two selectors are needed // because the last root may be either a bare
(no // children) or a
wrapping an
chain. `!important` // overrides NoteCard's own `border-b border-border` utility.
{roots.map((node) => ( ))}
); } function ReplyThread({ node, depth, depthless, renderItemHeader }: { node: ReplyNode; depth: number; depthless?: boolean; renderItemHeader?: (event: NostrEvent) => ReactNode }) { const [expanded, setExpanded] = useState(false); const [showHidden, setShowHidden] = useState(false); const hasChildren = node.children.length > 0; const hiddenCount = node.hiddenChildren?.length ?? 0; const shouldCollapse = !depthless && depth >= MAX_RENDER_DEPTH && hasChildren && !expanded; if (shouldCollapse) { return (
{renderItemHeader?.(node.event)} setExpanded(true)} isLast />
); } if (!hasChildren) { return (
{renderItemHeader?.(node.event)}
); } // Once expanded past the depth cap, skip further caps for this subtree const childDepthless = depthless || expanded; return (
{renderItemHeader?.(node.event)} {/* Show hidden sibling count between parent and first child */} {hiddenCount > 0 && !showHidden && ( setShowHidden(true)} /> )} {/* Revealed hidden siblings render as threaded items before the inline child */} {showHidden && node.hiddenChildren!.map((child) => (
{renderItemHeader?.(child.event)}
))} {node.children.map((child) => ( ))}
); } function countDescendants(node: ReplyNode): number { let count = 0; for (const child of node.children) { count += 1 + countDescendants(child); } return count; } function ExpandThreadButton({ count, onClick, isLast }: { count: number; onClick: () => void; isLast?: boolean }) { return ( ); } // ── Flat interface (for pages that don't need full threading) ── export interface ThreadedReply { reply: NostrEvent; firstSubReply?: NostrEvent; } /** Renders replies as a flat list, each with at most one sub-reply hint. */ export function FlatThreadedReplyList({ replies }: { replies: ThreadedReply[] }) { return (
{replies.map(({ reply, firstSubReply }) => (
{firstSubReply && }
))}
); }