Skip to content

Commit

Permalink
feat: add retry ability for image load failure (#2011)
Browse files Browse the repository at this point in the history
* feat: add reload image state to useLoadingImage hook

* chore: remove image key

* feat: reload gallery image

* chore: change the rendering order

* fix: failing gallery test
  • Loading branch information
santhoshvai committed Mar 16, 2023
1 parent 2b47ee3 commit 3b5a315
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 15 deletions.
27 changes: 22 additions & 5 deletions package/src/components/Attachment/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import type { Attachment } from 'stream-chat';

import { GalleryImage } from './GalleryImage';
import { ImageReloadIndicator } from './ImageReloadIndicator';
import { buildGallery } from './utils/buildGallery/buildGallery';

import type { Thumbnail } from './utils/buildGallery/types';
Expand Down Expand Up @@ -67,6 +68,11 @@ const styles = StyleSheet.create({
justifyContent: 'center',
position: 'absolute',
},
imageReloadContainerStyle: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
moreImagesContainer: {
alignItems: 'center',
justifyContent: 'center',
Expand Down Expand Up @@ -452,8 +458,13 @@ const GalleryImageThumbnail = <
GalleryThumbnailProps<StreamChatGenerics>,
'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius'
>) => {
const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } =
useLoadingImage();
const {
isLoadingImage,
isLoadingImageError,
onReloadImage,
setLoadingImage,
setLoadingImageError,
} = useLoadingImage();

const {
theme: {
Expand All @@ -471,11 +482,17 @@ const GalleryImageThumbnail = <
}}
>
{isLoadingImageError ? (
<ImageLoadingFailedIndicator style={[styles.imageLoadingErrorIndicatorStyle]} />
<>
<ImageLoadingFailedIndicator style={styles.imageLoadingErrorIndicatorStyle} />
<ImageReloadIndicator
onReloadImage={onReloadImage}
style={styles.imageReloadContainerStyle}
/>
</>
) : (
<>
<GalleryImage
onError={(error) => {
onError={({ nativeEvent: { error } }) => {
console.warn(error);
setLoadingImage(false);
setLoadingImageError(true);
Expand All @@ -494,7 +511,7 @@ const GalleryImageThumbnail = <
uri={thumbnail.url}
/>
{isLoadingImage && (
<View style={[styles.imageLoadingIndicatorContainer]}>
<View style={styles.imageLoadingIndicatorContainer}>
<ImageLoadingIndicator style={styles.imageLoadingIndicatorStyle} />
</View>
)}
Expand Down
27 changes: 27 additions & 0 deletions package/src/components/Attachment/ImageReloadIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Pressable } from 'react-native';

import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { Refresh } from '../../icons';

const REFRESH_ICON_SIZE = 24;

export const ImageReloadIndicator = ({
onReloadImage,
style,
}: {
onReloadImage: () => void;
style: React.ComponentProps<typeof Pressable>['style'];
}) => {
const {
theme: {
colors: { grey_dark },
},
} = useTheme();

return (
<Pressable onPress={onReloadImage} style={style}>
<Refresh height={REFRESH_ICON_SIZE} pathFill={grey_dark} width={REFRESH_ICON_SIZE} />
</Pressable>
);
};
4 changes: 3 additions & 1 deletion package/src/components/Attachment/__tests__/Gallery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ describe('Gallery', () => {
expect(queryAllByTestId('gallery-container').length).toBe(1);
});

fireEvent(getByA11yLabel('Gallery Image'), 'error');
fireEvent(getByA11yLabel('Gallery Image'), 'error', {
nativeEvent: { error: 'error loading image' },
});
expect(getByAccessibilityHint('image-loading-error')).toBeTruthy();
});

Expand Down
66 changes: 57 additions & 9 deletions package/src/hooks/useLoadingImage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,64 @@
import { useEffect, useState } from 'react';
import { useEffect, useReducer, useRef } from 'react';

import { useNetInfo } from '@react-native-community/netinfo';
import { useChatContext } from '../contexts/chatContext/ChatContext';

type ImageState = {
isLoadingImage: boolean;
isLoadingImageError: boolean;
};

type Action =
| { type: 'reloadImage' }
| { isLoadingImage: boolean; type: 'setLoadingImage' }
| { isLoadingImageError: boolean; type: 'setLoadingImageError' };

function reducer(prevState: ImageState, action: Action) {
switch (action.type) {
case 'reloadImage':
return {
...prevState,
isLoadingImage: true,
isLoadingImageError: false,
};
case 'setLoadingImage':
return { ...prevState, isLoadingImage: action.isLoadingImage };
case 'setLoadingImageError':
return { ...prevState, isLoadingImageError: action.isLoadingImageError };
default:
return prevState;
}
}
export const useLoadingImage = () => {
const [isLoadingImage, setLoadingImage] = useState(true);
const [isLoadingImageError, setLoadingImageError] = useState(false);
const { isConnected } = useNetInfo();
const [imageState, dispatch] = useReducer(reducer, {
isLoadingImage: true,
isLoadingImageError: false,
});
const { isLoadingImage, isLoadingImageError } = imageState;
const onReloadImageRef = useRef(() => dispatch({ type: 'reloadImage' }));
const setLoadingImageRef = useRef((isLoadingImage: boolean) =>
dispatch({ isLoadingImage, type: 'setLoadingImage' }),
);
const setLoadingImageErrorRef = useRef((isLoadingImageError: boolean) =>
dispatch({ isLoadingImageError, type: 'setLoadingImageError' }),
);
const { isOnline } = useChatContext();

// storing the value of isLoadingImageError in a ref to avoid passing as a dep to useEffect
const hasImageLoadedErroredRef = useRef(isLoadingImageError);
hasImageLoadedErroredRef.current = isLoadingImageError;

useEffect(() => {
if (isConnected) {
setLoadingImageError(false);
if (isOnline && hasImageLoadedErroredRef.current) {
// if there was an error previously, reload the image automatically when user comes back online
onReloadImageRef.current();
}
}, [isConnected]);
}, [isOnline]);

return { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError };
return {
isLoadingImage,
isLoadingImageError,
onReloadImage: onReloadImageRef.current,
setLoadingImage: setLoadingImageRef.current,
setLoadingImageError: setLoadingImageErrorRef.current,
};
};

0 comments on commit 3b5a315

Please sign in to comment.