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.
This commit is contained in:
Claude
2026-01-16 11:00:11 +00:00
parent 8849d9561e
commit e057760b55

View File

@@ -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<NostrEvent | null>(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]);