mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 17:19:27 +02:00
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.
This commit is contained in:
@@ -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") && (
|
||||
<span className="text-primary-foreground/60">•</span>
|
||||
)}
|
||||
<span className="capitalize text-primary-foreground/80">
|
||||
{conversation.type}
|
||||
</span>
|
||||
{conversation.protocol === "nip-10" ? (
|
||||
<span className="flex items-center gap-1 text-primary-foreground/80">
|
||||
<FileText className="size-3" />
|
||||
Thread
|
||||
</span>
|
||||
) : (
|
||||
<span className="capitalize text-primary-foreground/80">
|
||||
{conversation.type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 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" ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<Button
|
||||
onClick={handleLoadOlder}
|
||||
|
||||
@@ -26,9 +26,6 @@ export const ReplyPreview = memo(function ReplyPreview({
|
||||
// Load the event being replied to (reactive - updates when event arrives)
|
||||
const replyEvent = use$(() => eventStore.event(replyToId), [replyToId]);
|
||||
|
||||
// Check if replying to thread root (NIP-10)
|
||||
const isRoot = conversation.metadata?.rootEventId === replyToId;
|
||||
|
||||
// Fetch event from relays if not in store
|
||||
useEffect(() => {
|
||||
if (!replyEvent) {
|
||||
@@ -50,7 +47,7 @@ export const ReplyPreview = memo(function ReplyPreview({
|
||||
if (!replyEvent) {
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground mb-0.5">
|
||||
↳ Replying to {isRoot ? "thread root" : replyToId.slice(0, 8)}...
|
||||
↳ Replying to {replyToId.slice(0, 8)}...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,14 +59,10 @@ export const ReplyPreview = memo(function ReplyPreview({
|
||||
title="Click to scroll to message"
|
||||
>
|
||||
<span className="flex-shrink-0">↳</span>
|
||||
{isRoot ? (
|
||||
<span className="font-medium flex-shrink-0">thread root</span>
|
||||
) : (
|
||||
<UserName
|
||||
pubkey={replyEvent.pubkey}
|
||||
className="font-medium flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<UserName
|
||||
pubkey={replyEvent.pubkey}
|
||||
className="font-medium flex-shrink-0"
|
||||
/>
|
||||
<div className="line-clamp-1 overflow-hidden flex-1 min-w-0">
|
||||
<RichText
|
||||
event={replyEvent}
|
||||
|
||||
@@ -681,10 +681,11 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
|
||||
/**
|
||||
* Determine best relays for the thread
|
||||
* Includes relays from root author, provided event author, p-tagged participants, and active user
|
||||
*/
|
||||
private async getThreadRelays(
|
||||
rootEvent: NostrEvent,
|
||||
_providedEvent: NostrEvent,
|
||||
providedEvent: NostrEvent,
|
||||
providedRelays: string[],
|
||||
): Promise<string[]> {
|
||||
const relays = new Set<string>();
|
||||
@@ -692,7 +693,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
// 1. Provided relay hints
|
||||
providedRelays.forEach((r) => relays.add(normalizeURL(r)));
|
||||
|
||||
// 2. Root author's outbox relays (NIP-65)
|
||||
// 2. Root author's outbox relays (NIP-65) - highest priority
|
||||
try {
|
||||
const rootOutbox = await this.getOutboxRelays(rootEvent.pubkey);
|
||||
rootOutbox.slice(0, 3).forEach((r) => relays.add(normalizeURL(r)));
|
||||
@@ -700,9 +701,46 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
console.warn("[NIP-10] Failed to get root author outbox:", err);
|
||||
}
|
||||
|
||||
// 3. Active user's outbox (for publishing replies)
|
||||
// 3. Collect unique participant pubkeys from both events' p-tags
|
||||
const participantPubkeys = new Set<string>();
|
||||
|
||||
// Add p-tags from root event
|
||||
for (const tag of rootEvent.tags) {
|
||||
if (tag[0] === "p" && tag[1]) {
|
||||
participantPubkeys.add(tag[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add p-tags from provided event
|
||||
for (const tag of providedEvent.tags) {
|
||||
if (tag[0] === "p" && tag[1]) {
|
||||
participantPubkeys.add(tag[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add provided event author if different from root
|
||||
if (providedEvent.pubkey !== rootEvent.pubkey) {
|
||||
participantPubkeys.add(providedEvent.pubkey);
|
||||
}
|
||||
|
||||
// 4. Fetch outbox relays from participant subset (limit to avoid slowdown)
|
||||
// Take first 5 participants to get relay diversity without excessive fetching
|
||||
const participantsToCheck = Array.from(participantPubkeys).slice(0, 5);
|
||||
for (const pubkey of participantsToCheck) {
|
||||
try {
|
||||
const outbox = await this.getOutboxRelays(pubkey);
|
||||
// Add 1 relay from each participant for diversity
|
||||
if (outbox.length > 0) {
|
||||
relays.add(normalizeURL(outbox[0]));
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently continue if participant has no relay list
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Active user's outbox (for publishing replies)
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (activePubkey) {
|
||||
if (activePubkey && !participantPubkeys.has(activePubkey)) {
|
||||
try {
|
||||
const userOutbox = await this.getOutboxRelays(activePubkey);
|
||||
userOutbox.slice(0, 2).forEach((r) => relays.add(normalizeURL(r)));
|
||||
@@ -711,7 +749,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback to popular relays if we have too few
|
||||
// 6. Fallback to popular relays if we have too few
|
||||
if (relays.size < 3) {
|
||||
[
|
||||
"wss://relay.damus.io",
|
||||
@@ -720,8 +758,12 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
].forEach((r) => relays.add(r));
|
||||
}
|
||||
|
||||
// Limit to 7 relays max for performance
|
||||
return Array.from(relays).slice(0, 7);
|
||||
console.log(
|
||||
`[NIP-10] Collected ${relays.size} relays from root, ${participantsToCheck.length} participants, and fallbacks`,
|
||||
);
|
||||
|
||||
// Limit to 10 relays max for performance
|
||||
return Array.from(relays).slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -580,12 +580,7 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
appId: "chat",
|
||||
category: "Nostr",
|
||||
argParser: async (args: string[]) => {
|
||||
console.log("[chat argParser] Input args:", args);
|
||||
const result = parseChatCommand(args);
|
||||
console.log("[chat argParser] Parsed result:", {
|
||||
protocol: result.protocol,
|
||||
identifier: result.identifier,
|
||||
});
|
||||
return {
|
||||
protocol: result.protocol,
|
||||
identifier: result.identifier,
|
||||
|
||||
Reference in New Issue
Block a user