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:
schroda
2024-01-15 00:32:39 +01:00
committed by GitHub
parent 9962e0713d
commit 199b62341f
6 changed files with 70 additions and 48 deletions

View File

@@ -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 = {