From ddd696a790ff4aa4f664336d17487224f7e8225e Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 23:30:50 +0200 Subject: [PATCH] perf(P6A.T3): memoize MessageBubble + stabilize parent callbacks --- apps/desktop/src/components/MessageBubble.tsx | 35 ++++++++++--- apps/desktop/src/pages/ConversationPage.tsx | 51 ++++++++++++++----- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/components/MessageBubble.tsx b/apps/desktop/src/components/MessageBubble.tsx index 0b1eee9..1756867 100644 --- a/apps/desktop/src/components/MessageBubble.tsx +++ b/apps/desktop/src/components/MessageBubble.tsx @@ -5,7 +5,7 @@ import { softDeleteMessage, } from '@chat-app/shared/chat'; import { extractErrorCode } from '@chat-app/shared/i18n'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -60,8 +60,17 @@ interface Props { senderAvatarUrl?: string | null | undefined; conversationId: string; reactions: AggregatedReaction[]; - onToggleReaction: (emoji: string) => Promise; - onVotePoll?: (emoji: string, optionEmojis: string[]) => Promise; + /** + * Toggle a reaction on this message. Receives the message id so the parent + * can pass a stable handler reference across every row (lets `React.memo` + * actually skip re-renders triggered by composer keystrokes / typing pings). + */ + onToggleReaction: (messageId: string, emoji: string) => Promise; + /** + * Cast/clear an exclusive poll vote. Receives the message id for the same + * reason as `onToggleReaction`. + */ + onVotePoll?: (messageId: string, emoji: string, optionEmojis: string[]) => Promise; showSeen?: boolean; /** Delivery state for own messages: 'sent' / 'delivered' / 'read'. */ deliveryState?: 'sent' | 'delivered' | 'read'; @@ -83,7 +92,7 @@ interface Props { onTogglePin?: (messageId: string) => void; } -export function MessageBubble({ +function MessageBubbleInner({ message, mine, groupedWithPrev, @@ -257,12 +266,12 @@ export function MessageBubble({ async (emoji: string) => { setPickerOpen(false); try { - await onToggleReaction(emoji); + await onToggleReaction(message.id, emoji); } catch (err: unknown) { console.error('toggleReaction failed', err); } }, - [onToggleReaction], + [onToggleReaction, message.id], ); const copyableText = @@ -527,7 +536,9 @@ export function MessageBubble({ reactions={reactions} mine={mine} onVote={(emoji) => - onVotePoll ? onVotePoll(emoji, pollOptionEmojis) : onToggleReaction(emoji) + onVotePoll + ? onVotePoll(message.id, emoji, pollOptionEmojis) + : onToggleReaction(message.id, emoji) } /> ) : ( @@ -593,7 +604,7 @@ export function MessageBubble({