From cc95429c576d5b24e4d0573cb03c1696b3873813 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 29 Jul 2026 16:16:58 +0800 Subject: [PATCH 1/2] defer image loading until navigation transition ended --- src/components/DeferredImageWithLoading.tsx | 54 +++++++++++++++++++++ src/components/ImageWithLoading.tsx | 1 + src/components/ImageWithSizeCalculation.tsx | 4 +- src/components/ReceiptImage/index.tsx | 4 +- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/components/DeferredImageWithLoading.tsx diff --git a/src/components/DeferredImageWithLoading.tsx b/src/components/DeferredImageWithLoading.tsx new file mode 100644 index 000000000000..c7f3712f1fd9 --- /dev/null +++ b/src/components/DeferredImageWithLoading.tsx @@ -0,0 +1,54 @@ +import ScreenWrapperStatusContext from '@components/ScreenWrapper/ScreenWrapperStatusContext'; + +import useRunAfterTransitions from '@hooks/useRunAfterTransitions'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import React, {useContext} from 'react'; +import {View} from 'react-native'; + +import type {ImageWithSizeLoadingProps} from './ImageWithLoading'; + +import ImageWithLoading from './ImageWithLoading'; + +/** + * Wrapper around ImageWithLoading that keeps the image out of the render passes happening during a screen's entry + * transition: fetching, decoding and laying out an image competes with the animation, so the screen slides in with an + * empty placeholder of the same size and the image mounts once the transition is over. + * + * The gate is the enclosing ScreenWrapper's `didScreenTransitionEnd`, not `useRunAfterTransitions(true)` alone: an + * incoming screen's subtree mounts (and runs its effects) *before* React Navigation emits `transitionStart`, so at that + * point TransitionTracker has no active transition and would let the image through immediately. + * + * On a screen that has already settled - a receipt arriving in an open chat - `didScreenTransitionEnd` is already true, + * so the image mounts on the very next render with no perceptible delay. + */ +function DeferredImageWithLoading({containerStyles, onLayout, ...rest}: ImageWithSizeLoadingProps) { + const styles = useThemeStyles(); + + // Read the context directly rather than through `useScreenWrapperTransitionStatus`, which throws outside a + // ScreenWrapper. There is no screen entry transition to wait for in that case, so don't hold the image back. + const screenWrapperStatus = useContext(ScreenWrapperStatusContext); + const didScreenTransitionEnd = screenWrapperStatus?.didScreenTransitionEnd ?? true; + + // Also wait out any transition still tracked once the screen has settled, e.g. an overlapping modal or keyboard one. + const shouldRenderImage = useRunAfterTransitions(didScreenTransitionEnd); + + if (!shouldRenderImage) { + return ( + + ); + } + + return ( + + ); +} + +export default DeferredImageWithLoading; diff --git a/src/components/ImageWithLoading.tsx b/src/components/ImageWithLoading.tsx index 508960051142..fcaadda8e235 100644 --- a/src/components/ImageWithLoading.tsx +++ b/src/components/ImageWithLoading.tsx @@ -149,3 +149,4 @@ function ImageWithLoading({ ImageWithLoading.displayName = 'ImageWithLoading'; export default React.memo(ImageWithLoading); +export type {ImageWithSizeLoadingProps}; diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index 7710611bda61..777455c1cb32 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -12,8 +12,8 @@ import React, {useMemo} from 'react'; import type {FullScreenLoadingIndicatorIconSize} from './FullscreenLoadingIndicator'; import type {ImageObjectPosition} from './Image/types'; +import DeferredImageWithLoading from './DeferredImageWithLoading'; import RESIZE_MODES from './Image/resizeModes'; -import ImageWithLoading from './ImageWithLoading'; type OnMeasure = (args: {width: number; height: number}) => void; @@ -95,7 +95,7 @@ function ImageWithSizeCalculation({ }; return ( - { if (e.nativeEvent.layout.width !== receiptImageWidth && e.timeStamp - lastUpdateWidthTimestampRef.current > MIN_UPDATE_WIDTH_DIFF) { setReceiptImageWidth(e.nativeEvent.layout.width); From 305bd9002c43355a5183fe5c88b09a74804889b2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 29 Jul 2026 16:17:20 +0800 Subject: [PATCH 2/2] use the original image source --- src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 8d9d6a7a4a0a..73f46ded020d 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -191,7 +191,7 @@ function ReportActionItemImage({ propsObj = { shouldUseThumbnailImage: shouldUseThumbnailImage ?? true, - source: thumbnailSource, + source: originalImageSource, fallbackIcon: icons.Receipt, fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge, isAuthTokenRequired: true,