mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 07:27:23 +02:00
feat(chat): render root event with KindRenderer in NIP-10 and NIP-22 chats
The root event (OP) at the top of a comments/thread chat is now rendered using the full KindRenderer (feed renderer) instead of a plain text chat bubble. This shows the original event with its proper kind-specific UI (article, picture, repo, note, etc.). - Add RootEventItem component wrapping KindRenderer for Nostr events - Add ExternalRootItem component for NIP-73 external roots (URLs, ISBNs, DOIs) rendered as a descriptive header card - Extend isRootMessage detection to both NIP-10 and NIP-22 protocols - Root events bypass MessageItem entirely, rendered inline in feed - External roots shown as Virtuoso Header when no Nostr event exists - Show NIP-22 protocol badge in conversation header - Disable "Load older" pagination for NIP-22 (like NIP-10) https://claude.ai/code/session_01PsevnSkZf2Pn1yhc1Rinc3
This commit is contained in:
@@ -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 (
|
||||
<div className="border-b border-muted mb-2">
|
||||
<KindRenderer event={event} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<div className="border-b border-muted mb-2 px-3 py-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<FileText className="size-4 flex-shrink-0" />
|
||||
<span className="font-medium uppercase text-xs">{displayLabel}</span>
|
||||
</div>
|
||||
<p className="text-sm mt-1 break-all">
|
||||
{externalKind === "web" ? (
|
||||
<a
|
||||
href={externalId}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{displayValue}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-mono">{displayValue}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* ChatViewer - Main chat interface component
|
||||
*
|
||||
@@ -1096,7 +1166,8 @@ export function ChatViewer({
|
||||
<MembersDropdown participants={derivedParticipants} />
|
||||
<RelaysDropdown conversation={conversation} />
|
||||
{(conversation.type === "group" ||
|
||||
conversation.type === "live-chat") && (
|
||||
conversation.type === "live-chat" ||
|
||||
conversation.type === "comments") && (
|
||||
<button
|
||||
onClick={handleNipClick}
|
||||
className="rounded bg-muted px-1.5 py-0.5 font-mono hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
@@ -1125,28 +1196,41 @@ export function ChatViewer({
|
||||
}}
|
||||
alignToBottom
|
||||
components={{
|
||||
Header: () =>
|
||||
hasMore &&
|
||||
conversationResult.status === "success" &&
|
||||
protocol !== "nip-10" ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
disabled={isLoadingOlder}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{isLoadingOlder ? (
|
||||
<>
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
<span className="text-xs">Loading...</span>
|
||||
</>
|
||||
) : (
|
||||
"Load older messages"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : null,
|
||||
Header: () => (
|
||||
<>
|
||||
{/* External root display for NIP-22 conversations without a Nostr root event */}
|
||||
{protocol === "nip-22" &&
|
||||
conversation.metadata?.externalId &&
|
||||
conversation.metadata?.externalKind && (
|
||||
<ExternalRootItem
|
||||
externalId={conversation.metadata.externalId}
|
||||
externalKind={conversation.metadata.externalKind}
|
||||
/>
|
||||
)}
|
||||
{hasMore &&
|
||||
conversationResult.status === "success" &&
|
||||
protocol !== "nip-10" &&
|
||||
protocol !== "nip-22" && (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
disabled={isLoadingOlder}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{isLoadingOlder ? (
|
||||
<>
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
<span className="text-xs">Loading...</span>
|
||||
</>
|
||||
) : (
|
||||
"Load older messages"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
Footer: () => <div className="h-1" />,
|
||||
}}
|
||||
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 (
|
||||
<RootEventItem key={item.data.id} event={item.data.event} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageItem
|
||||
key={item.data.id}
|
||||
|
||||
Reference in New Issue
Block a user