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,7 @@ if (failed) { progressIndicator = ( - + ); } else if (progressPercent !== 1) { @@ -100,7 +102,7 @@ ); } -const styles = StyleSheet.create({ +const unboundStyles = { centerContainer: { alignItems: 'center', bottom: 0, @@ -114,14 +116,14 @@ flex: 1, }, playButton: { - color: 'white', + color: 'whiteText', opacity: 0.9, textShadowColor: '#000', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 1, }, processingStepText: { - color: 'white', + color: 'whiteText', fontSize: 12, textShadowColor: '#000', textShadowRadius: 1, @@ -132,18 +134,18 @@ position: 'absolute', }, progressPercentText: { - color: 'white', + color: 'whiteText', fontSize: 24, fontWeight: 'bold', textShadowColor: '#000', textShadowRadius: 1, }, uploadError: { - color: 'white', - textShadowColor: '#000', - textShadowOffset: { width: 0, height: 1 }, - textShadowRadius: 1, + color: 'whiteText', + 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 @@ -36,6 +36,7 @@ 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') { @@ -73,10 +74,14 @@ 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 { + setErrorOccured(true); + } } }; @@ -102,6 +107,7 @@ style={style} invisibleLoad={invisibleLoad} key={attempt} + errorOccured={errorOccured} /> ); } 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 = { @@ -14,20 +16,28 @@ +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], + [props.style, styles.invisible], ); if (!loaded && props.invisibleLoad) { @@ -36,15 +46,22 @@ source={source} placeholder={placeholder} onLoad={onLoad} + onError={onError} style={invisibleStyle} /> ); } - let spinner; - if (!loaded) { - spinner = ( - + let statusIndicator; + if (error || errorOccured) { + statusIndicator = ( + + + + ); + } else if (!loaded) { + statusIndicator = ( + ); @@ -56,21 +73,28 @@ source={source} placeholder={placeholder} onLoad={onLoad} + onError={onError} style={props.style} /> - {spinner} + {statusIndicator} ); } -const styles = StyleSheet.create({ +const unboundStyles = { container: { flex: 1, }, + errorIndicator: { + color: 'whiteText', + backgroundColor: 'vibrantRedButton', + borderRadius: 21, + overflow: 'hidden', + }, invisible: { opacity: 0, }, - spinnerContainer: { + statusIndicatorContainer: { alignItems: 'center', bottom: 0, justifyContent: 'center', @@ -79,6 +103,6 @@ right: 0, top: 0, }, -}); +}; export default LoadableImage;