mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
feat: add kind 10009 (Public Chats) renderer with clickable group links
Implements rendering of NIP-51 kind 10009 (Public Chats list) events, displaying NIP-29 groups similar to how relay lists are rendered. Components: - GroupLink: Clickable group component with icon and name - Fetches kind 39000 metadata from EventStore for group info - Displays group icon (if available) or chat icon fallback - Opens chat window on click with relay'group-id identifier - PublicChatsRenderer: Renderer for kind 10009 events - Extracts "group" tags: ["group", "<group-id>", "<relay-url>"] - Displays each group as clickable link - Similar styling to relay list renderers Integration: - Registered kind 10009 renderer in kinds index - Opens chat with NIP-29 protocol on click Fixes: - Fix missing type imports in NIP-29 adapter (Participant, ParticipantRole) - Fix relayUrl type error in sendMessage (use validated variable) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
94
src/components/nostr/GroupLink.tsx
Normal file
94
src/components/nostr/GroupLink.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-crosshair hover:bg-muted/50 rounded px-1 py-0.5 transition-colors",
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1 overflow-hidden">
|
||||
{groupIcon ? (
|
||||
<img
|
||||
src={groupIcon}
|
||||
alt=""
|
||||
className={cn("size-4 flex-shrink-0 rounded-sm", iconClassname)}
|
||||
/>
|
||||
) : (
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
"size-4 flex-shrink-0 text-muted-foreground",
|
||||
iconClassname,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs truncate">{displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/nostr/kinds/PublicChatsRenderer.tsx
Normal file
57
src/components/nostr/kinds/PublicChatsRenderer.tsx
Normal file
@@ -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", "<group-id>", "<relay-url>", ...]
|
||||
*/
|
||||
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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="text-xs text-muted-foreground italic">
|
||||
No public chats configured
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{groups.map((group) => (
|
||||
<GroupLink
|
||||
key={`${group.relayUrl}'${group.groupId}`}
|
||||
groupId={group.groupId}
|
||||
relayUrl={group.relayUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
@@ -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<number, React.ComponentType<BaseEventProps>> = {
|
||||
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)
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user