From ddb88ab78a4ddf27e098dde6923d778ae76b6f53 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 11:52:18 +0000 Subject: [PATCH] refactor: improve NIP-10 thread chat UX and relay/participant handling UI Improvements: - Remove "thread root" marker from reply previews - treat all replies uniformly - Hide "load older messages" for NIP-10 threads (all replies loaded reactively) - Display "Thread" with note icon instead of "Group" for NIP-10 conversations - Remove debug logging from chat command parser and ChatViewer Participant Management: - Derive participants dynamically from messages (like live-chat does) - Root author (OP) always listed first with "op" role - All unique message authors included in member list - Updates in real-time as new people reply Relay Management: - Expand relay collection to include participant outbox relays - Fetch relays from root author, provided event author, and p-tagged participants - Check up to 5 participants for relay diversity - Increase max relay limit from 7 to 10 for better coverage - Add logging for relay collection debugging This makes NIP-10 threads feel more like proper chat conversations with accurate participant lists and better relay coverage across the thread. --- src/components/ChatViewer.tsx | 88 +++++++++++++++++-------- src/components/chat/ReplyPreview.tsx | 17 ++--- src/lib/chat/adapters/nip-10-adapter.ts | 56 ++++++++++++++-- src/types/man.ts | 5 -- 4 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 8d5c160..b36229d 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -11,6 +11,7 @@ import { Paperclip, Copy, CopyCheck, + FileText, } from "lucide-react"; import { nip19 } from "nostr-tools"; import { getZapRequest } from "applesauce-common/helpers/zap"; @@ -437,10 +438,6 @@ export function ChatViewer({ customTitle, headerPrefix, }: ChatViewerProps) { - console.log("[ChatViewer] Received props:", { - protocol, - identifier: identifier?.type, - }); const { addWindow } = useGrimoire(); // Get active account with signing capability @@ -752,33 +749,63 @@ export function ChatViewer({ ? conversation?.metadata?.liveActivity : undefined; - // Derive participants from messages for live activities (unique pubkeys who have chatted) + // Derive participants from messages for live activities and NIP-10 threads const derivedParticipants = useMemo(() => { - if (conversation?.type !== "live-chat" || !messages) { - return conversation?.participants || []; - } + // NIP-10 threads: derive from messages with OP first + if (protocol === "nip-10" && messages && conversation) { + const rootAuthor = conversation.metadata?.rootEventId + ? messages.find((m) => m.id === conversation.metadata?.rootEventId) + ?.author + : undefined; - const hostPubkey = liveActivity?.hostPubkey; - const participants: { pubkey: string; role: "host" | "member" }[] = []; + const participants: { pubkey: string; role: "op" | "member" }[] = []; - // Host always first - if (hostPubkey) { - participants.push({ pubkey: hostPubkey, role: "host" }); - } - - // Add other participants from messages (excluding host) - const seen = new Set(hostPubkey ? [hostPubkey] : []); - for (const msg of messages) { - if (msg.type !== "system" && !seen.has(msg.author)) { - seen.add(msg.author); - participants.push({ pubkey: msg.author, role: "member" }); + // OP (root author) always first + if (rootAuthor) { + participants.push({ pubkey: rootAuthor, role: "op" }); } + + // Add other participants from messages (excluding OP) + const seen = new Set(rootAuthor ? [rootAuthor] : []); + for (const msg of messages) { + if (msg.type !== "system" && !seen.has(msg.author)) { + seen.add(msg.author); + participants.push({ pubkey: msg.author, role: "member" }); + } + } + + return participants; } - return participants; + // Live activities: derive from messages with host first + if (conversation?.type === "live-chat" && messages) { + const hostPubkey = liveActivity?.hostPubkey; + const participants: { pubkey: string; role: "host" | "member" }[] = []; + + // Host always first + if (hostPubkey) { + participants.push({ pubkey: hostPubkey, role: "host" }); + } + + // Add other participants from messages (excluding host) + const seen = new Set(hostPubkey ? [hostPubkey] : []); + for (const msg of messages) { + if (msg.type !== "system" && !seen.has(msg.author)) { + seen.add(msg.author); + participants.push({ pubkey: msg.author, role: "member" }); + } + } + + return participants; + } + + // Other protocols: use static participants from conversation + return conversation?.participants || []; }, [ + protocol, conversation?.type, conversation?.participants, + conversation?.metadata?.rootEventId, messages, liveActivity?.hostPubkey, ]); @@ -881,9 +908,16 @@ export function ChatViewer({ conversation.type === "live-chat") && ( )} - - {conversation.type} - + {conversation.protocol === "nip-10" ? ( + + + Thread + + ) : ( + + {conversation.type} + + )} {/* Live Activity Status */} {liveActivity?.status && ( @@ -953,7 +987,9 @@ export function ChatViewer({ alignToBottom components={{ Header: () => - hasMore && conversationResult.status === "success" ? ( + hasMore && + conversationResult.status === "success" && + protocol !== "nip-10" ? (