mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
feat: batch-load group metadata and handle unmanaged "_" groups
Improvements to kind 10009 (Public Chats) renderer: Batch Loading: - PublicChatsRenderer now batch-loads all group metadata (kind 39000) in a single subscription - Much more efficient than individual subscriptions per group - Filters out "_" groups from metadata fetch (they don't have metadata) - Creates a metadata map to pass to each GroupLink Special "_" Group Handling: - "_" represents the unmanaged relay top-level group - Displays the relay name instead of group ID - Example: "pyramid.fiatjaf.com" instead of "_" GroupLink Updates: - Accepts optional metadata prop (pre-loaded from parent) - No longer fetches metadata individually (more efficient) - Extracts group name and icon from provided metadata - Falls back to group ID if metadata not available Performance: - Single subscription for all groups vs N subscriptions - Reduces relay traffic and improves rendering speed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,16 +123,9 @@ export function ChatViewer({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header with conversation info and controls */}
|
||||
<div className="px-1 border-b w-full">
|
||||
<div className="px-4 border-b w-full py-0.5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex flex-1 min-w-0 items-center gap-2">
|
||||
{conversation.metadata?.icon && (
|
||||
<img
|
||||
src={conversation.metadata.icon}
|
||||
alt={conversation.title}
|
||||
className="h-4 w-4 object-cover flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 flex flex-row gap-2 items-baseline min-w-0">
|
||||
<h2 className="truncate text-base font-semibold">
|
||||
{customTitle || conversation.title}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
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";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
|
||||
/**
|
||||
* Format group identifier for display
|
||||
* Shows just the group-id part without the relay URL
|
||||
* Format relay URL for display
|
||||
* Removes protocol and trailing slash
|
||||
*/
|
||||
function formatGroupIdForDisplay(groupId: string): string {
|
||||
return groupId;
|
||||
function formatRelayForDisplay(url: string): string {
|
||||
return url.replace(/^wss?:\/\//, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export interface GroupLinkProps {
|
||||
groupId: string;
|
||||
relayUrl: string;
|
||||
metadata?: NostrEvent; // Optional pre-loaded metadata
|
||||
className?: string;
|
||||
iconClassname?: string;
|
||||
}
|
||||
@@ -25,35 +24,36 @@ export interface GroupLinkProps {
|
||||
* GroupLink - Clickable NIP-29 group component
|
||||
* Displays group name (from kind 39000 metadata) or group ID
|
||||
* Opens chat window on click
|
||||
*
|
||||
* Special case: "_" group ID represents the unmanaged relay top-level group
|
||||
*/
|
||||
export function GroupLink({
|
||||
groupId,
|
||||
relayUrl,
|
||||
metadata,
|
||||
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],
|
||||
);
|
||||
// Handle special case: "_" is the unmanaged relay top-level group
|
||||
const isUnmanagedGroup = groupId === "_";
|
||||
|
||||
// Extract group name from metadata if available
|
||||
const groupName =
|
||||
groupMetadata && groupMetadata.kind === 39000
|
||||
? getTagValue(groupMetadata, "name") || groupId
|
||||
: groupId;
|
||||
let groupName: string;
|
||||
if (isUnmanagedGroup) {
|
||||
// For "_" groups, show the relay name
|
||||
groupName = formatRelayForDisplay(relayUrl);
|
||||
} else if (metadata && metadata.kind === 39000) {
|
||||
groupName = getTagValue(metadata, "name") || groupId;
|
||||
} else {
|
||||
groupName = groupId;
|
||||
}
|
||||
|
||||
// Extract group icon if available
|
||||
// Extract group icon if available (not applicable for "_" groups)
|
||||
const groupIcon =
|
||||
groupMetadata && groupMetadata.kind === 39000
|
||||
? getTagValue(groupMetadata, "picture")
|
||||
!isUnmanagedGroup && metadata && metadata.kind === 39000
|
||||
? getTagValue(metadata, "picture")
|
||||
: undefined;
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -68,8 +68,6 @@ export function GroupLink({
|
||||
});
|
||||
};
|
||||
|
||||
const displayName = formatGroupIdForDisplay(groupName);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -93,7 +91,7 @@ export function GroupLink({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs truncate">{displayName}</span>
|
||||
<span className="text-xs truncate">{groupName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import { map } from "rxjs/operators";
|
||||
import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer";
|
||||
import { GroupLink } from "../GroupLink";
|
||||
import eventStore from "@/services/event-store";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
|
||||
/**
|
||||
* Extract group references from a kind 10009 event
|
||||
@@ -27,10 +31,35 @@ function extractGroups(event: { tags: string[][] }): Array<{
|
||||
* Public Chats Renderer (Kind 10009)
|
||||
* NIP-51 list of NIP-29 groups
|
||||
* Displays each group as a clickable link with icon and name
|
||||
* Batch-loads metadata for all groups to show their names
|
||||
*/
|
||||
export function PublicChatsRenderer({ event }: BaseEventProps) {
|
||||
const groups = extractGroups(event);
|
||||
|
||||
// Batch-load metadata for all groups at once
|
||||
// Filter out "_" which is the unmanaged relay group (doesn't have metadata)
|
||||
const groupIds = groups.map((g) => g.groupId).filter((id) => id !== "_");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return metadataMap;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
[groupIds.join(",")],
|
||||
);
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
@@ -49,6 +78,7 @@ export function PublicChatsRenderer({ event }: BaseEventProps) {
|
||||
key={`${group.relayUrl}'${group.groupId}`}
|
||||
groupId={group.groupId}
|
||||
relayUrl={group.relayUrl}
|
||||
metadata={groupMetadataMap?.get(group.groupId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user