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 @@ -4,7 +4,7 @@ import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; -import { decryptMedia } from './encryption-utils.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'; @@ -16,13 +16,19 @@ +spinnerColor: string, +style: ImageStyle, +invisibleLoad: boolean, + +thumbHash?: ?string, }; type Props = { ...BaseProps, }; function EncryptedImage(props: Props): React.Node { - const { holder, encryptionKey, onLoad: onLoadProp } = props; + const { + holder, + encryptionKey, + onLoad: onLoadProp, + thumbHash: encryptedThumbHash, + } = props; const mediaCache = React.useContext(MediaCacheContext); const [source, setSource] = React.useState(null); @@ -38,6 +44,21 @@ 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); @@ -74,6 +95,7 @@ return ( <LoadableImage + placeholder={placeholder} source={source} onLoad={onLoad} spinnerColor={spinnerColor} 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 @@ -8,6 +8,7 @@ import type { ImageStyle } from '../types/styles.js'; type Props = { + +placeholder: ?ImageSource, +source: ?ImageSource, +onLoad: () => void, +spinnerColor: string, @@ -15,7 +16,7 @@ +invisibleLoad: boolean, }; function LoadableImage(props: Props): React.Node { - const { source, onLoad: onLoadProp } = props; + const { source, placeholder, onLoad: onLoadProp } = props; const [loaded, setLoaded] = React.useState(false); @@ -30,7 +31,14 @@ ); if (!loaded && props.invisibleLoad) { - return <Image source={source} onLoad={onLoad} style={invisibleStyle} />; + return ( + <Image + source={source} + placeholder={placeholder} + onLoad={onLoad} + style={invisibleStyle} + /> + ); } let spinner; @@ -44,8 +52,13 @@ return ( <View style={styles.container}> + <Image + source={source} + placeholder={placeholder} + onLoad={onLoad} + style={props.style} + /> {spinner} - <Image source={source} onLoad={onLoad} style={props.style} /> </View> ); } diff --git a/native/media/multimedia.react.js b/native/media/multimedia.react.js --- a/native/media/multimedia.react.js +++ b/native/media/multimedia.react.js @@ -16,11 +16,13 @@ | { +kind: 'uri', +uri: string, + +thumbHash?: ?string, } | { +kind: 'encrypted', +holder: string, +encryptionKey: string, + +thumbHash?: ?string, }; type BaseProps = { +mediaInfo: MediaInfo | AvatarMediaInfo, @@ -111,6 +113,7 @@ if (source.kind === 'encrypted') { return ( <EncryptedImage + thumbHash={source.thumbHash} holder={source.holder} encryptionKey={source.encryptionKey} onLoad={this.onLoad} @@ -121,12 +124,14 @@ /> ); } - const { uri } = source; + const { uri, thumbHash } = source; + const placeholder = thumbHash ? { thumbhash: thumbHash } : null; if (uri.startsWith('http')) { return ( <RemoteImage uri={uri} onLoad={this.onLoad} + placeholder={placeholder} spinnerColor={this.props.spinnerColor} style={styles.image} invisibleLoad={invisibleLoad} @@ -138,6 +143,7 @@ <Image source={{ uri }} onLoad={this.onLoad} + placeholder={placeholder} style={styles.image} key={uri} /> diff --git a/native/media/remote-image.react.js b/native/media/remote-image.react.js --- a/native/media/remote-image.react.js +++ b/native/media/remote-image.react.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; +import type { ImageSource } from 'react-native/Libraries/Image/ImageSource'; import { type ConnectionStatus } from 'lib/types/socket-types.js'; @@ -14,6 +15,7 @@ +spinnerColor: string, +style: ImageStyle, +invisibleLoad: boolean, + +placeholder?: ?ImageSource, }; type Props = { ...BaseProps, @@ -41,12 +43,13 @@ } render() { - const { style, spinnerColor, invisibleLoad, uri } = this.props; + const { style, spinnerColor, invisibleLoad, uri, placeholder } = this.props; const source = { uri }; return ( <LoadableImage source={source} + placeholder={placeholder} onLoad={this.onLoad} spinnerColor={spinnerColor} style={style}