diff --git a/native/media/encrypted-image.react.js b/native/media/encrypted-image.react.js index d72d7e2b9..dbbca66f6 100644 --- a/native/media/encrypted-image.react.js +++ b/native/media/encrypted-image.react.js @@ -1,120 +1,121 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import { connectionSelector } from 'lib/selectors/keyserver-selectors.js'; import { ashoatKeyserverID } from 'lib/utils/validation-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 { ImageSource } from '../types/react-native.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 [source, setSource] = React.useState(null); const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const connectionStatus = 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 { 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 130075b06..0389d9680 100644 --- a/native/media/loadable-image.react.js +++ b/native/media/loadable-image.react.js @@ -1,116 +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 { ImageSource } from '../types/react-native.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, 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 (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; diff --git a/native/media/remote-image.react.js b/native/media/remote-image.react.js index d59cbffe1..b222d446a 100644 --- a/native/media/remote-image.react.js +++ b/native/media/remote-image.react.js @@ -1,104 +1,104 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import type { ImageSource } from 'react-native/Libraries/Image/ImageSource'; import { connectionSelector } from 'lib/selectors/keyserver-selectors.js'; import { type ConnectionStatus } from 'lib/types/socket-types.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import LoadableImage from './loadable-image.react.js'; import { useSelector } from '../redux/redux-utils.js'; +import type { ImageSource } from '../types/react-native.js'; import type { ImageStyle } from '../types/styles.js'; type BaseProps = { +uri: string, +onLoad?: (uri: string) => void, +spinnerColor: string, +style: ImageStyle, +invisibleLoad: boolean, +placeholder?: ?ImageSource, }; type Props = { ...BaseProps, +connectionStatus: ConnectionStatus, }; type State = { +attempt: number, }; class RemoteImage extends React.PureComponent { loaded: boolean = false; state: State = { attempt: 0, }; componentDidUpdate(prevProps: Props) { if ( !this.loaded && this.props.connectionStatus === 'connected' && prevProps.connectionStatus !== 'connected' ) { this.setState(otherPrevState => ({ attempt: otherPrevState.attempt + 1, })); } } render(): React.Node { const { style, spinnerColor, invisibleLoad, uri, placeholder } = this.props; const source = { uri }; return ( ); } onLoad = () => { this.loaded = true; this.props.onLoad && this.props.onLoad(this.props.uri); }; } function ConnectedRemoteImage(props: BaseProps): React.Node { const connection = useSelector(connectionSelector(ashoatKeyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const connectionStatus = connection.status; const { uri, onLoad, spinnerColor, style, invisibleLoad, placeholder } = props; const connectedRemoteImage = React.useMemo( () => ( ), [ connectionStatus, invisibleLoad, onLoad, placeholder, spinnerColor, style, uri, ], ); return connectedRemoteImage; } export default ConnectedRemoteImage; diff --git a/native/types/react-native.js b/native/types/react-native.js index 2dfd03d60..8d4acc22f 100644 --- a/native/types/react-native.js +++ b/native/types/react-native.js @@ -1,48 +1,51 @@ // @flow import AnimatedInterpolation from 'react-native/Libraries/Animated/nodes/AnimatedInterpolation.js'; import type ReactNativeAnimatedValue from 'react-native/Libraries/Animated/nodes/AnimatedValue.js'; +import type { ImageSource } from 'react-native/Libraries/Image/ImageSource.js'; import type { ViewToken } from 'react-native/Libraries/Lists/ViewabilityHelper.js'; import type { ____ViewStyle_Internal } from 'react-native/Libraries/StyleSheet/StyleSheetTypes.js'; export type { Layout, LayoutEvent, ScrollEvent, } from 'react-native/Libraries/Types/CoreEventTypes.js'; export type { ContentSizeChangeEvent, KeyPressEvent, FocusEvent, BlurEvent, SelectionChangeEvent, } from 'react-native/Libraries/Components/TextInput/TextInput.js'; export type { NativeMethods } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes.js'; export type { KeyboardEvent } from 'react-native/Libraries/Components/Keyboard/Keyboard.js'; export type { EventSubscription } from 'react-native/Libraries/vendor/emitter/EventEmitter.js'; export type AnimatedValue = ReactNativeAnimatedValue; export type ViewableItemsChange = { +viewableItems: ViewToken[], +changed: ViewToken[], ... }; export type EmitterSubscription = { +remove: () => void, ... }; export type ImagePasteEvent = { +fileName: string, +filePath: string, +height: number, +width: number, +threadID: string, }; export type { AnimatedInterpolation }; export type ViewStyleObj = ____ViewStyle_Internal; + +export type { ImageSource };