mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat(chat): add Communikey support to group list viewer
Updates PublicChatsRenderer and GroupLink to properly display Communikeys: - PublicChatsRenderer now detects Communikeys (pubkey-based groups) - Splits groups into Communikeys vs regular NIP-29 groups - Fetches kind 0 (profile) for Communikeys - Fetches kind 39000 (group metadata) for NIP-29 groups - Merges both metadata types into single map - GroupLink now handles both metadata types - kind 39000: NIP-29 group metadata (name, picture from tags) - kind 0: Communikey profile (name, picture from JSON content) - Automatically detects and opens with correct protocol This enables Communikeys stored in kind 10009 group lists to show proper names and pictures from their profile metadata, just like regular NIP-29 groups.
This commit is contained in:
@@ -16,15 +16,15 @@ function formatRelayForDisplay(url: string): string {
|
||||
export interface GroupLinkProps {
|
||||
groupId: string;
|
||||
relayUrl: string;
|
||||
metadata?: NostrEvent; // Optional pre-loaded metadata
|
||||
metadata?: NostrEvent; // Optional pre-loaded metadata (kind 39000 for NIP-29, kind 0 for Communikey)
|
||||
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
|
||||
* GroupLink - Clickable group component for NIP-29 and Communikey
|
||||
* Displays group name from metadata (kind 39000 for NIP-29, kind 0 for Communikey) or group ID
|
||||
* Opens chat window on click with automatic protocol detection
|
||||
*
|
||||
* Special case: "_" group ID represents the unmanaged relay top-level group
|
||||
*/
|
||||
@@ -42,21 +42,32 @@ export function GroupLink({
|
||||
|
||||
// Extract group name from metadata if available
|
||||
let groupName: string;
|
||||
let groupIcon: string | undefined;
|
||||
|
||||
if (isUnmanagedGroup) {
|
||||
// For "_" groups, show the relay name
|
||||
groupName = formatRelayForDisplay(relayUrl);
|
||||
} else if (metadata && metadata.kind === 39000) {
|
||||
groupName = getTagValue(metadata, "name") || groupId;
|
||||
} else if (metadata) {
|
||||
if (metadata.kind === 39000) {
|
||||
// NIP-29 group metadata
|
||||
groupName = getTagValue(metadata, "name") || groupId;
|
||||
groupIcon = getTagValue(metadata, "picture");
|
||||
} else if (metadata.kind === 0) {
|
||||
// Communikey profile metadata
|
||||
try {
|
||||
const profile = JSON.parse(metadata.content);
|
||||
groupName = profile.name || groupId;
|
||||
groupIcon = profile.picture;
|
||||
} catch {
|
||||
groupName = groupId;
|
||||
}
|
||||
} else {
|
||||
groupName = groupId;
|
||||
}
|
||||
} else {
|
||||
groupName = groupId;
|
||||
}
|
||||
|
||||
// Extract group icon if available (not applicable for "_" groups)
|
||||
const groupIcon =
|
||||
!isUnmanagedGroup && metadata && metadata.kind === 39000
|
||||
? getTagValue(metadata, "picture")
|
||||
: undefined;
|
||||
|
||||
const handleClick = async () => {
|
||||
// Check if this is a Communikey (group ID is pubkey with kind 10222)
|
||||
if (await isCommunikey(groupId, [relayUrl])) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer";
|
||||
import { GroupLink } from "../GroupLink";
|
||||
import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
import { isValidPubkey } from "@/lib/chat-parser";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
|
||||
/**
|
||||
@@ -31,42 +32,60 @@ function extractGroups(event: { tags: string[][] }): Array<{
|
||||
|
||||
/**
|
||||
* Public Chats Renderer (Kind 10009)
|
||||
* NIP-51 list of NIP-29 groups
|
||||
* NIP-51 list of NIP-29 groups and Communikeys
|
||||
* Displays each group as a clickable link with icon and name
|
||||
* Batch-loads metadata for all groups to show their names
|
||||
* For Communikeys (pubkey-based groups), fetches kind 0 (profile) metadata
|
||||
* For regular NIP-29 groups, fetches kind 39000 (group metadata)
|
||||
*/
|
||||
export function PublicChatsRenderer({ event }: BaseEventProps) {
|
||||
const groups = extractGroups(event);
|
||||
|
||||
// Batch-load metadata for all groups at once
|
||||
// Split groups into Communikeys (valid pubkeys) and regular NIP-29 groups
|
||||
// Filter out "_" which is the unmanaged relay group (doesn't have metadata)
|
||||
const groupIds = groups.map((g) => g.groupId).filter((id) => id !== "_");
|
||||
const communikeyGroups = groups.filter(
|
||||
(g) => g.groupId !== "_" && isValidPubkey(g.groupId),
|
||||
);
|
||||
const nip29Groups = groups.filter(
|
||||
(g) => g.groupId !== "_" && !isValidPubkey(g.groupId),
|
||||
);
|
||||
|
||||
const communikeyPubkeys = communikeyGroups.map((g) => g.groupId);
|
||||
const nip29GroupIds = nip29Groups.map((g) => g.groupId);
|
||||
|
||||
// Subscribe to relays to fetch group metadata
|
||||
// Extract unique relay URLs from groups
|
||||
const relayUrls = Array.from(new Set(groups.map((g) => g.relayUrl)));
|
||||
|
||||
useEffect(() => {
|
||||
if (groupIds.length === 0) return;
|
||||
if (communikeyPubkeys.length === 0 && nip29GroupIds.length === 0) return;
|
||||
|
||||
console.log(
|
||||
`[PublicChatsRenderer] Fetching metadata for ${groupIds.length} groups from ${relayUrls.length} relays`,
|
||||
`[PublicChatsRenderer] Fetching metadata for ${communikeyPubkeys.length} Communikeys and ${nip29GroupIds.length} NIP-29 groups from ${relayUrls.length} relays`,
|
||||
);
|
||||
|
||||
// Subscribe to fetch metadata events (kind 39000) from the group relays
|
||||
// Build filters for both types
|
||||
const filters = [];
|
||||
|
||||
// Fetch kind 0 (profiles) for Communikeys
|
||||
if (communikeyPubkeys.length > 0) {
|
||||
filters.push({ kinds: [0], authors: communikeyPubkeys });
|
||||
}
|
||||
|
||||
// Fetch kind 39000 (group metadata) for regular NIP-29 groups
|
||||
if (nip29GroupIds.length > 0) {
|
||||
filters.push({ kinds: [39000], "#d": nip29GroupIds });
|
||||
}
|
||||
|
||||
// Subscribe to fetch metadata from the group relays
|
||||
const subscription = pool
|
||||
.subscription(
|
||||
relayUrls,
|
||||
[{ kinds: [39000], "#d": groupIds }],
|
||||
{ eventStore }, // Automatically add to store
|
||||
)
|
||||
.subscription(relayUrls, filters, { eventStore })
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
if (typeof response === "string") {
|
||||
console.log("[PublicChatsRenderer] EOSE received for metadata");
|
||||
} else {
|
||||
console.log(
|
||||
`[PublicChatsRenderer] Received metadata: ${response.id.slice(0, 8)}...`,
|
||||
`[PublicChatsRenderer] Received metadata k${response.kind}: ${response.id.slice(0, 8)}...`,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -75,26 +94,47 @@ export function PublicChatsRenderer({ event }: BaseEventProps) {
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [groupIds.join(","), relayUrls.join(",")]);
|
||||
}, [
|
||||
communikeyPubkeys.join(","),
|
||||
nip29GroupIds.join(","),
|
||||
relayUrls.join(","),
|
||||
]);
|
||||
|
||||
// Build combined metadata map from both kind 0 (Communikeys) and kind 39000 (NIP-29)
|
||||
const groupMetadataMap = use$(
|
||||
() =>
|
||||
groupIds.length > 0
|
||||
? eventStore.timeline([{ kinds: [39000], "#d": groupIds }]).pipe(
|
||||
map((events) => {
|
||||
const metadataMap = new Map<string, NostrEvent>();
|
||||
for (const evt of events) {
|
||||
// Extract group ID from #d tag
|
||||
const dTag = evt.tags.find((t) => t[0] === "d");
|
||||
if (dTag && dTag[1]) {
|
||||
metadataMap.set(dTag[1], evt);
|
||||
communikeyPubkeys.length > 0 || nip29GroupIds.length > 0
|
||||
? eventStore
|
||||
.timeline([
|
||||
...(communikeyPubkeys.length > 0
|
||||
? [{ kinds: [0], authors: communikeyPubkeys }]
|
||||
: []),
|
||||
...(nip29GroupIds.length > 0
|
||||
? [{ kinds: [39000], "#d": nip29GroupIds }]
|
||||
: []),
|
||||
])
|
||||
.pipe(
|
||||
map((events) => {
|
||||
const metadataMap = new Map<string, NostrEvent>();
|
||||
|
||||
for (const evt of events) {
|
||||
if (evt.kind === 0) {
|
||||
// Communikey profile (kind 0) - map by pubkey
|
||||
metadataMap.set(evt.pubkey, evt);
|
||||
} else if (evt.kind === 39000) {
|
||||
// NIP-29 group metadata - map by d-tag (group ID)
|
||||
const dTag = evt.tags.find((t) => t[0] === "d");
|
||||
if (dTag && dTag[1]) {
|
||||
metadataMap.set(dTag[1], evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadataMap;
|
||||
}),
|
||||
)
|
||||
|
||||
return metadataMap;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
[groupIds.join(",")],
|
||||
[communikeyPubkeys.join(","), nip29GroupIds.join(",")],
|
||||
);
|
||||
|
||||
if (groups.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user