mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 07:56:50 +02:00
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
This commit is contained in:
@@ -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 (
|
||||
<div className="border-b border-border/50 px-3 py-2 bg-muted/20">
|
||||
<KindRenderer event={message.event} bare={true} />
|
||||
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-border/30">
|
||||
<UserName pubkey={message.author} className="text-sm font-medium" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Timestamp timestamp={message.timestamp} />
|
||||
</span>
|
||||
<MessageReactions messageId={message.id} relays={relays} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// System messages (join/leave) have special styling
|
||||
if (message.type === "system") {
|
||||
return (
|
||||
@@ -918,6 +938,11 @@ export function ChatViewer({
|
||||
<FileText className="size-3" />
|
||||
Thread
|
||||
</span>
|
||||
) : conversation.protocol === "nip-22" ? (
|
||||
<span className="flex items-center gap-1 text-primary-foreground/80">
|
||||
<MessageSquare className="size-3" />
|
||||
Comments
|
||||
</span>
|
||||
) : (
|
||||
<span className="capitalize text-primary-foreground/80">
|
||||
{conversation.type}
|
||||
@@ -1028,6 +1053,10 @@ export function ChatViewer({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Check if this is the root post (for NIP-10/NIP-22)
|
||||
const rootEventId = conversation.metadata?.rootEventId;
|
||||
const isRootPost = rootEventId === item.data.id;
|
||||
|
||||
return (
|
||||
<MessageItem
|
||||
key={item.data.id}
|
||||
@@ -1037,6 +1066,7 @@ export function ChatViewer({
|
||||
onReply={handleReply}
|
||||
canReply={canSign}
|
||||
onScrollToMessage={handleScrollToMessage}
|
||||
isRootPost={isRootPost}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<BaseEventContainer event={event} bare={bare}>
|
||||
<TooltipProvider>
|
||||
{/* Show reply event (immediate parent) */}
|
||||
{replyPointer && !replyEvent && (
|
||||
{/* Show reply event (immediate parent) - hide in bare mode */}
|
||||
{!bare && replyPointer && !replyEvent && (
|
||||
<InlineReplySkeleton icon={<Reply className="size-3" />} />
|
||||
)}
|
||||
|
||||
{replyPointer && replyEvent && (
|
||||
{!bare && replyPointer && replyEvent && (
|
||||
<ParentEventCard
|
||||
parentEvent={replyEvent}
|
||||
icon={Reply}
|
||||
|
||||
@@ -237,9 +237,9 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
* 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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<BaseEventContainer event={event} bare={bare}>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words">
|
||||
{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 <Renderer event={event} depth={depth} />;
|
||||
return <Renderer event={event} depth={depth} bare={bare} />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user