diff --git a/src/components/nostr/GroupLink.tsx b/src/components/nostr/GroupLink.tsx new file mode 100644 index 0000000..5cddef7 --- /dev/null +++ b/src/components/nostr/GroupLink.tsx @@ -0,0 +1,94 @@ +import { MessageSquare } from "lucide-react"; +import { useGrimoire } from "@/core/state"; +import { cn } from "@/lib/utils"; +import { use$ } from "applesauce-react/hooks"; +import { map } from "rxjs/operators"; +import eventStore from "@/services/event-store"; +import { getTagValue } from "applesauce-core/helpers"; + +/** + * Format group identifier for display + * Shows just the group-id part without the relay URL + */ +function formatGroupIdForDisplay(groupId: string): string { + return groupId; +} + +export interface GroupLinkProps { + groupId: string; + relayUrl: string; + className?: string; + iconClassname?: string; +} + +/** + * GroupLink - Clickable NIP-29 group component + * Displays group name (from kind 39000 metadata) or group ID + * Opens chat window on click + */ +export function GroupLink({ + groupId, + relayUrl, + className, + iconClassname, +}: GroupLinkProps) { + const { addWindow } = useGrimoire(); + + // Try to fetch group metadata (kind 39000) from EventStore + // NIP-29 metadata events use #d tag with group ID + const groupMetadata = use$( + () => + eventStore + .timeline([{ kinds: [39000], "#d": [groupId], limit: 1 }]) + .pipe(map((events) => events[0])), + [groupId], + ); + + // Extract group name from metadata if available + const groupName = + groupMetadata && groupMetadata.kind === 39000 + ? getTagValue(groupMetadata, "name") || groupId + : groupId; + + // Extract group icon if available + const groupIcon = + groupMetadata && groupMetadata.kind === 39000 + ? getTagValue(groupMetadata, "picture") + : undefined; + + const handleClick = () => { + // Open chat with NIP-29 format: relay'group-id + const identifier = `${relayUrl}'${groupId}`; + addWindow("chat", { protocol: "nip-29", identifier }); + }; + + const displayName = formatGroupIdForDisplay(groupName); + + return ( +
+
+ {groupIcon ? ( + + ) : ( + + )} + {displayName} +
+
+ ); +} diff --git a/src/components/nostr/kinds/PublicChatsRenderer.tsx b/src/components/nostr/kinds/PublicChatsRenderer.tsx new file mode 100644 index 0000000..03d5f20 --- /dev/null +++ b/src/components/nostr/kinds/PublicChatsRenderer.tsx @@ -0,0 +1,57 @@ +import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; +import { GroupLink } from "../GroupLink"; + +/** + * Extract group references from a kind 10009 event + * Groups are stored in "group" tags: ["group", "", "", ...] + */ +function extractGroups(event: { tags: string[][] }): Array<{ + groupId: string; + relayUrl: string; +}> { + const groups: Array<{ groupId: string; relayUrl: string }> = []; + + for (const tag of event.tags) { + if (tag[0] === "group" && tag[1] && tag[2]) { + groups.push({ + groupId: tag[1], + relayUrl: tag[2], + }); + } + } + + return groups; +} + +/** + * Public Chats Renderer (Kind 10009) + * NIP-51 list of NIP-29 groups + * Displays each group as a clickable link with icon and name + */ +export function PublicChatsRenderer({ event }: BaseEventProps) { + const groups = extractGroups(event); + + if (groups.length === 0) { + return ( + +
+ No public chats configured +
+
+ ); + } + + return ( + +
+ {groups.map((group) => ( + + ))} +
+
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index da8f34c..54201fb 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -37,6 +37,7 @@ import { RepositoryStateRenderer } from "./RepositoryStateRenderer"; import { RepositoryStateDetailRenderer } from "./RepositoryStateDetailRenderer"; import { Kind39701Renderer } from "./BookmarkRenderer"; import { GenericRelayListRenderer } from "./GenericRelayListRenderer"; +import { PublicChatsRenderer } from "./PublicChatsRenderer"; import { LiveActivityRenderer } from "./LiveActivityRenderer"; import { LiveActivityDetailRenderer } from "./LiveActivityDetailRenderer"; import { SpellRenderer, SpellDetailRenderer } from "./SpellRenderer"; @@ -89,6 +90,7 @@ const kindRenderers: Record> = { 10317: Kind10317Renderer, // User Grasp List (NIP-34) 10006: GenericRelayListRenderer, // Blocked Relays (NIP-51) 10007: GenericRelayListRenderer, // Search Relays (NIP-51) + 10009: PublicChatsRenderer, // Public Chats List (NIP-51) 10012: GenericRelayListRenderer, // Favorite Relays (NIP-51) 10050: GenericRelayListRenderer, // DM Relay List (NIP-51) 30002: GenericRelayListRenderer, // Relay Sets (NIP-51) diff --git a/src/lib/chat/adapters/nip-29-adapter.ts b/src/lib/chat/adapters/nip-29-adapter.ts index 3cd04c2..c17f4a0 100644 --- a/src/lib/chat/adapters/nip-29-adapter.ts +++ b/src/lib/chat/adapters/nip-29-adapter.ts @@ -8,6 +8,8 @@ import type { ProtocolIdentifier, ChatCapabilities, LoadMessagesOptions, + Participant, + ParticipantRole, } from "@/types/chat"; import type { NostrEvent } from "@/types/nostr"; import eventStore from "@/services/event-store"; @@ -370,7 +372,7 @@ export class Nip29Adapter extends ChatProtocolAdapter { const event = await factory.sign(draft); // Publish only to the group relay - await publishEventToRelays(event, [conversation?.metadata?.relayUrl]); + await publishEventToRelays(event, [relayUrl]); } /**