diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 9f11d58..206585d 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -32,6 +32,7 @@ import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter"; import { Nip53Adapter } from "@/lib/chat/adapters/nip-53-adapter"; import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter"; import type { Message } from "@/types/chat"; +import type { NostrEvent } from "@/types/nostr"; import type { ChatAction } from "@/types/chat-actions"; import { parseSlashCommand } from "@/lib/chat/slash-command-parser"; import { @@ -41,6 +42,7 @@ import { } from "@/lib/chat/group-system-messages"; 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"; @@ -531,6 +533,74 @@ const MessageItem = memo(function MessageItem({ return messageContent; }); +/** + * RootEventItem - Renders the root event (OP) using the full KindRenderer + * so it looks like a proper feed item at the top of the comments thread + */ +const RootEventItem = memo(function RootEventItem({ + event, +}: { + event: NostrEvent; +}) { + return ( +
+ +
+ ); +}); + +/** + * ExternalRootItem - Renders an external resource root (NIP-73) + * for conversations scoped to URLs, ISBNs, DOIs, etc. + */ +const ExternalRootItem = memo(function ExternalRootItem({ + externalId, + externalKind, +}: { + externalId: string; + externalKind: string; +}) { + let displayLabel = externalKind; + let displayValue = externalId; + + if (externalKind === "web") { + displayLabel = "URL"; + try { + const url = new URL(externalId); + displayValue = url.hostname + url.pathname; + } catch { + // keep raw + } + } else if (externalKind.startsWith("isbn")) { + displayLabel = "ISBN"; + } else if (externalKind.startsWith("doi")) { + displayLabel = "DOI"; + } + + return ( +
+
+ + {displayLabel} +
+

+ {externalKind === "web" ? ( + + {displayValue} + + ) : ( + {displayValue} + )} +

+
+ ); +}); + /** * ChatViewer - Main chat interface component * @@ -1096,7 +1166,8 @@ export function ChatViewer({ {(conversation.type === "group" || - conversation.type === "live-chat") && ( + conversation.type === "live-chat" || + conversation.type === "comments") && ( - - ) : null, + Header: () => ( + <> + {/* External root display for NIP-22 conversations without a Nostr root event */} + {protocol === "nip-22" && + conversation.metadata?.externalId && + conversation.metadata?.externalKind && ( + + )} + {hasMore && + conversationResult.status === "success" && + protocol !== "nip-10" && + protocol !== "nip-22" && ( +
+ +
+ )} + + ), Footer: () =>
, }} itemContent={(_index, item) => { @@ -1172,11 +1256,18 @@ export function ChatViewer({ ); } - // For NIP-10 threads, check if this is the root message + // For NIP-10 and NIP-22, check if this is the root message const isRootMessage = - protocol === "nip-10" && + (protocol === "nip-10" || protocol === "nip-22") && conversation.metadata?.rootEventId === item.data.id; + // Root messages are rendered with the full KindRenderer (feed view) + if (isRootMessage && item.data.event) { + return ( + + ); + } + return (