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 };