From 7c033aa370e8f9fe007ad1a1e41968c73931bdda Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 16:54:42 +0000 Subject: [PATCH] feat: render chat root posts using feed renderer - Add `bare` prop to BaseEventContainer and KindRenderer to render content without header/footer wrapper - Update ChatViewer MessageItem to detect root posts and render them using KindRenderer with bare mode - Root posts display with styled container showing author/timestamp below - Fix protocol labels: "Thread" for NIP-10, "Comments" for NIP-22 - Add MessageSquare icon for NIP-22 comment threads --- src/components/ChatViewer.tsx | 30 +++++++++++++++++++ .../nostr/kinds/BaseEventRenderer.tsx | 14 +++++++++ src/components/nostr/kinds/NoteRenderer.tsx | 14 +++++---- src/components/nostr/kinds/index.tsx | 8 +++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 686d9bd..7f5d614 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -12,6 +12,7 @@ import { Copy, CopyCheck, FileText, + MessageSquare, } from "lucide-react"; import { nip19 } from "nostr-tools"; import { getZapRequest } from "applesauce-common/helpers/zap"; @@ -35,6 +36,7 @@ import type { ChatAction } from "@/types/chat-actions"; import { parseSlashCommand } from "@/lib/chat/slash-command-parser"; import { UserName } from "./nostr/UserName"; import { RichText } from "./nostr/RichText"; +import { KindRenderer } from "./nostr/kinds"; import Timestamp from "./Timestamp"; import { ReplyPreview } from "./chat/ReplyPreview"; import { MembersDropdown } from "./chat/MembersDropdown"; @@ -263,6 +265,7 @@ const MessageItem = memo(function MessageItem({ onReply, canReply, onScrollToMessage, + isRootPost = false, }: { message: Message; adapter: ChatProtocolAdapter; @@ -270,6 +273,7 @@ const MessageItem = memo(function MessageItem({ onReply?: (messageId: string) => void; canReply: boolean; onScrollToMessage?: (messageId: string) => void; + isRootPost?: boolean; }) { // Get relays for this conversation (memoized to prevent unnecessary re-subscriptions) const relays = useMemo( @@ -277,6 +281,22 @@ const MessageItem = memo(function MessageItem({ [conversation], ); + // Root post: render using KindRenderer with bare mode (no header/footer) + if (isRootPost && message.event) { + return ( +
+ +
+ + + + + +
+
+ ); + } + // System messages (join/leave) have special styling if (message.type === "system") { return ( @@ -918,6 +938,11 @@ export function ChatViewer({ Thread + ) : conversation.protocol === "nip-22" ? ( + + + Comments + ) : ( {conversation.type} @@ -1028,6 +1053,10 @@ export function ChatViewer({ ); } + // Check if this is the root post (for NIP-10/NIP-22) + const rootEventId = conversation.metadata?.rootEventId; + const isRootPost = rootEventId === item.data.id; + return ( ); }} diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index b10f633..562c3ca 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -48,6 +48,11 @@ export interface BaseEventProps { pubkey: string; label?: string; // e.g., "Host", "Sender", "Zapper", "From" }; + /** + * If true, render content without header/footer wrapper + * Used in chat views where the container provides its own context + */ + bare?: boolean; } /** @@ -363,6 +368,8 @@ export function ClickableEventTitle({ /** * Base event container with universal header * Kind-specific renderers can wrap their content with this + * + * @param bare - If true, render children without header/footer wrapper */ /** * Format relative time (e.g., "2m ago", "3h ago", "5d ago") @@ -372,6 +379,7 @@ export function BaseEventContainer({ event, children, authorOverride, + bare = false, }: { event: NostrEvent; children: React.ReactNode; @@ -379,9 +387,15 @@ export function BaseEventContainer({ pubkey: string; label?: string; }; + bare?: boolean; }) { const { locale } = useGrimoire(); + // If bare mode, just render children without wrapper + if (bare) { + return <>{children}; + } + // Format relative time for display const relativeTime = formatTimestamp( event.created_at, diff --git a/src/components/nostr/kinds/NoteRenderer.tsx b/src/components/nostr/kinds/NoteRenderer.tsx index 9614de7..e815fd9 100644 --- a/src/components/nostr/kinds/NoteRenderer.tsx +++ b/src/components/nostr/kinds/NoteRenderer.tsx @@ -71,7 +71,11 @@ function ParentEventCard({ * Renderer for Kind 1 - Short Text Note (NIP-10 threading) * Shows immediate parent (reply) only for cleaner display */ -export function Kind1Renderer({ event, depth = 0 }: BaseEventProps) { +export function Kind1Renderer({ + event, + depth = 0, + bare = false, +}: BaseEventProps) { const { addWindow } = useGrimoire(); // Use NIP-10 threading helpers @@ -93,14 +97,14 @@ export function Kind1Renderer({ event, depth = 0 }: BaseEventProps) { }; return ( - + - {/* Show reply event (immediate parent) */} - {replyPointer && !replyEvent && ( + {/* Show reply event (immediate parent) - hide in bare mode */} + {!bare && replyPointer && !replyEvent && ( } /> )} - {replyPointer && replyEvent && ( + {!bare && replyPointer && replyEvent && ( > = { * Default renderer for kinds without custom implementations * Shows basic event info with raw content */ -function DefaultKindRenderer({ event }: BaseEventProps) { +function DefaultKindRenderer({ event, bare = false }: BaseEventProps) { return ( - +
           {event.content || "(empty content)"}
@@ -256,12 +256,14 @@ function DefaultKindRenderer({ event }: BaseEventProps) {
 export function KindRenderer({
   event,
   depth = 0,
+  bare = false,
 }: {
   event: NostrEvent;
   depth?: number;
+  bare?: boolean;
 }) {
   const Renderer = kindRenderers[event.kind] || DefaultKindRenderer;
-  return ;
+  return ;
 }
 
 /**