Render NIP-29 group metadata as links (#63)

* 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.

* feat: add kind names for NIP-29 group events and simplify renderer

- Add kind 39000 (Group), 39001 (Group Admins), 39002 (Group Members)
  to EVENT_KINDS so kind badge displays proper names
- Simplify GroupMetadataRenderer to show only title, description, and
  Open Chat CTA (remove group picture)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-12 17:46:41 +01:00
committed by GitHub
parent 76dd1e801d
commit 73de36a544
3 changed files with 89 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
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");
// 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 flex-col gap-1">
<ClickableEventTitle event={event} className="font-semibold">
{name}
</ClickableEventTitle>
{about && (
<p className="text-xs text-muted-foreground line-clamp-2">{about}</p>
)}
{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>
</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)
};

View File

@@ -1415,6 +1415,27 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
nip: "51",
icon: Play,
},
39000: {
kind: 39000,
name: "Group",
description: "Group Metadata",
nip: "29",
icon: Users,
},
39001: {
kind: 39001,
name: "Group Admins",
description: "Group Admins List",
nip: "29",
icon: UserCheck,
},
39002: {
kind: 39002,
name: "Group Members",
description: "Group Members List",
nip: "29",
icon: Users,
},
39701: {
kind: 39701,
name: "Web Bookmark",