Skip to content
Merged
53 changes: 53 additions & 0 deletions src/components/DeferredImageWithLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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';
import ScreenWrapperStatusContext from './ScreenWrapper/ScreenWrapperStatusContext';

/**
* 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 (
<View
style={[styles.w100, styles.h100, containerStyles]}
onLayout={onLayout}
/>
);
}

return (
<ImageWithLoading
containerStyles={containerStyles}
onLayout={onLayout}
{...rest}
/>
);
}

export default DeferredImageWithLoading;
12 changes: 4 additions & 8 deletions src/components/ImageWithLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ function ImageWithLoading({
const isLoadedRef = useRef<boolean | null>(null);
const [isImageCached, setIsImageCached] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isThumbnailLoading, setIsThumbnailLoading] = useState(!!previewUri);
const {isOffline} = useNetwork();

const handleError = () => {
Expand Down Expand Up @@ -98,12 +97,9 @@ function ImageWithLoading({
<Image
{...rest}
source={{uri: previewUri}}
style={[styles.w100, styles.h100, style]}
style={[styles.w100, styles.h100, styles.opacitySemiTransparent, style]}
resizeMode={resizeMode}
onLoad={(e) => {
setIsThumbnailLoading(false);
onLoad?.(e);
}}
onLoad={onLoad}
loadingIconSize={loadingIconSize}
loadingIndicatorStyles={loadingIndicatorStyles}
/>
Expand All @@ -129,13 +125,12 @@ function ImageWithLoading({
isLoadedRef.current = false;
setIsImageCached(false);
setIsLoading(true);
setIsThumbnailLoading(!!previewUri);
waitForSession?.();
}}
loadingIconSize={loadingIconSize}
loadingIndicatorStyles={loadingIndicatorStyles}
/>
{isLoading && (!previewUri || isThumbnailLoading) && !isImageCached && !isOffline && (
{(previewUri ? isLoading : isLoading && !isImageCached) && !isOffline && (
<LoadingIndicator
iconSize={loadingIconSize}
style={[styles.opacity1, styles.bgTransparent, loadingIndicatorStyles]}
Expand All @@ -149,3 +144,4 @@ function ImageWithLoading({
ImageWithLoading.displayName = 'ImageWithLoading';

export default React.memo(ImageWithLoading);
export type {ImageWithSizeLoadingProps};
4 changes: 2 additions & 2 deletions src/components/ImageWithSizeCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,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;

Expand Down Expand Up @@ -90,7 +90,7 @@ function ImageWithSizeCalculation({
};

return (
<ImageWithLoading
<DeferredImageWithLoading
containerStyles={[styles.w100, styles.h100, style]}
style={[styles.w100, styles.h100]}
source={source}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ReceiptImage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import DeferredImageWithLoading from '@components/DeferredImageWithLoading';
import EReceiptStaticThumbnail from '@components/EReceiptStaticThumbnail';
import EReceiptThumbnail from '@components/EReceiptThumbnail';
import type {IconSize} from '@components/EReceiptThumbnail';
import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation';
import type {FullScreenLoadingIndicatorIconSize} from '@components/FullscreenLoadingIndicator';
import ImageWithLoading from '@components/ImageWithLoading';
import ReceiptEmptyState from '@components/ReceiptEmptyState';
import LocalPDFReceiptPreview from '@components/ReportActionItem/LocalPDFReceiptPreview';
import type {TransactionListItemType} from '@components/Search/SearchList/ListItem/types';
Expand Down Expand Up @@ -249,7 +249,7 @@ function ReceiptImage({
}

return (
<ImageWithLoading
<DeferredImageWithLoading
onLayout={(e) => {
if (e.nativeEvent.layout.width !== receiptImageWidth && e.timeStamp - lastUpdateWidthTimestampRef.current > MIN_UPDATE_WIDTH_DIFF) {
setReceiptImageWidth(e.nativeEvent.layout.width);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function ReportActionItemImage({
propsObj = {
shouldUseThumbnailImage: shouldUseThumbnailImage ?? true,

source: thumbnailSource,
source: isPDF || shouldUseThumbnailImage !== false ? thumbnailSource : originalImageSource,
fallbackIcon: icons.Receipt,
fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge,
isAuthTokenRequired: true,
Expand Down
Loading