From 287be81632c15fb88fe916729d9b75ae1d5586b3 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Jul 2026 14:08:01 +0200 Subject: [PATCH 1/3] perf: prevent recycling remounting in MessageContent descendants --- package/src/components/Attachment/FileAttachmentGroup.tsx | 4 ++-- package/src/components/Attachment/Gallery.tsx | 2 +- .../src/components/Message/MessageItemView/MessageContent.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package/src/components/Attachment/FileAttachmentGroup.tsx b/package/src/components/Attachment/FileAttachmentGroup.tsx index 84374798b3..d89cb934c3 100644 --- a/package/src/components/Attachment/FileAttachmentGroup.tsx +++ b/package/src/components/Attachment/FileAttachmentGroup.tsx @@ -17,7 +17,7 @@ export type FileAttachmentGroupPropsWithContext = Pick { - const { files, message, styles: stylesProp = {} } = props; + const { files, styles: stylesProp = {} } = props; const { Attachment } = useComponentsContext(); const { @@ -32,7 +32,7 @@ const FileAttachmentGroupWithContext = (props: FileAttachmentGroupPropsWithConte {files.map((file, index) => ( diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index 5a1b0bd103..b675a24725 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -272,7 +272,7 @@ const GalleryThumbnail = ({ accessibilityLabel={thumbnailAccessibilityLabel} accessibilityRole='button' disabled={preventPress} - key={`gallery-item-${message.id}/${colIndex}/${rowIndex}/${imagesAndVideos.length}`} + key={`gallery-item-${colIndex}/${rowIndex}`} ref={thumbnailRef} onLongPress={(event) => { if (onLongPress) { diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx index 716dd4d651..8f699f06ee 100644 --- a/package/src/components/Message/MessageItemView/MessageContent.tsx +++ b/package/src/components/Message/MessageItemView/MessageContent.tsx @@ -281,7 +281,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { ); case 'attachments': return otherAttachments.map((attachment, attachmentIndex) => ( - + )); case 'files': return ( @@ -297,7 +297,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { const pollId = message.poll_id; const poll = pollId && client.polls.fromState(pollId); return pollId && poll ? ( - + ) : null; } case 'location': From a451f4caf3dc5702dbc5771b96321291cd3e4266 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: Thu, 2 Jul 2026 14:25:02 +0200 Subject: [PATCH 2/3] perf: optimize getItemType for MessageFlashList --- .../MessageList/MessageFlashList.tsx | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 7c6d75a341..58447e524d 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -55,9 +55,11 @@ import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext' import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; import { useStableCallback, useStateStore } from '../../hooks'; +import { isVideoPlayerAvailable } from '../../native'; import { bumpOverlayLayoutRevision, useHasActiveId } from '../../state-store'; import { MessageInputHeightState } from '../../state-store/message-input-height-store'; import { primitives } from '../../theme'; +import { FileTypes } from '../../types/types'; import { transitions } from '../../utils/animations/transitions'; import { MessageWrapper } from '../Message/MessageItemView/MessageWrapper'; import { excludeCanceledUploadNotifications } from '../Notifications/notificationFilters'; @@ -222,10 +224,56 @@ type MessageFlashListPropsWithContext = Pick< const WAIT_FOR_SCROLL_TIMEOUT = 0; +// Classify an attachment bearing message by its primary shape so FlashList only +// recycles same shaped cells (means less work to rerender). Gallery/media is the +// heaviest subtree to mount, so we short circuit to it as soon as we see one gallery +// image/video nad this keeps gallery cells recycling only with other gallery cells, +// so the Gallery subtree reconciles on rebind instead of unmount & remount. Mirrors +// the attachment categorization in Message. +const getAttachmentItemType = (message: LocalMessage) => { + const attachments = message.attachments ?? []; + let hasGiphy = false; + let hasFile = false; + let hasCard = false; + for (const attachment of attachments) { + const isGalleryImage = + attachment.type === FileTypes.Image && + !attachment.og_scrape_url && + !attachment.title_link && + (!!attachment.image_url || !!attachment.thumb_url); + const isGalleryVideo = + attachment.type === FileTypes.Video && !attachment.og_scrape_url && isVideoPlayerAvailable(); + if (isGalleryImage || isGalleryVideo) { + return 'message-with-gallery'; + } + if (attachment.type === FileTypes.Giphy) { + hasGiphy = true; + } else if ( + attachment.type === FileTypes.File || + attachment.type === FileTypes.Audio || + attachment.type === FileTypes.VoiceRecording + ) { + hasFile = true; + } else if (attachment.og_scrape_url || attachment.title_link) { + hasCard = true; + } + } + if (hasGiphy) { + return 'message-with-giphy'; + } + if (hasFile) { + return 'message-with-file'; + } + if (hasCard) { + return 'message-with-card'; + } + return 'message-with-attachments'; +}; + const getItemTypeInternal = (message: LocalMessage) => { if (message.type === 'regular') { if ((message.attachments?.length ?? 0) > 0) { - return 'message-with-attachments'; + return getAttachmentItemType(message); } if (message.poll_id) { @@ -1079,7 +1127,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => Date: Thu, 2 Jul 2026 14:47:42 +0200 Subject: [PATCH 3/3] perf: include audio attachments as separate pool --- package/src/components/MessageList/MessageFlashList.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index 58447e524d..46c99b714e 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -233,6 +233,7 @@ const WAIT_FOR_SCROLL_TIMEOUT = 0; const getAttachmentItemType = (message: LocalMessage) => { const attachments = message.attachments ?? []; let hasGiphy = false; + let hasAudio = false; let hasFile = false; let hasCard = false; for (const attachment of attachments) { @@ -249,10 +250,11 @@ const getAttachmentItemType = (message: LocalMessage) => { if (attachment.type === FileTypes.Giphy) { hasGiphy = true; } else if ( - attachment.type === FileTypes.File || attachment.type === FileTypes.Audio || attachment.type === FileTypes.VoiceRecording ) { + hasAudio = true; + } else if (attachment.type === FileTypes.File) { hasFile = true; } else if (attachment.og_scrape_url || attachment.title_link) { hasCard = true; @@ -261,6 +263,9 @@ const getAttachmentItemType = (message: LocalMessage) => { if (hasGiphy) { return 'message-with-giphy'; } + if (hasAudio) { + return 'message-with-audio'; + } if (hasFile) { return 'message-with-file'; }