From e057760b55af02b2b6316e1da2fde529d62c11a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Jan 2026 11:00:11 +0000 Subject: [PATCH] fix: Fix e-tag reply resolution for NIP-17 rumor IDs The e-tags in NIP-17 messages reference the innermost event (rumor) IDs, not the gift wrap IDs. Updated ReplyPreview to properly handle this: - Add local state fallback for when eventStore doesn't track synthetic events - Use adapter's loadReplyMessage() return value directly - syntheticEventCache now reliably provides rumor events for reply previews This ensures reply previews work correctly even if eventStore doesn't properly index events with empty signatures. --- src/components/chat/ReplyPreview.tsx | 40 +++++++++++++++++++++------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/chat/ReplyPreview.tsx b/src/components/chat/ReplyPreview.tsx index 197c5e5..5f52d53 100644 --- a/src/components/chat/ReplyPreview.tsx +++ b/src/components/chat/ReplyPreview.tsx @@ -1,10 +1,11 @@ -import { memo, useEffect } from "react"; +import { memo, useEffect, useState } from "react"; import { use$ } from "applesauce-react/hooks"; import eventStore from "@/services/event-store"; import { UserName } from "../nostr/UserName"; import { RichText } from "../nostr/RichText"; import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter"; import type { Conversation } from "@/types/chat"; +import type { NostrEvent } from "@/types/nostr"; interface ReplyPreviewProps { replyToId: string; @@ -16,6 +17,10 @@ interface ReplyPreviewProps { /** * ReplyPreview - Shows who is being replied to with truncated message content * Automatically fetches missing events from protocol-specific relays + * + * For NIP-17 (gift-wrapped DMs), reply targets reference rumor IDs (innermost events). + * These are stored as synthetic events in adapter caches since eventStore may not + * properly index events with empty signatures. */ export const ReplyPreview = memo(function ReplyPreview({ replyToId, @@ -23,18 +28,33 @@ export const ReplyPreview = memo(function ReplyPreview({ conversation, onScrollToMessage, }: ReplyPreviewProps) { - // Load the event being replied to (reactive - updates when event arrives) - const replyEvent = use$(() => eventStore.event(replyToId), [replyToId]); + // State for manually loaded events (NIP-17 synthetic events) + const [manualEvent, setManualEvent] = useState(null); - // Fetch event from relays if not in store + // Load the event being replied to (reactive - updates when event arrives) + const storeEvent = use$(() => eventStore.event(replyToId), [replyToId]); + + // Use store event if available, otherwise fall back to manually loaded event + const replyEvent = storeEvent ?? manualEvent; + + // Fetch event from adapter if not in store useEffect(() => { if (!replyEvent) { - adapter.loadReplyMessage(conversation, replyToId).catch((err) => { - console.error( - `[ReplyPreview] Failed to load reply ${replyToId.slice(0, 8)}:`, - err, - ); - }); + adapter + .loadReplyMessage(conversation, replyToId) + .then((event) => { + if (event) { + // For NIP-17, eventStore may not track synthetic events properly + // Store it in local state to ensure it displays + setManualEvent(event); + } + }) + .catch((err) => { + console.error( + `[ReplyPreview] Failed to load reply ${replyToId.slice(0, 8)}:`, + err, + ); + }); } }, [replyEvent, adapter, conversation, replyToId]);