Skip to content

Commit a19c11b

Browse files
authored
Merge pull request Expensify#62696 from Krishna2323/krishna2323/issue/62036
fix: Reports - No spinner on receipt thumbnail.
2 parents 345d3b0 + b2761ec commit a19c11b

3 files changed

Lines changed: 137 additions & 73 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import delay from 'lodash/delay';
2+
import React, {useEffect, useRef, useState} from 'react';
3+
import type {StyleProp, ViewStyle} from 'react-native';
4+
import {View} from 'react-native';
5+
import useNetwork from '@hooks/useNetwork';
6+
import useThemeStyles from '@hooks/useThemeStyles';
7+
import AttachmentOfflineIndicator from './AttachmentOfflineIndicator';
8+
import FullscreenLoadingIndicator from './FullscreenLoadingIndicator';
9+
import Image from './Image';
10+
import type {ImageObjectPosition, ImageOnLoadEvent, ImageProps} from './Image/types';
11+
12+
type ImageWithSizeLoadingProps = {
13+
/** Any additional styles to apply */
14+
containerStyles?: StyleProp<ViewStyle>;
15+
16+
/** Whether the image requires an authToken */
17+
isAuthTokenRequired: boolean;
18+
19+
/** The object position of image */
20+
objectPosition?: ImageObjectPosition;
21+
22+
/** Whether to show offline indicator */
23+
shouldShowOfflineIndicator?: boolean;
24+
} & ImageProps;
25+
26+
function ImageWithSizeCalculation({
27+
onError,
28+
containerStyles,
29+
shouldShowOfflineIndicator = true,
30+
loadingIconSize,
31+
waitForSession,
32+
loadingIndicatorStyles,
33+
resizeMode,
34+
onLoad,
35+
...rest
36+
}: ImageWithSizeLoadingProps) {
37+
const styles = useThemeStyles();
38+
const isLoadedRef = useRef<boolean | null>(null);
39+
const [isImageCached, setIsImageCached] = useState(true);
40+
const [isLoading, setIsLoading] = useState(false);
41+
const {isOffline} = useNetwork();
42+
43+
const handleError = () => {
44+
onError?.();
45+
if (isLoadedRef.current) {
46+
isLoadedRef.current = false;
47+
setIsImageCached(false);
48+
}
49+
if (isOffline) {
50+
return;
51+
}
52+
setIsLoading(false);
53+
};
54+
55+
const imageLoadedSuccessfully = (e: ImageOnLoadEvent) => {
56+
isLoadedRef.current = true;
57+
setIsLoading(false);
58+
setIsImageCached(true);
59+
onLoad?.(e);
60+
};
61+
62+
/** Delay the loader to detect whether the image is being loaded from the cache or the internet. */
63+
useEffect(() => {
64+
if (isLoadedRef.current ?? !isLoading) {
65+
return;
66+
}
67+
const timeout = delay(() => {
68+
if (!isLoading || isLoadedRef.current) {
69+
return;
70+
}
71+
setIsImageCached(false);
72+
}, 200);
73+
return () => clearTimeout(timeout);
74+
}, [isLoading]);
75+
76+
return (
77+
<View style={[styles.w100, styles.h100, containerStyles]}>
78+
<Image
79+
// eslint-disable-next-line react/jsx-props-no-spreading
80+
{...rest}
81+
style={[styles.w100, styles.h100]}
82+
onLoadStart={() => {
83+
if (isLoadedRef.current ?? isLoading) {
84+
return;
85+
}
86+
setIsLoading(true);
87+
}}
88+
onError={handleError}
89+
onLoad={(e) => {
90+
imageLoadedSuccessfully(e);
91+
}}
92+
waitForSession={() => {
93+
// Called when the image should wait for a valid session to reload
94+
// At the moment this function is called, the image is not in cache anymore
95+
isLoadedRef.current = false;
96+
setIsImageCached(false);
97+
setIsLoading(true);
98+
waitForSession?.();
99+
}}
100+
loadingIconSize={loadingIconSize}
101+
loadingIndicatorStyles={loadingIndicatorStyles}
102+
/>
103+
{isLoading && !isImageCached && !isOffline && (
104+
<FullscreenLoadingIndicator
105+
iconSize={loadingIconSize}
106+
style={[styles.opacity1, styles.bgTransparent, loadingIndicatorStyles]}
107+
/>
108+
)}
109+
{isLoading && shouldShowOfflineIndicator && !isImageCached && <AttachmentOfflineIndicator isPreview />}
110+
</View>
111+
);
112+
}
113+
114+
ImageWithSizeCalculation.displayName = 'ImageWithSizeCalculation';
115+
export default React.memo(ImageWithSizeCalculation);

src/components/ImageWithSizeCalculation.tsx

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import delay from 'lodash/delay';
2-
import React, {useEffect, useMemo, useRef, useState} from 'react';
1+
import React, {useMemo} from 'react';
32
import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
4-
import {View} from 'react-native';
5-
import useNetwork from '@hooks/useNetwork';
63
import useThemeStyles from '@hooks/useThemeStyles';
74
import Log from '@libs/Log';
85
import CONST from '@src/CONST';
9-
import AttachmentOfflineIndicator from './AttachmentOfflineIndicator';
10-
import FullscreenLoadingIndicator from './FullscreenLoadingIndicator';
11-
import Image from './Image';
126
import RESIZE_MODES from './Image/resizeModes';
137
import type {ImageObjectPosition} from './Image/types';
8+
import ImageWithLoading from './ImageWithLoading';
149

