diff --git a/src/components/CommunikeyViewer.tsx b/src/components/CommunikeyViewer.tsx index 5db33a9..89b9b60 100644 --- a/src/components/CommunikeyViewer.tsx +++ b/src/components/CommunikeyViewer.tsx @@ -1,7 +1,8 @@ -import { useEffect } from "react"; +import { useEffect, useState, lazy, Suspense } from "react"; import { useEventStore, use$ } from "applesauce-react/hooks"; import { addressLoader } from "@/services/loaders"; import { useProfile } from "@/hooks/useProfile"; +import { useTimeline } from "@/hooks/useTimeline"; import { getDisplayName } from "@/lib/nostr-utils"; import { useGrimoire } from "@/core/state"; import { @@ -14,8 +15,7 @@ import { getCommunikeyTos, } from "@/lib/communikeys-helpers"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Users, Radio, @@ -29,12 +29,22 @@ import { Copy, CopyCheck, User as UserIcon, + Info, + Loader2, } from "lucide-react"; import { nip19 } from "nostr-tools"; import { UserName } from "./nostr/UserName"; import { useCopy } from "@/hooks/useCopy"; import { getKindName, getKindIcon } from "@/constants/kinds"; +import { KindRenderer } from "./nostr/kinds"; +import { EventErrorBoundary } from "./EventErrorBoundary"; import type { ContentSection } from "@/lib/communikeys-helpers"; +import type { NostrEvent } from "@/types/nostr"; + +// Lazy load ChatViewer to avoid circular dependency +const ChatViewer = lazy(() => + import("./ChatViewer").then((m) => ({ default: m.ChatViewer })), +); const COMMUNIKEY_KIND = 10222; @@ -45,13 +55,14 @@ export interface CommunikeyViewerProps { /** * CommunikeyViewer - View a Communikey community - * Shows community profile, configuration, and content sections + * Shows community profile with tabbed content sections and chat */ export function CommunikeyViewer({ pubkey, relays }: CommunikeyViewerProps) { - const { state, addWindow } = useGrimoire(); + const { state } = useGrimoire(); const accountPubkey = state.activeAccount?.pubkey; const eventStore = useEventStore(); const { copy, copied } = useCopy(); + const [activeTab, setActiveTab] = useState("chat"); // Resolve $me alias const resolvedPubkey = pubkey === "$me" ? accountPubkey : pubkey; @@ -112,27 +123,6 @@ export function CommunikeyViewer({ pubkey, relays }: CommunikeyViewerProps) { // Generate npub for copying const npub = resolvedPubkey ? nip19.npubEncode(resolvedPubkey) : ""; - // Open chat for this community - const openChat = () => { - if (resolvedPubkey) { - addWindow("chat", { - protocol: "communikeys", - identifier: { - type: "communikey", - value: resolvedPubkey, - relays: communityRelays, - }, - }); - } - }; - - // View community profile - const viewProfile = () => { - if (resolvedPubkey) { - addWindow("profile", { pubkey: resolvedPubkey }); - } - }; - if (pubkey === "$me" && !accountPubkey) { return (
@@ -157,225 +147,424 @@ export function CommunikeyViewer({ pubkey, relays }: CommunikeyViewerProps) { return (
{/* Compact Header */} -
- {/* Left: npub */} +
+ {/* Left: Profile info */} +
+ {profile?.picture ? ( + {displayName} + ) : ( +
+ +
+ )} +
+

{displayName}

+

+ {description?.slice(0, 60)} + {description && description.length > 60 ? "..." : ""} +

+
+
+ + {/* Right: npub copy button */} +
- {/* Right: Community icon */} -
-
- - Communikey + {/* Tabs */} + + + {/* Chat tab - always first */} + + + Chat + + + {/* Content section tabs */} + {contentSections.map((section) => { + const FirstKindIcon = section.kinds[0] + ? getKindIcon(section.kinds[0]) + : FileText; + return ( + + + {section.name} + + ); + })} + + {/* Info tab - always last */} + + + Info + + + + {/* Chat content */} + + + +
+ } + > + + + + + {/* Content section tabs */} + {contentSections.map((section) => ( + + + + ))} + + {/* Info content */} + + + + +
+ ); +} + +/** + * ContentSectionFeed - Displays a feed of events for a content section + */ +function ContentSectionFeed({ + section, + communityPubkey, + relays, +}: { + section: ContentSection; + communityPubkey: string; + relays: string[]; +}) { + const { events, loading } = useTimeline( + `communikey-${communityPubkey}-${section.name}`, + { + kinds: section.kinds, + "#h": [communityPubkey], + }, + relays, + { limit: 50 }, + ); + + if (loading && events.length === 0) { + return ( +
+ +
+ ); + } + + if (events.length === 0) { + return ( +
+

No content in this section yet

+

+ Content types: {section.kinds.map((k) => getKindName(k)).join(", ")} +

+
+ ); + } + + return ( +
+ {events.map((event) => ( + + ))} +
+ ); +} + +/** + * FeedEvent - Renders a single event with error boundary + */ +function FeedEvent({ event }: { event: NostrEvent }) { + return ( + + + + ); +} + +/** + * CommunityInfo - Shows detailed community information + */ +function CommunityInfo({ + profile, + displayName, + description, + location, + communityRelays, + contentSections, + blossomServers, + mints, + tos, + communityEvent, + resolvedPubkey, +}: { + profile: any; + displayName: string; + description?: string; + location?: string; + communityRelays: string[]; + contentSections: ContentSection[]; + blossomServers: string[]; + mints: { url: string; protocol?: string }[]; + tos?: { id: string; relay?: string }; + communityEvent?: NostrEvent; + resolvedPubkey: string; +}) { + const { addWindow } = useGrimoire(); + + const viewProfile = () => { + addWindow("profile", { pubkey: resolvedPubkey }); + }; + + return ( +
+ {/* Header */} +
+
+ {profile?.picture ? ( + {displayName} + ) : ( +
+ +
+ )} +
+

{displayName}

+
-
- {/* Main Content */} -
-
- {/* Header */} -
-
-
- {profile?.picture ? ( - {displayName} - ) : ( -
- -
- )} -
-

{displayName}

- -
-
+ {/* Description */} + {description && ( +

+ {description} +

+ )} - -
+ {/* Location */} + {location && ( +
+ + {location} +
+ )} - {/* Description */} - {description && ( -

- {description} -

- )} - - {/* Location */} - {location && ( -
- - {location} -
- )} - - {/* Quick stats */} -
- - - {communityRelays.length}{" "} - {communityRelays.length === 1 ? "relay" : "relays"} - - - - {contentSections.length}{" "} - {contentSections.length === 1 ? "section" : "sections"} - - {blossomServers.length > 0 && ( - - - {blossomServers.length} blossom{" "} - {blossomServers.length === 1 ? "server" : "servers"} - - )} - {mints.length > 0 && ( - - - {mints.length} {mints.length === 1 ? "mint" : "mints"} - - )} -
- - {/* No community config warning */} - {!communityEvent && ( -
-

- No community configuration found (kind 10222). This pubkey may - not have set up a Communikey community yet. -

-
- )} -
- - {/* Content Sections */} - {contentSections.length > 0 && ( -
-

Content Sections

-
- {contentSections.map((section) => ( - - ))} -
-
- )} - - {/* Relays */} - {communityRelays.length > 0 && ( -
-

- - Relays -

-
- {communityRelays.map((relay, index) => ( -
- - {relay} - {index === 0 && ( - - Main - - )} -
- ))} -
-
- )} - - {/* Blossom Servers */} + {/* Quick stats */} +
+ + + {communityRelays.length}{" "} + {communityRelays.length === 1 ? "relay" : "relays"} + + + + {contentSections.length}{" "} + {contentSections.length === 1 ? "section" : "sections"} + {blossomServers.length > 0 && ( -
-

- - Blossom Servers -

-
- {blossomServers.map((server) => ( -
- - {server} -
- ))} -
-
+ + + {blossomServers.length} blossom{" "} + {blossomServers.length === 1 ? "server" : "servers"} + )} - - {/* Ecash Mints */} {mints.length > 0 && ( -
-

- - Ecash Mints -

-
- {mints.map((mint) => ( -
- - {mint.url} - {mint.protocol && ( - - {mint.protocol} - - )} -
- ))} -
-
- )} - - {/* Terms of Service */} - {tos && ( -
-

- - Terms of Service -

-
- {tos.id} - {tos.relay && ( -

- Relay: {tos.relay} -

- )} -
-
+ + + {mints.length} {mints.length === 1 ? "mint" : "mints"} + )}
-
+ + {/* No community config warning */} + {!communityEvent && ( +
+

+ No community configuration found (kind 10222). This pubkey may not + have set up a Communikey community yet. +

+
+ )} + + + {/* Content Sections */} + {contentSections.length > 0 && ( +
+

Content Sections

+
+ {contentSections.map((section) => ( + + ))} +
+
+ )} + + {/* Relays */} + {communityRelays.length > 0 && ( +
+

+ + Relays +

+
+ {communityRelays.map((relay, index) => ( +
+ + {relay} + {index === 0 && ( + + Main + + )} +
+ ))} +
+
+ )} + + {/* Blossom Servers */} + {blossomServers.length > 0 && ( +
+

+ + Blossom Servers +

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

+ + Ecash Mints +

+
+ {mints.map((mint) => ( +
+ + {mint.url} + {mint.protocol && ( + + {mint.protocol} + + )} +
+ ))} +
+
+ )} + + {/* Terms of Service */} + {tos && ( +
+

+ + Terms of Service +

+
+ {tos.id} + {tos.relay && ( +

+ Relay: {tos.relay} +

+ )} +
+
+ )}
); } @@ -385,51 +574,47 @@ export function CommunikeyViewer({ pubkey, relays }: CommunikeyViewerProps) { */ function ContentSectionCard({ section }: { section: ContentSection }) { return ( - - - - {section.name} -
- {section.exclusive && ( - - - Exclusive - - )} - {section.fee && ( - - - {section.fee.amount} {section.fee.unit} - - )} - {section.badgeRequirement && ( - - - Badge Required - - )} -
-
-
- -
- {section.kinds.map((kind) => { - const KindIcon = getKindIcon(kind); - return ( - - - {getKindName(kind)} - ({kind}) - - ); - })} +
+
+ {section.name} +
+ {section.exclusive && ( + + + Exclusive + + )} + {section.fee && ( + + + {section.fee.amount} {section.fee.unit} + + )} + {section.badgeRequirement && ( + + + Badge Required + + )}
- {section.badgeRequirement && ( -

- Requires badge: {section.badgeRequirement} -

- )} - - +
+
+ {section.kinds.map((kind) => { + const KindIcon = getKindIcon(kind); + return ( + + + {getKindName(kind)} + ({kind}) + + ); + })} +
+ {section.badgeRequirement && ( +

+ Requires badge: {section.badgeRequirement} +

+ )} +
); }