Fix reaction loading to use protocol-specific relay hints

Previously MessageReactions was only querying EventStore without actually
fetching reactions from relays. Now it properly:

- Starts a relay subscription per message to fetch kind 7 reactions
- Uses protocol-specific relay hints via getConversationRelays() helper:
  * NIP-29 groups: Single relay from conversation.metadata.relayUrl
  * NIP-53 live chats: Multiple relays from conversation.metadata.liveActivity.relays
- Memoizes relay array in MessageItem to prevent unnecessary re-subscriptions
- Cleans up subscriptions when message unmounts or changes

This ensures reactions are actually fetched and displayed correctly across
different chat protocols.
This commit is contained in:
Claude
2026-01-15 17:04:10 +00:00
parent 96751be135
commit 560a3a3ed9
2 changed files with 72 additions and 16 deletions

View File

@@ -135,6 +135,24 @@ function isLiveActivityMetadata(value: unknown): value is LiveActivityMetadata {
);
}
/**
* Get relay URLs for a conversation based on protocol
* Used for fetching protocol-specific data like reactions
*/
function getConversationRelays(conversation: Conversation): string[] {
// NIP-53 live chats: Use full relay list from liveActivity metadata
if (conversation.protocol === "nip-53") {
const liveActivity = conversation.metadata?.liveActivity;
if (isLiveActivityMetadata(liveActivity) && liveActivity.relays) {
return liveActivity.relays;
}
}
// NIP-29 groups and fallback: Use single relay URL
const relayUrl = conversation.metadata?.relayUrl;
return relayUrl ? [relayUrl] : [];
}
/**
* Get the chat command identifier for a conversation
* Returns a string that can be passed to the `chat` command to open this conversation
@@ -249,6 +267,12 @@ const MessageItem = memo(function MessageItem({
canReply: boolean;
onScrollToMessage?: (messageId: string) => void;
}) {
// Get relays for this conversation (memoized to prevent unnecessary re-subscriptions)
const relays = useMemo(
() => getConversationRelays(conversation),
[conversation],
);
// System messages (join/leave) have special styling
if (message.type === "system") {
return (
@@ -334,10 +358,7 @@ const MessageItem = memo(function MessageItem({
</div>
</div>
{/* Reactions display - lazy loaded per message */}
<MessageReactions
messageId={message.id}
relayUrl={conversation.metadata?.relayUrl}
/>
<MessageReactions messageId={message.id} relays={relays} />
</div>
);
}
@@ -380,10 +401,7 @@ const MessageItem = memo(function MessageItem({
)}
</div>
{/* Reactions display - lazy loaded per message */}
<MessageReactions
messageId={message.id}
relayUrl={conversation.metadata?.relayUrl}
/>
<MessageReactions messageId={message.id} relays={relays} />
</div>
</div>
);

View File

@@ -1,13 +1,14 @@
import { useMemo } from "react";
import { useMemo, useEffect } from "react";
import { use$ } from "applesauce-react/hooks";
import eventStore from "@/services/event-store";
import pool from "@/services/relay-pool";
import { EMOJI_SHORTCODE_REGEX } from "@/lib/emoji-helpers";
import type { NostrEvent } from "@/types/nostr";
interface MessageReactionsProps {
messageId: string;
/** Relay URL for fetching reactions (NIP-29 group relay) */
relayUrl?: string;
/** Relay URLs for fetching reactions - protocol-specific */
relays: string[];
}
interface ReactionSummary {
@@ -26,14 +27,51 @@ interface ReactionSummary {
* Loads kind 7 (reaction) events that reference the messageId via e-tag.
* Aggregates by emoji and displays as tiny inline badges in bottom-right corner.
*
* Uses EventStore timeline for reactive updates - new reactions appear automatically.
* Fetches reactions from protocol-specific relays and uses EventStore timeline
* for reactive updates - new reactions appear automatically.
*/
export function MessageReactions({
messageId,
relayUrl,
}: MessageReactionsProps) {
export function MessageReactions({ messageId, relays }: MessageReactionsProps) {
// Start relay subscription to fetch reactions for this message
useEffect(() => {
if (relays.length === 0) return;
const filter = {
kinds: [7],
"#e": [messageId],
limit: 100, // Reasonable limit for reactions
};
// Subscribe to relays to fetch reactions
const subscription = pool
.subscription(relays, [filter], {
eventStore, // Automatically add reactions to EventStore
})
.subscribe({
next: (response) => {
if (typeof response !== "string") {
// Event received - it's automatically added to EventStore
console.log(
`[MessageReactions] Reaction received for ${messageId.slice(0, 8)}...`,
);
}
},
error: (err) => {
console.error(
`[MessageReactions] Subscription error for ${messageId.slice(0, 8)}...`,
err,
);
},
});
// Cleanup subscription when component unmounts or messageId changes
return () => {
subscription.unsubscribe();
};
}, [messageId, relays]);
// Load reactions for this message from EventStore
// Filter: kind 7, e-tag pointing to messageId
// This observable will update automatically as reactions arrive from the subscription above
const reactions = use$(
() =>
eventStore.timeline({