From 50f9cc40fedf2c82d8f2af1159377e50a790541a Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Fri, 17 Apr 2026 21:43:47 +0800 Subject: [PATCH] fix(room): load reactions for IDB-cached messages via WS-first fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - thisLoadReactions helper: batch-fetches reactions for loaded messages via WS (RoomWsClient.request() does WS-first → HTTP fallback automatically) - Called after both IDB paths (initial load + loadMore) so reactions are populated even when messages come from IndexedDB cache - Also deduplicated API-path reaction loading to use the same helper --- src/contexts/room-context.tsx | 64 ++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx index cf82794..4dbc3c4 100644 --- a/src/contexts/room-context.tsx +++ b/src/contexts/room-context.tsx @@ -260,6 +260,40 @@ export function RoomProvider({ }; }, [activeRoomId, wsClient]); + /** + * Fetch reactions for a batch of messages via WS (with HTTP fallback), + * then merge them into the messages state. Fires-and-forgets so it + * does not block the caller. + */ + const thisLoadReactions = ( + roomId: string, + client: NonNullable>, + msgs: MessageWithMeta[], + ) => { + const msgIds = msgs.map((m) => m.id); + if (msgIds.length === 0) return; + client + .reactionListBatch(roomId, msgIds) + .then((reactionResults) => { + const reactionMap = new Map(); + for (const result of reactionResults) { + if (result.reactions.length > 0) { + reactionMap.set(result.message_id, result.reactions); + } + } + if (reactionMap.size > 0) { + setMessages((prev) => + prev.map((m) => + reactionMap.has(m.id) ? { ...m, reactions: reactionMap.get(m.id) } : m, + ), + ); + } + }) + .catch(() => { + // Non-fatal: WS push will keep reactions up to date + }); + }; + const loadMore = useCallback( async (cursor?: number | null) => { const client = wsClientRef.current; @@ -284,7 +318,9 @@ export function RoomProvider({ const minSeq = cached[0].seq; setNextCursor(minSeq > 0 ? minSeq - 1 : null); setIsLoadingMore(false); - // No API call needed — WS will push any new messages that arrived while away + // No API call needed — WS will push any new messages that arrived while away. + // Fetch reactions via WS (with HTTP fallback) so reactions appear without extra latency. + thisLoadReactions(activeRoomId, client, cached); return; } } @@ -310,6 +346,8 @@ export function RoomProvider({ setIsHistoryLoaded(true); } setIsLoadingMore(false); + // Also fetch reactions for the IDB-loaded history messages. + thisLoadReactions(activeRoomId, client, idbMessages); return; } // IDB empty for this range — fall through to API @@ -355,28 +393,8 @@ export function RoomProvider({ } setNextCursor(resp.messages.length > 0 ? resp.messages[resp.messages.length - 1].seq : null); - // Fetch reactions for all loaded messages - const msgIds = newMessages.map((m) => m.id); - if (msgIds.length > 0) { - try { - const reactionResults = await client.reactionListBatch(activeRoomId, msgIds); - const reactionMap = new Map(); - for (const result of reactionResults) { - if (result.reactions.length > 0) { - reactionMap.set(result.message_id, result.reactions); - } - } - if (reactionMap.size > 0) { - setMessages((prev) => - prev.map((m) => - reactionMap.has(m.id) ? { ...m, reactions: reactionMap.get(m.id) } : m, - ), - ); - } - } catch { - // Reactions will be loaded via WebSocket updates if backend supports it - } - } + // Fetch reactions for all loaded messages (WS-first with HTTP fallback) + thisLoadReactions(activeRoomId, client, newMessages); } catch (error) { if (abortController.signal.aborted) return; handleRoomError('Load messages', error);