fix(room): load reactions for IDB-cached messages via WS-first fallback

- 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
This commit is contained in:
ZhenYi 2026-04-17 21:43:47 +08:00
parent b70d91866c
commit 50f9cc40fe

View File

@ -260,6 +260,40 @@ export function RoomProvider({
}; };
}, [activeRoomId, wsClient]); }, [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<ReturnType<typeof wsClientRef.current>>,
msgs: MessageWithMeta[],
) => {
const msgIds = msgs.map((m) => m.id);
if (msgIds.length === 0) return;
client
.reactionListBatch(roomId, msgIds)
.then((reactionResults) => {
const reactionMap = new Map<string, import('@/lib/room-ws-client').ReactionItem[]>();
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( const loadMore = useCallback(
async (cursor?: number | null) => { async (cursor?: number | null) => {
const client = wsClientRef.current; const client = wsClientRef.current;
@ -284,7 +318,9 @@ export function RoomProvider({
const minSeq = cached[0].seq; const minSeq = cached[0].seq;
setNextCursor(minSeq > 0 ? minSeq - 1 : null); setNextCursor(minSeq > 0 ? minSeq - 1 : null);
setIsLoadingMore(false); 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; return;
} }
} }
@ -310,6 +346,8 @@ export function RoomProvider({
setIsHistoryLoaded(true); setIsHistoryLoaded(true);
} }
setIsLoadingMore(false); setIsLoadingMore(false);
// Also fetch reactions for the IDB-loaded history messages.
thisLoadReactions(activeRoomId, client, idbMessages);
return; return;
} }
// IDB empty for this range — fall through to API // 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); setNextCursor(resp.messages.length > 0 ? resp.messages[resp.messages.length - 1].seq : null);
// Fetch reactions for all loaded messages // Fetch reactions for all loaded messages (WS-first with HTTP fallback)
const msgIds = newMessages.map((m) => m.id); thisLoadReactions(activeRoomId, client, newMessages);
if (msgIds.length > 0) {
try {
const reactionResults = await client.reactionListBatch(activeRoomId, msgIds);
const reactionMap = new Map<string, import('@/lib/room-ws-client').ReactionItem[]>();
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
}
}
} catch (error) { } catch (error) {
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
handleRoomError('Load messages', error); handleRoomError('Load messages', error);