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;