diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 6452d7e..c8c7905 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -446,40 +446,6 @@ export function ChatViewer({ // Ref to MentionEditor for programmatic submission const editorRef = useRef(null); - // Extract communikey blossom servers if available - const communikeyServers = useMemo(() => { - if ( - conversationResult.status === "success" && - conversationResult.conversation.protocol === "communikeys" - ) { - return ( - conversationResult.conversation.metadata?.communikeyConfig - ?.blossomServers || [] - ); - } - return []; - }, [conversationResult]); - - // Blossom upload hook for file attachments - const { open: openUpload, dialog: uploadDialog } = useBlossomUpload({ - accept: "image/*,video/*,audio/*", - communikeyServers, - onSuccess: (results) => { - if (results.length > 0 && editorRef.current) { - // Insert the first successful upload as a blob attachment with metadata - const { blob, server } = results[0]; - editorRef.current.insertBlob({ - url: blob.url, - sha256: blob.sha256, - mimeType: blob.type, - size: blob.size, - server, - }); - editorRef.current.focus(); - } - }, - }); - // Get the appropriate adapter for this protocol const adapter = useMemo(() => getAdapter(protocol), [protocol]); @@ -515,6 +481,34 @@ export function ChatViewer({ ? conversationResult.conversation : null; + // Extract communikey blossom servers if available + const communikeyServers = useMemo(() => { + if (conversation && conversation.protocol === "communikeys") { + return conversation.metadata?.communikeyConfig?.blossomServers || []; + } + return []; + }, [conversation]); + + // Blossom upload hook for file attachments + const { open: openUpload, dialog: uploadDialog } = useBlossomUpload({ + accept: "image/*,video/*,audio/*", + communikeyServers, + onSuccess: (results) => { + if (results.length > 0 && editorRef.current) { + // Insert the first successful upload as a blob attachment with metadata + const { blob, server } = results[0]; + editorRef.current.insertBlob({ + url: blob.url, + sha256: blob.sha256, + mimeType: blob.type, + size: blob.size, + server, + }); + editorRef.current.focus(); + } + }, + }); + // Slash command search for action autocomplete // Context-aware: only shows relevant actions based on membership status const searchCommands = useCallback( diff --git a/src/components/nostr/kinds/CommunikeyRenderer.tsx b/src/components/nostr/kinds/CommunikeyRenderer.tsx new file mode 100644 index 0000000..0ef28df --- /dev/null +++ b/src/components/nostr/kinds/CommunikeyRenderer.tsx @@ -0,0 +1,356 @@ +import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; +import type { NostrEvent } from "@/types/nostr"; +import { getTagValue } from "applesauce-core/helpers"; +import { getTagValues } from "@/lib/nostr-utils"; +import { useGrimoire } from "@/core/state"; +import { MessageSquare, HardDrive, Coins, Shield } from "lucide-react"; +import { useProfile } from "@/hooks/useProfile"; +import { UserName } from "@/components/nostr/UserName"; + +/** + * Kind 10222 Renderer - Communikey Definition (Feed View) + * Shows communikey info with chat link + */ +export function CommunikeyRenderer({ event }: BaseEventProps) { + const { addWindow } = useGrimoire(); + const profile = useProfile(event.pubkey); + + // Extract communikey configuration + const description = getTagValue(event, "description"); + const relays = getTagValues(event, "r"); + const blossomServers = getTagValues(event, "blossom"); + const mints = getTagValues(event, "mint"); + + // Get content sections + const contentTags = event.tags.filter((t) => t[0] === "content"); + const contentSections = contentTags.map((t) => t[1]).filter(Boolean); + + // Community name from profile or pubkey + const name = + profile?.display_name || + profile?.name || + `Communikey ${event.pubkey.slice(0, 8)}`; + + const handleOpenChat = () => { + try { + addWindow("chat", { + protocol: "communikeys", + identifier: { + type: "group", + value: event.pubkey, + relays, + }, + }); + } catch (error) { + console.error("Failed to open communikey chat:", error); + } + }; + + const canOpenChat = relays.length > 0; + + return ( + +
+ {/* Title */} +
+ +
{name}
+ + () + +
+ + {/* Description */} + {(description || profile?.about) && ( +

+ {description || profile?.about} +

+ )} + + {/* Stats */} +
+ {relays.length > 0 && ( + + + {relays.length} relay{relays.length !== 1 ? "s" : ""} + + )} + {blossomServers.length > 0 && ( + + + {blossomServers.length} blossom + + )} + {mints.length > 0 && ( + + + {mints.length} mint{mints.length !== 1 ? "s" : ""} + + )} +
+ + {/* Content Sections */} + {contentSections.length > 0 && ( +
+ {contentSections.slice(0, 3).map((section, i) => ( + + {section} + + ))} + {contentSections.length > 3 && ( + + +{contentSections.length - 3} more + + )} +
+ )} + + {/* Open Chat Button */} + {canOpenChat && ( + + )} +
+
+ ); +} + +/** + * Kind 10222 Detail Renderer - Communikey Definition (Detail View) + * Shows full communikey configuration + */ +export function CommunikeyDetailRenderer({ event }: { event: NostrEvent }) { + const { addWindow } = useGrimoire(); + const profile = useProfile(event.pubkey); + + // Extract all configuration + const description = getTagValue(event, "description"); + const location = getTagValue(event, "location"); + const geoHash = getTagValue(event, "g"); + const relays = getTagValues(event, "r"); + const blossomServers = getTagValues(event, "blossom"); + const mints = getTagValues(event, "mint"); + + // Parse content sections with their settings + interface ContentSection { + name: string; + kinds: number[]; + roles: string[]; + fee?: { amount: number; unit: string }; + exclusive: boolean; + } + + const contentSections: ContentSection[] = []; + let currentSection: ContentSection | null = null; + + for (const tag of event.tags) { + if (tag[0] === "content" && tag[1]) { + // Save previous section + if (currentSection) { + contentSections.push(currentSection); + } + // Start new section + currentSection = { + name: tag[1], + kinds: [], + roles: [], + exclusive: false, + }; + } else if (currentSection) { + if (tag[0] === "k" && tag[1]) { + const kind = parseInt(tag[1], 10); + if (!isNaN(kind)) { + currentSection.kinds.push(kind); + } + } else if (tag[0] === "role") { + currentSection.roles.push(...tag.slice(1)); + } else if (tag[0] === "fee" && tag[1] && tag[2]) { + currentSection.fee = { + amount: parseInt(tag[1], 10), + unit: tag[2], + }; + } else if (tag[0] === "exclusive") { + currentSection.exclusive = tag[1] === "true"; + } + } + } + + // Don't forget the last section + if (currentSection) { + contentSections.push(currentSection); + } + + // Community name from profile or pubkey + const name = + profile?.display_name || + profile?.name || + `Communikey ${event.pubkey.slice(0, 8)}`; + + const handleOpenChat = () => { + try { + addWindow("chat", { + protocol: "communikeys", + identifier: { + type: "group", + value: event.pubkey, + relays, + }, + }); + } catch (error) { + console.error("Failed to open communikey chat:", error); + } + }; + + return ( +
+ {/* Header */} +
+
+ +

{name}

+
+
+ +
+
+ + {/* Description */} + {(description || profile?.about) && ( +
{description || profile?.about}
+ )} + + {/* Location */} + {location && ( +
+ Location: {location} + {geoHash && ( + ({geoHash}) + )} +
+ )} + + {/* Relays */} + {relays.length > 0 && ( +
+

+ + Relays ({relays.length}) +

+
+ {relays.map((relay, i) => ( +
+ {i === 0 && ( + + [Main] + + )} + {relay} +
+ ))} +
+
+ )} + + {/* Blossom Servers */} + {blossomServers.length > 0 && ( +
+

+ + Blossom Servers ({blossomServers.length}) +

+
+ {blossomServers.map((server, i) => ( +
+ {server} +
+ ))} +
+
+ )} + + {/* Mints */} + {mints.length > 0 && ( +
+

+ + Ecash Mints ({mints.length}) +

+
+ {mints.map((mint, i) => ( +
+ {mint} +
+ ))} +
+
+ )} + + {/* Content Sections */} + {contentSections.length > 0 && ( +
+

+ Content Sections ({contentSections.length}) +

+
+ {contentSections.map((section, i) => ( +
+
{section.name}
+ {section.kinds.length > 0 && ( +
+ Kinds:{" "} + {section.kinds.join(", ")} +
+ )} + {section.roles.length > 0 && ( +
+ Roles:{" "} + {section.roles.join(", ")} +
+ )} + {section.fee && ( +
+ Fee:{" "} + {section.fee.amount} {section.fee.unit} +
+ )} + {section.exclusive && ( +
+ ⚠ Exclusive (cannot target other communities) +
+ )} +
+ ))} +
+
+ )} + + {/* Open Chat Button */} + {relays.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index fcec3b3..7955d4f 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -30,6 +30,10 @@ import { BlossomServerListRenderer, BlossomServerListDetailRenderer, } from "./BlossomServerListRenderer"; +import { + CommunikeyRenderer, + CommunikeyDetailRenderer, +} from "./CommunikeyRenderer"; import { Kind10317Renderer } from "./GraspListRenderer"; import { Kind10317DetailRenderer } from "./GraspListDetailRenderer"; import { Kind30023Renderer } from "./ArticleRenderer"; @@ -189,6 +193,7 @@ const kindRenderers: Record> = { 10063: BlossomServerListRenderer, // Blossom User Server List (BUD-03) 10101: WikiAuthorsRenderer, // Good Wiki Authors (NIP-51) 10102: WikiRelaysRenderer, // Good Wiki Relays (NIP-51) + 10222: CommunikeyRenderer, // Communikey Definition (NIP-CC) 10317: Kind10317Renderer, // User Grasp List (NIP-34) 13534: RelayMembersRenderer, // Relay Members (NIP-43) 30000: FollowSetRenderer, // Follow Sets (NIP-51) @@ -282,6 +287,7 @@ const detailRenderers: Record< 10063: BlossomServerListDetailRenderer, // Blossom User Server List Detail (BUD-03) 10101: WikiAuthorsDetailRenderer, // Good Wiki Authors Detail (NIP-51) 10102: WikiRelaysDetailRenderer, // Good Wiki Relays Detail (NIP-51) + 10222: CommunikeyDetailRenderer, // Communikey Definition Detail (NIP-CC) 10317: Kind10317DetailRenderer, // User Grasp List Detail (NIP-34) 13534: RelayMembersDetailRenderer, // Relay Members Detail (NIP-43) 30000: FollowSetDetailRenderer, // Follow Sets Detail (NIP-51)