diff --git a/native/chat/inline-multimedia.react.js b/native/chat/inline-multimedia.react.js --- a/native/chat/inline-multimedia.react.js +++ b/native/chat/inline-multimedia.react.js @@ -3,7 +3,7 @@ import Icon from '@expo/vector-icons/Feather.js'; import IonIcon from '@expo/vector-icons/Ionicons.js'; import * as React from 'react'; -import { View, StyleSheet, Text } from 'react-native'; +import { View, Text } from 'react-native'; import * as Progress from 'react-native-progress'; import tinycolor from 'tinycolor2'; @@ -13,6 +13,7 @@ import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js'; import type { PendingMultimediaUpload } from '../input/input-state.js'; import Multimedia from '../media/multimedia.react.js'; +import { useStyles } from '../themes/colors.js'; type Props = { +mediaInfo: MediaInfo, @@ -23,6 +24,7 @@ }; function InlineMultimedia(props: Props): React.Node { const { mediaInfo, pendingUpload, postInProgress } = props; + const styles = useStyles(unboundStyles); let failed = isLocalUploadID(mediaInfo.id) && !postInProgress; let progressPercent = 1; @@ -35,7 +37,12 @@ if (failed) { progressIndicator = ( - + ); } else if (progressPercent !== 1) { @@ -100,7 +107,7 @@ ); } -const styles = StyleSheet.create({ +const unboundStyles = { centerContainer: { alignItems: 'center', bottom: 0, @@ -139,11 +146,10 @@ textShadowRadius: 1, }, uploadError: { - color: 'white', - textShadowColor: '#000', - textShadowOffset: { width: 0, height: 1 }, - textShadowRadius: 1, + backgroundColor: 'vibrantRedButton', + borderRadius: 21, + overflow: 'hidden', }, -}); +}; export default InlineMultimedia; diff --git a/native/media/encrypted-image.react.js b/native/media/encrypted-image.react.js --- a/native/media/encrypted-image.react.js +++ b/native/media/encrypted-image.react.js @@ -73,10 +73,15 @@ const { result } = await decryptMedia(blobURI, encryptionKey, { destination: 'data_uri', }); - // TODO: decide what to do if decryption fails - if (result.success && isMounted) { - mediaCache?.set(blobURI, result.uri); - setSource({ uri: result.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:,' }); + } } }; diff --git a/native/media/loadable-image.react.js b/native/media/loadable-image.react.js --- a/native/media/loadable-image.react.js +++ b/native/media/loadable-image.react.js @@ -1,10 +1,12 @@ // @flow +import Icon from '@expo/vector-icons/Feather.js'; import { Image } from 'expo-image'; import * as React from 'react'; -import { View, StyleSheet, ActivityIndicator } from 'react-native'; +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 = { @@ -17,17 +19,24 @@ }; function LoadableImage(props: Props): React.Node { const { source, placeholder, onLoad: onLoadProp } = 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], + [props.style, styles.invisible], ); if (!loaded && props.invisibleLoad) { @@ -36,41 +45,61 @@ source={source} placeholder={placeholder} onLoad={onLoad} + onError={onError} style={invisibleStyle} /> ); } - let spinner; + let statusIndicator; if (!loaded) { - spinner = ( - + statusIndicator = ( + ); } + if (error) { + statusIndicator = ( + + + + ); + } + return ( - {spinner} + {statusIndicator} ); } -const styles = StyleSheet.create({ +const unboundStyles = { container: { flex: 1, }, + errorIndicator: { + backgroundColor: 'vibrantRedButton', + borderRadius: 21, + overflow: 'hidden', + }, invisible: { opacity: 0, }, - spinnerContainer: { + statusIndicatorContainer: { alignItems: 'center', bottom: 0, justifyContent: 'center', @@ -79,6 +108,6 @@ right: 0, top: 0, }, -}); +}; export default LoadableImage;