Skip to content

Commit e25caee

Browse files
authored
Merge pull request #88575 from callstack-internal/feat/show-thumbnail
Show thumbnail preview while receipt image loads
2 parents 1a2d167 + 32757f0 commit e25caee

5 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/components/ImageWithLoading.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ type ImageWithSizeLoadingProps = {
2424

2525
/** Invoked on mount and layout changes */
2626
onLayout?: (event: LayoutChangeEvent) => void;
27+
28+
/** Low-resolution URI shown as a placeholder while the full image loads */
29+
previewUri?: string;
2730
} & ImageProps;
2831

2932
function ImageWithLoading({
@@ -37,12 +40,14 @@ function ImageWithLoading({
3740
onLoad,
3841
onLayout,
3942
style,
43+
previewUri,
4044
...rest
4145
}: ImageWithSizeLoadingProps) {
4246
const styles = useThemeStyles();
4347
const isLoadedRef = useRef<boolean | null>(null);
4448
const [isImageCached, setIsImageCached] = useState(true);
4549
const [isLoading, setIsLoading] = useState(false);
50+
const [isThumbnailLoading, setIsThumbnailLoading] = useState(!!previewUri);
4651
const {isOffline} = useNetwork();
4752

4853
const handleError = () => {
@@ -83,6 +88,21 @@ function ImageWithLoading({
8388
style={[styles.w100, styles.h100, containerStyles]}
8489
onLayout={onLayout}
8590
>
91+
{isLoading && !!previewUri && (
92+
// eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invert-colors -- Custom Image wrapper does not support this prop.
93+
<Image
94+
{...rest}
95+
source={{uri: previewUri}}
96+
style={[styles.w100, styles.h100, style]}
97+
resizeMode={resizeMode}
98+
onLoad={(e) => {
99+
setIsThumbnailLoading(false);
100+
onLoad?.(e);
101+
}}
102+
loadingIconSize={loadingIconSize}
103+
loadingIndicatorStyles={loadingIndicatorStyles}
104+
/>
105+
)}
86106
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invert-colors -- Custom Image wrapper does not support this prop. */}
87107
<Image
88108
{...rest}
@@ -104,12 +124,13 @@ function ImageWithLoading({
104124
isLoadedRef.current = false;
105125
setIsImageCached(false);
106126
setIsLoading(true);
127+
setIsThumbnailLoading(!!previewUri);
107128
waitForSession?.();
108129
}}
109130
loadingIconSize={loadingIconSize}
110131
loadingIndicatorStyles={loadingIndicatorStyles}
111132
/>
112-
{isLoading && !isImageCached && !isOffline && (
133+
{isLoading && (!previewUri || isThumbnailLoading) && !isImageCached && !isOffline && (
113134
<LoadingIndicator
114135
iconSize={loadingIconSize}
115136
style={[styles.opacity1, styles.bgTransparent, loadingIndicatorStyles]}

src/components/ImageWithSizeCalculation.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type ImageWithSizeCalculationProps = {
5353

5454
/** Reason attributes for skeleton span telemetry */
5555
reasonAttributes?: SkeletonSpanReasonAttributes;
56+
57+
/** Low-resolution URI shown as a placeholder while the full image loads */
58+
previewUri?: string;
5659
};
5760

5861
/**
@@ -74,6 +77,7 @@ function ImageWithSizeCalculation({
7477
onLoad,
7578
resizeMode,
7679
reasonAttributes,
80+
previewUri,
7781
}: ImageWithSizeCalculationProps) {
7882
const styles = useThemeStyles();
7983

@@ -104,6 +108,7 @@ function ImageWithSizeCalculation({
104108
loadingIconSize={loadingIconSize}
105109
loadingIndicatorStyles={loadingIndicatorStyles}
106110
reasonAttributes={reasonAttributes}
111+
previewUri={previewUri}
107112
/>
108113
);
109114
}

src/components/ReceiptImage/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ type ReceiptImageProps = (
131131

132132
/** Any additional styles to apply */
133133
style?: StyleProp<ViewStyle & ImageStyle>;
134+
135+
/** Low-resolution URI shown as a placeholder while the full image loads */
136+
previewUri?: string;
134137
};
135138

136139
function ReceiptImage({
@@ -161,6 +164,7 @@ function ReceiptImage({
161164
onLoadFailure,
162165
resizeMode,
163166
style,
167+
previewUri,
164168
}: ReceiptImageProps) {
165169
const styles = useThemeStyles();
166170
const [receiptImageWidth, setReceiptImageWidth] = useState<number | undefined>(undefined);
@@ -216,6 +220,7 @@ function ReceiptImage({
216220
return (
217221
<ThumbnailImage
218222
previewSourceURL={source ?? ''}
223+
previewUri={previewUri}
219224
style={[styles.w100, styles.h100, style, thumbnailContainerStyles]}
220225
isAuthTokenRequired={isAuthTokenRequired ?? false}
221226
shouldDynamicallyResize={false}
@@ -255,6 +260,7 @@ function ReceiptImage({
255260
onError={onLoadFailure}
256261
resizeMode={resizeMode}
257262
reasonAttributes={reasonAttributes}
263+
previewUri={previewUri}
258264
/>
259265
);
260266
}

src/components/ReportActionItem/ReportActionItemImage.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize';
1414
import useThemeStyles from '@hooks/useThemeStyles';
1515
import {getReportIDForExpense} from '@libs/MergeTransactionUtils';
1616
import Navigation from '@libs/Navigation/Navigation';
17+
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
1718
import {hasEReceipt, hasReceiptSource, isDistanceRequest, isFetchingWaypointsFromServer, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils';
1819
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
1920
import variables from '@styles/variables';
@@ -142,10 +143,13 @@ function ReportActionItemImage({
142143
const localSource = transaction?.receipt?.localSource;
143144
const effectiveIsLocalFile = isLocalFile || !!localSource;
144145
const effectiveThumbnail = localSource ?? thumbnail;
146+
const receiptURIs = transaction ? getThumbnailAndImageURIs(transaction, null, null) : undefined;
147+
const effectivePreviewUri = localSource ? undefined : receiptURIs?.thumbnail320;
145148
const effectiveImage = localSource != null && typeof image === 'string' ? localSource : image;
146149

147150
const originalImageSource = tryResolveUrlFromApiRoot(effectiveImage ?? '');
148151
const thumbnailSource = tryResolveUrlFromApiRoot(effectiveThumbnail ?? '');
152+
const previewUriSource = effectivePreviewUri ? tryResolveUrlFromApiRoot(effectivePreviewUri) : undefined;
149153
const isEReceipt = transaction && !hasReceiptSource(transaction) && hasEReceipt(transaction);
150154
const isPDF = filename && Str.isPDF(filename);
151155

@@ -207,6 +211,7 @@ function ReportActionItemImage({
207211
onLoad={onLoad}
208212
shouldUseFullHeight={shouldUseFullHeight}
209213
onLoadFailure={onLoadFailure}
214+
previewUri={previewUriSource}
210215
/>
211216
</PressableWithoutFocus>
212217
);
@@ -219,6 +224,7 @@ function ReportActionItemImage({
219224
thumbnailContainerStyles={styles.thumbnailImageContainerHover}
220225
onLoad={onLoad}
221226
onLoadFailure={onLoadFailure}
227+
previewUri={previewUriSource}
222228
/>
223229
);
224230
}

src/components/ThumbnailImage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ type ThumbnailImageProps = {
8383

8484
/** Reason attributes for skeleton span telemetry */
8585
reasonAttributes?: SkeletonSpanReasonAttributes;
86+
87+
/** Low-resolution URI shown as a placeholder while the full image loads */
88+
previewUri?: string;
8689
};
8790

8891
function ThumbnailImage({
@@ -106,6 +109,7 @@ function ThumbnailImage({
106109
onLoad,
107110
resizeMode,
108111
reasonAttributes,
112+
previewUri,
109113
}: ThumbnailImageProps) {
110114
const icons = useMemoizedLazyExpensifyIcons(['Gallery', 'OfflineCloud']);
111115
const styles = useThemeStyles();
@@ -172,6 +176,7 @@ function ThumbnailImage({
172176
onLoad={onLoad}
173177
resizeMode={resizeMode}
174178
reasonAttributes={reasonAttributes}
179+
previewUri={previewUri}
175180
/>
176181
</View>
177182
</View>

0 commit comments

Comments
 (0)