Feature/add retry button for failed image requests (#550)
* Improve img loading placeholder Prevent showing a loading placeholder in case the image is already cached. * Add failed img load retry button
This commit is contained in:
@@ -6,10 +6,13 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, CSSProperties } from 'react';
|
||||
import React, { useState, CSSProperties, useEffect } from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Theme, SxProps } from '@mui/material';
|
||||
import { Theme, SxProps, Stack, Button } from '@mui/material';
|
||||
|
||||
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
interface IProps {
|
||||
src: string;
|
||||
@@ -25,41 +28,66 @@ interface IProps {
|
||||
|
||||
export function SpinnerImage(props: IProps) {
|
||||
const { src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle } = props;
|
||||
const [imageSrc, setImagsrc] = useState<string>('');
|
||||
|
||||
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean | undefined>(undefined);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const updateImageState = (loading: boolean, error: boolean = false) => {
|
||||
setIsLoading(loading);
|
||||
setHasError(error);
|
||||
|
||||
if (!loading && !error) {
|
||||
onImageLoad?.();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
// only activate the loading state in case the image has not been cached yet.
|
||||
// otherwise, the loading placeholder will always be visible before the actual image is shown, which looks like flickering
|
||||
const timeout = setTimeout(() => setIsLoading((prevState) => (prevState === undefined ? true : prevState)), 1);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
img.onload = () => {
|
||||
setImagsrc(src);
|
||||
onImageLoad?.();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
// Setting to an actual image so CSS styling works consistently
|
||||
setImagsrc('/notFound.svg');
|
||||
};
|
||||
|
||||
return () => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
if (imageSrc.length === 0) {
|
||||
return (
|
||||
<Box sx={spinnerStyle}>
|
||||
<CircularProgress thickness={5} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (imageSrc === 'Not Found') {
|
||||
return <Box sx={spinnerStyle} />;
|
||||
}
|
||||
|
||||
return <img style={imgStyle} ref={imgRef} src={imageSrc} alt={alt} draggable={false} />;
|
||||
return (
|
||||
<>
|
||||
{(isLoading || hasError) && (
|
||||
<Box sx={spinnerStyle}>
|
||||
<Stack height="100%" alignItems="center" justifyContent="center">
|
||||
{isLoading && <CircularProgress thickness={5} />}
|
||||
{hasError && (
|
||||
<>
|
||||
<BrokenImageIcon />
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
<img
|
||||
key={`${src}_${imgLoadRetryKey}`}
|
||||
style={{ ...imgStyle, display: isLoading || hasError ? 'none' : undefined }}
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={() => updateImageState(false)}
|
||||
onError={() => updateImageState(false, true)}
|
||||
draggable={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SpinnerImage.defaultProps = {
|
||||
|
||||
Reference in New Issue
Block a user