From 560a3a3ed9eeb180bc78af15f97247e051c1a6a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 17:04:10 +0000 Subject: [PATCH] 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. --- src/components/ChatViewer.tsx | 34 +++++++++++---- src/components/chat/MessageReactions.tsx | 54 ++++++++++++++++++++---- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index cb52b84..9338d8e 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -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({ {/* Reactions display - lazy loaded per message */} - + ); } @@ -380,10 +401,7 @@ const MessageItem = memo(function MessageItem({ )} {/* Reactions display - lazy loaded per message */} - + ); diff --git a/src/components/chat/MessageReactions.tsx b/src/components/chat/MessageReactions.tsx index d0d6f06..28fdaf0 100644 --- a/src/components/chat/MessageReactions.tsx +++ b/src/components/chat/MessageReactions.tsx @@ -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({