From 3b5a3153dd2689609453e1a8bc03c311d06ca178 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri <3846977+santhoshvai@users.noreply.github.com> Date: Thu, 16 Mar 2023 16:24:06 +0100 Subject: [PATCH] feat: add retry ability for image load failure (#2011) * 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 --- package/src/components/Attachment/Gallery.tsx | 27 ++++++-- .../Attachment/ImageReloadIndicator.tsx | 27 ++++++++ .../Attachment/__tests__/Gallery.test.js | 4 +- package/src/hooks/useLoadingImage.tsx | 66 ++++++++++++++++--- 4 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 package/src/components/Attachment/ImageReloadIndicator.tsx diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index e4b25582e..3f9227883 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -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'; @@ -67,6 +68,11 @@ const styles = StyleSheet.create({ justifyContent: 'center', position: 'absolute', }, + imageReloadContainerStyle: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + }, moreImagesContainer: { alignItems: 'center', justifyContent: 'center', @@ -452,8 +458,13 @@ const GalleryImageThumbnail = < GalleryThumbnailProps, 'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius' >) => { - const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } = - useLoadingImage(); + const { + isLoadingImage, + isLoadingImageError, + onReloadImage, + setLoadingImage, + setLoadingImageError, + } = useLoadingImage(); const { theme: { @@ -471,11 +482,17 @@ const GalleryImageThumbnail = < }} > {isLoadingImageError ? ( - + <> + + + ) : ( <> { + onError={({ nativeEvent: { error } }) => { console.warn(error); setLoadingImage(false); setLoadingImageError(true); @@ -494,7 +511,7 @@ const GalleryImageThumbnail = < uri={thumbnail.url} /> {isLoadingImage && ( - + )} diff --git a/package/src/components/Attachment/ImageReloadIndicator.tsx b/package/src/components/Attachment/ImageReloadIndicator.tsx new file mode 100644 index 000000000..fb9624e0b --- /dev/null +++ b/package/src/components/Attachment/ImageReloadIndicator.tsx @@ -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['style']; +}) => { + const { + theme: { + colors: { grey_dark }, + }, + } = useTheme(); + + return ( + + + + ); +}; diff --git a/package/src/components/Attachment/__tests__/Gallery.test.js b/package/src/components/Attachment/__tests__/Gallery.test.js index 2ce3b7382..6ba986a06 100644 --- a/package/src/components/Attachment/__tests__/Gallery.test.js +++ b/package/src/components/Attachment/__tests__/Gallery.test.js @@ -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(); }); diff --git a/package/src/hooks/useLoadingImage.tsx b/package/src/hooks/useLoadingImage.tsx index 38f1e69f1..98b8bdda7 100644 --- a/package/src/hooks/useLoadingImage.tsx +++ b/package/src/hooks/useLoadingImage.tsx @@ -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, + }; };