1510
type OnMeasure = (args: {width: number; height: number}) => void;
1611

@@ -51,78 +46,31 @@ type ImageWithSizeCalculationProps = {
5146
*/
5247
function ImageWithSizeCalculation({url, altText, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL}: ImageWithSizeCalculationProps) {
5348
const styles = useThemeStyles();
54-
const isLoadedRef = useRef<boolean | null>(null);
55-
const [isImageCached, setIsImageCached] = useState(true);
56-
const [isLoading, setIsLoading] = useState(false);
57-
const {isOffline} = useNetwork();
5849

5950
const source = useMemo(() => (typeof url === 'string' ? {uri: url} : url), [url]);
6051

6152
const onError = () => {
6253
Log.hmmm('Unable to fetch image to calculate size', {url});
6354
onLoadFailure?.();
64-
if (isLoadedRef.current) {
65-
isLoadedRef.current = false;
66-
setIsImageCached(false);
67-
}
68-
if (isOffline) {
69-
return;
70-
}
71-
setIsLoading(false);
7255
};
7356

74-
const imageLoadedSuccessfully = (event: OnLoadNativeEvent) => {
75-
isLoadedRef.current = true;
76-
setIsLoading(false);
77-
setIsImageCached(true);
78-
onMeasure({
79-
width: event.nativeEvent.width,
80-
height: event.nativeEvent.height,
81-
});
82-
};
83-
84-
/** Delay the loader to detect whether the image is being loaded from the cache or the internet. */
85-
useEffect(() => {
86-
if (isLoadedRef.current ?? !isLoading) {
87-
return;
88-
}
89-
const timeout = delay(() => {
90-
if (!isLoading || isLoadedRef.current) {
91-
return;
92-
}
93-
setIsImageCached(false);
94-
}, 200);
95-
return () => clearTimeout(timeout);
96-
}, [isLoading]);
97-
9857
return (
99-
<View style={[styles.w100, styles.h100, style]}>
100-
<Image
101-
style={[styles.w100, styles.h100]}
102-
source={source}
103-
aria-label={altText}
104-
isAuthTokenRequired={isAuthTokenRequired}
105-
resizeMode={RESIZE_MODES.cover}
106-
onLoadStart={() => {
107-
if (isLoadedRef.current ?? isLoading) {
108-
return;
109-
}
110-
setIsLoading(true);
111-
}}
112-
onError={onError}
113-
onLoad={imageLoadedSuccessfully}
114-
waitForSession={() => {
115-
// Called when the image should wait for a valid session to reload
116-
// At the moment this function is called, the image is not in cache anymore
117-
isLoadedRef.current = false;
118-
setIsImageCached(false);
119-
setIsLoading(true);
120-
}}
121-
objectPosition={objectPosition}
122-
/>
123-
{isLoading && !isImageCached && !isOffline && <FullscreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}
124-
{isLoading && !isImageCached && <AttachmentOfflineIndicator isPreview />}
125-
</View>
58+
<ImageWithLoading
59+
containerStyles={[styles.w100, styles.h100, style]}
60+
style={[styles.w100, styles.h100]}
61+
source={source}
62+
aria-label={altText}
63+
isAuthTokenRequired={isAuthTokenRequired}
64+
resizeMode={RESIZE_MODES.cover}
65+
onError={onError}
66+
onLoad={(event: OnLoadNativeEvent) => {
67+
onMeasure({
68+
width: event.nativeEvent.width,
69+
height: event.nativeEvent.height,
70+
});
71+
}}
72+
objectPosition={objectPosition}
73+
/>
12674
);
12775
}
12876

src/components/ReceiptImage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import EReceiptThumbnail from './EReceiptThumbnail';
99
import type {IconSize} from './EReceiptThumbnail';
1010
import EReceiptWithSizeCalculation from './EReceiptWithSizeCalculation';
1111
import type {FullScreenLoadingIndicatorIconSize} from './FullscreenLoadingIndicator';
12-
import Image from './Image';
12+
import ImageWithLoading from './ImageWithLoading';
1313
import PDFThumbnail from './PDFThumbnail';
1414
import ReceiptEmptyState from './ReceiptEmptyState';
1515
import type {TransactionListItemType} from './SelectionList/types';
@@ -197,12 +197,13 @@ function ReceiptImage({
197197
}
198198

199199
return (
200-
<Image
200+
<ImageWithLoading
201201
source={{uri: source}}
202202
style={[style ?? [styles.w100, styles.h100], styles.overflowHidden]}
203-
isAuthTokenRequired={isAuthTokenRequired}
203+
isAuthTokenRequired={!!isAuthTokenRequired}
204204
loadingIconSize={loadingIconSize}
205205
loadingIndicatorStyles={loadingIndicatorStyles}
206+
shouldShowOfflineIndicator={false}
206207
/>
207208
);
208209
}

0 commit comments

Comments
 (0)