feat: add GroupMetadataRenderer for NIP-29 group metadata (kind 39000)

Render kind 39000 events with group name, picture, description, and
an "Open Chat" link that opens the NIP-29 group in the chat viewer.
This commit is contained in:
Claude
2026-01-12 16:35:56 +00:00
parent 76dd1e801d
commit ff5ee8dcca
2 changed files with 93 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
import type { NostrEvent } from "@/types/nostr";
import { getTagValue } from "applesauce-core/helpers";
import { getSeenRelays } from "applesauce-core/helpers/relays";
import { BaseEventContainer, ClickableEventTitle } from "./BaseEventRenderer";
import { useGrimoire } from "@/core/state";
import { MessageSquare } from "lucide-react";
interface GroupMetadataRendererProps {
event: NostrEvent;
}
/**
* Renderer for NIP-29 Group Metadata events (kind 39000)
* Displays group info and links to chat
*/
export function GroupMetadataRenderer({ event }: GroupMetadataRendererProps) {
const { addWindow } = useGrimoire();
// Extract group metadata
const groupId = getTagValue(event, "d") || "";
const name = getTagValue(event, "name") || groupId;
const about = getTagValue(event, "about");
const picture = getTagValue(event, "picture");
// Get relay URL from where we saw this event
const seenRelaysSet = getSeenRelays(event);
const relayUrl = seenRelaysSet?.values().next().value;
const handleOpenChat = () => {
if (!relayUrl) return;
addWindow("chat", {
protocol: "nip-29",
identifier: {
type: "group",
value: groupId,
relays: [relayUrl],
},
});
};
const canOpenChat = !!relayUrl && !!groupId;
return (
<BaseEventContainer event={event}>
<div className="flex gap-3">
{/* Group Picture */}
<div
className={canOpenChat ? "cursor-crosshair" : ""}
onClick={canOpenChat ? handleOpenChat : undefined}
>
{picture ? (
<img
src={picture}
alt={name}
className="size-12 rounded-lg object-cover flex-shrink-0"
/>
) : (
<div className="size-12 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
<MessageSquare className="size-6 text-muted-foreground" />
</div>
)}
</div>
{/* Group Info */}
<div className="flex flex-col gap-1 min-w-0 flex-1">
<ClickableEventTitle event={event} className="font-semibold">
{name}
</ClickableEventTitle>
{about && (
<p className="text-xs text-muted-foreground line-clamp-2">
{about}
</p>
)}
{/* Open Chat Link */}
{canOpenChat && (
<button
onClick={handleOpenChat}
className="text-xs text-primary hover:underline flex items-center gap-1 w-fit mt-1"
>
<MessageSquare className="size-3" />
Open Chat
</button>
)}
</div>
</div>
</BaseEventContainer>
);
}

View File

@@ -61,6 +61,7 @@ import { ZapstoreAppSetRenderer } from "./ZapstoreAppSetRenderer";
import { ZapstoreAppSetDetailRenderer } from "./ZapstoreAppSetDetailRenderer";
import { ZapstoreReleaseRenderer } from "./ZapstoreReleaseRenderer";
import { ZapstoreReleaseDetailRenderer } from "./ZapstoreReleaseDetailRenderer";
import { GroupMetadataRenderer } from "./GroupMetadataRenderer";
import { NostrEvent } from "@/types/nostr";
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
@@ -116,6 +117,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
31989: HandlerRecommendationRenderer, // Handler Recommendation (NIP-89)
31990: ApplicationHandlerRenderer, // Application Handler (NIP-89)
32267: ZapstoreAppRenderer, // Zapstore App
39000: GroupMetadataRenderer, // Group Metadata (NIP-29)
39701: Kind39701Renderer, // Web Bookmarks (NIP-B0)
};