diff --git a/native/media/encrypted-image.react.js b/native/media/encrypted-image.react.js
index 3185b6fc9..891fa6516 100644
--- a/native/media/encrypted-image.react.js
+++ b/native/media/encrypted-image.react.js
@@ -1,114 +1,115 @@
// @flow
import * as React from 'react';
import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js';
import { decryptBase64, decryptMedia } from './encryption-utils.js';
import LoadableImage from './loadable-image.react.js';
import { useSelector } from '../redux/redux-utils.js';
import type { ImageStyle } from '../types/styles.js';
type BaseProps = {
+blobURI: string,
+encryptionKey: string,
+onLoad?: (uri: string) => void,
+spinnerColor: string,
+style: ImageStyle,
+invisibleLoad: boolean,
+thumbHash?: ?string,
};
type Props = {
...BaseProps,
};
function EncryptedImage(props: Props): React.Node {
const {
blobURI,
encryptionKey,
onLoad: onLoadProp,
thumbHash: encryptedThumbHash,
} = props;
const mediaCache = React.useContext(MediaCacheContext);
const [source, setSource] = React.useState(null);
const connectionStatus = useSelector(state => state.connection.status);
const prevConnectionStatusRef = React.useRef(connectionStatus);
const [attempt, setAttempt] = React.useState(0);
+ const [errorOccured, setErrorOccured] = React.useState(false);
if (prevConnectionStatusRef.current !== connectionStatus) {
if (!source && connectionStatus === 'connected') {
setAttempt(attempt + 1);
}
prevConnectionStatusRef.current = connectionStatus;
}
const placeholder = React.useMemo(() => {
if (!encryptedThumbHash) {
return null;
}
try {
const decryptedThumbHash = decryptBase64(
encryptedThumbHash,
encryptionKey,
);
return { thumbhash: decryptedThumbHash };
} catch (e) {
return null;
}
}, [encryptedThumbHash, encryptionKey]);
React.useEffect(() => {
let isMounted = true;
setSource(null);
const loadDecrypted = async () => {
const cached = await mediaCache?.get(blobURI);
if (cached && isMounted) {
setSource({ uri: cached });
return;
}
const { result } = await decryptMedia(blobURI, encryptionKey, {
destination: 'data_uri',
});
if (isMounted) {
if (result.success) {
mediaCache?.set(blobURI, result.uri);
setSource({ uri: result.uri });
} else {
- // Setting an invalid uri will cause the Image to run onError
- setSource({ uri: 'data:,' });
+ setErrorOccured(true);
}
}
};
loadDecrypted();
return () => {
isMounted = false;
};
}, [attempt, blobURI, encryptionKey, mediaCache]);
const onLoad = React.useCallback(() => {
onLoadProp && onLoadProp(blobURI);
}, [blobURI, onLoadProp]);
const { style, spinnerColor, invisibleLoad } = props;
return (
);
}
export default EncryptedImage;
diff --git a/native/media/loadable-image.react.js b/native/media/loadable-image.react.js
index ff1ab8cd9..130075b06 100644
--- a/native/media/loadable-image.react.js
+++ b/native/media/loadable-image.react.js
@@ -1,109 +1,116 @@
// @flow
import Icon from '@expo/vector-icons/Feather.js';
import { Image } from 'expo-image';
import * as React from 'react';
import { View, ActivityIndicator } from 'react-native';
import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
import { useStyles } from '../themes/colors.js';
import type { ImageStyle } from '../types/styles.js';
type Props = {
+placeholder: ?ImageSource,
+source: ?ImageSource,
+onLoad?: () => void,
+spinnerColor: string,
+style: ImageStyle,
+invisibleLoad: boolean,
+ +errorOccured?: boolean,
};
function LoadableImage(props: Props): React.Node {
- const { source, placeholder, onLoad: onLoadProp } = props;
+ const { source, placeholder, onLoad: onLoadProp, errorOccured } = props;
const styles = useStyles(unboundStyles);
const [loaded, setLoaded] = React.useState(false);
const [error, setError] = React.useState(false);
const onError = React.useCallback(() => {
setError(true);
}, []);
const onLoad = React.useCallback(() => {
setError(false);
setLoaded(true);
onLoadProp && onLoadProp();
}, [onLoadProp]);
const invisibleStyle = React.useMemo(
() => [props.style, styles.invisible],
[props.style, styles.invisible],
);
if (!loaded && props.invisibleLoad) {
return (
);
}
let statusIndicator;
- if (!loaded) {
+ if (error || errorOccured) {
+ statusIndicator = (
+
+
+
+ );
+ } else if (!loaded) {
statusIndicator = (
);
}
if (error) {
statusIndicator = (
);
}
return (
{statusIndicator}
);
}
const unboundStyles = {
container: {
flex: 1,
},
errorIndicator: {
color: 'whiteText',
backgroundColor: 'vibrantRedButton',
borderRadius: 21,
overflow: 'hidden',
},
invisible: {
opacity: 0,
},
statusIndicatorContainer: {
alignItems: 'center',
bottom: 0,
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
};
export default LoadableImage;