diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js
index bcfcaf78d5c6..b365f507a4fc 100644
--- a/src/components/ImageView/index.native.js
+++ b/src/components/ImageView/index.native.js
@@ -1,13 +1,13 @@
-import React, {PureComponent} from 'react';
+import React, {useState, useRef, useEffect} from 'react';
import PropTypes from 'prop-types';
import {View, PanResponder} from 'react-native';
import ImageZoom from 'react-native-image-pan-zoom';
import _ from 'underscore';
import styles from '../../styles/styles';
import variables from '../../styles/variables';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import FullscreenLoadingIndicator from '../FullscreenLoadingIndicator';
import Image from '../Image';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
/**
* On the native layer, we use a image library to handle zoom functionality
@@ -25,59 +25,34 @@ const propTypes = {
/** Function for handle on press */
onPress: PropTypes.func,
- ...windowDimensionsPropTypes,
+ /** Additional styles to add to the component */
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
};
const defaultProps = {
isAuthTokenRequired: false,
onPress: () => {},
+ style: {},
};
-class ImageView extends PureComponent {
- constructor(props) {
- super(props);
-
- this.state = {
- isLoading: true,
- imageWidth: 0,
- imageHeight: 0,
- interactionPromise: undefined,
- };
-
- // Use the default double click interval from the ImageZoom library
- // https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
- this.doubleClickInterval = 175;
- this.imageZoomScale = 1;
- this.lastClickTime = 0;
- this.amountOfTouches = 0;
-
- // PanResponder used to capture how many touches are active on the attachment image
- this.panResponder = PanResponder.create({
- onStartShouldSetPanResponder: this.updatePanResponderTouches.bind(this),
- });
-
- this.configureImageZoom = this.configureImageZoom.bind(this);
- this.imageLoadingStart = this.imageLoadingStart.bind(this);
- }
+// Use the default double click interval from the ImageZoom library
+// https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
+const DOUBLE_CLICK_INTERVAL = 175;
- componentDidUpdate(prevProps) {
- if (this.props.url === prevProps.url) {
- return;
- }
+function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style}) {
+ const {windowWidth, windowHeight} = useWindowDimensions();
- this.imageLoadingStart();
+ const [isLoading, setIsLoading] = useState(true);
+ const [imageDimensions, setImageDimensions] = useState({
+ width: 0,
+ height: 0,
+ });
+ const [containerHeight, setContainerHeight] = useState(null);
- if (this.interactionPromise) {
- this.state.interactionPromise.cancel();
- }
- }
-
- componentWillUnmount() {
- if (!this.state.interactionPromise) {
- return;
- }
- this.state.interactionPromise.cancel();
- }
+ const imageZoomScale = useRef(1);
+ const lastClickTime = useRef(0);
+ const numberOfTouches = useRef(0);
+ const zoom = useRef(null);
/**
* Updates the amount of active touches on the PanResponder on our ImageZoom overlay View
@@ -86,14 +61,58 @@ class ImageView extends PureComponent {
* @param {GestureState} gestureState
* @returns {Boolean}
*/
- updatePanResponderTouches(e, gestureState) {
+ const updatePanResponderTouches = (e, gestureState) => {
if (_.isNumber(gestureState.numberActiveTouches)) {
- this.amountOfTouches = gestureState.numberActiveTouches;
+ numberOfTouches.current = gestureState.numberActiveTouches;
}
// We don't need to set the panResponder since all we care about is checking the gestureState, so return false
return false;
- }
+ };
+
+ // PanResponder used to capture how many touches are active on the attachment image
+ const panResponder = useRef(
+ PanResponder.create({
+ onStartShouldSetPanResponder: updatePanResponderTouches,
+ }),
+ ).current;
+
+ /**
+ * When the url changes and the image must load again,
+ * this resets the zoom to ensure the next image loads with the correct dimensions.
+ */
+ const resetImageZoom = () => {
+ if (imageZoomScale.current !== 1) {
+ imageZoomScale.current = 1;
+ }
+
+ if (zoom.current) {
+ zoom.current.centerOn({
+ x: 0,
+ y: 0,
+ scale: 1,
+ duration: 0,
+ });
+ }
+ };
+
+ const imageLoadingStart = () => {
+ if (isLoading) {
+ return;
+ }
+
+ resetImageZoom();
+ setImageDimensions({
+ width: 0,
+ height: 0,
+ });
+ setIsLoading(true);
+ };
+
+ useEffect(() => {
+ imageLoadingStart();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the url changes
+ }, [url]);
/**
* The `ImageZoom` component requires image dimensions which
@@ -102,148 +121,126 @@ class ImageView extends PureComponent {
*
* @param {Object} nativeEvent
*/
- configureImageZoom({nativeEvent}) {
- let imageWidth = nativeEvent.width;
- let imageHeight = nativeEvent.height;
- const containerWidth = Math.round(this.props.windowWidth);
- const containerHeight = Math.round(this.state.containerHeight ? this.state.containerHeight : this.props.windowHeight);
+ const configureImageZoom = ({nativeEvent}) => {
+ let imageZoomWidth = nativeEvent.width;
+ let imageZoomHeight = nativeEvent.height;
+ const roundedContainerWidth = Math.round(windowWidth);
+ const roundedContainerHeight = Math.round(containerHeight || windowHeight);
- const aspectRatio = Math.min(containerHeight / imageHeight, containerWidth / imageWidth);
+ const aspectRatio = Math.min(roundedContainerHeight / imageZoomHeight, roundedContainerWidth / imageZoomWidth);
- imageHeight *= aspectRatio;
- imageWidth *= aspectRatio;
+ imageZoomHeight *= aspectRatio;
+ imageZoomWidth *= aspectRatio;
// Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well.
const maxDimensionsScale = 11;
- imageWidth = Math.min(imageWidth, containerWidth * maxDimensionsScale);
- imageHeight = Math.min(imageHeight, containerHeight * maxDimensionsScale);
- this.setState({imageHeight, imageWidth, isLoading: false});
- }
+ imageZoomWidth = Math.min(imageZoomWidth, roundedContainerWidth * maxDimensionsScale);
+ imageZoomHeight = Math.min(imageZoomHeight, roundedContainerHeight * maxDimensionsScale);
- /**
- * When the url changes and the image must load again,
- * this resets the zoom to ensure the next image loads with the correct dimensions.
- */
- resetImageZoom() {
- if (this.imageZoomScale !== 1) {
- this.imageZoomScale = 1;
+ setImageDimensions({
+ height: imageZoomHeight,
+ width: imageZoomWidth,
+ });
+ setIsLoading(false);
+ };
+
+ const configurePanResponder = () => {
+ const currentTimestamp = new Date().getTime();
+ const isDoubleClick = currentTimestamp - lastClickTime.current <= DOUBLE_CLICK_INTERVAL;
+ lastClickTime.current = currentTimestamp;
+
+ // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
+ if (numberOfTouches.current === 2 || imageZoomScale.current !== 1) {
+ return true;
}
- if (this.zoom) {
- this.zoom.centerOn({
+ // When we have a double click and the zoom scale is 1 then programmatically zoom the image
+ // but let the tap fall through to the parent so we can register a swipe down to dismiss
+ if (isDoubleClick) {
+ zoom.current.centerOn({
x: 0,
y: 0,
- scale: 1,
- duration: 0,
+ scale: 2,
+ duration: 100,
});
- }
- }
- imageLoadingStart() {
- if (this.state.isLoading) {
- return;
+ // onMove will be called after the zoom animation.
+ // So it's possible to zoom and swipe and stuck in between the images.
+ // Sending scale just when we actually trigger the animation makes this nearly impossible.
+ // you should be really fast to catch in between state updates.
+ // And this lucky case will be fixed by migration to UI thread only code
+ // with gesture handler and reanimated.
+ onScaleChanged(2);
}
- this.resetImageZoom();
- this.setState({imageHeight: 0, imageWidth: 0, isLoading: true});
- }
-
- render() {
- // Default windowHeight accounts for the modal header height
- const windowHeight = this.props.windowHeight - variables.contentHeaderHeight;
- const hasImageDimensions = this.state.imageWidth !== 0 && this.state.imageHeight !== 0;
- const shouldShowLoadingIndicator = this.state.isLoading || !hasImageDimensions;
-
- // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
- return (
- {
- const layout = event.nativeEvent.layout;
- this.setState({
- containerHeight: layout.height,
- });
- }}
- >
- {Boolean(this.state.containerHeight) && (
- (this.zoom = el)}
- onClick={() => this.props.onPress()}
- cropWidth={this.props.windowWidth}
- cropHeight={windowHeight}
- imageWidth={this.state.imageWidth}
- imageHeight={this.state.imageHeight}
- onStartShouldSetPanResponder={() => {
- const isDoubleClick = new Date().getTime() - this.lastClickTime <= this.doubleClickInterval;
- this.lastClickTime = new Date().getTime();
-
- // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
- if (this.amountOfTouches === 2 || this.imageZoomScale !== 1) {
- return true;
- }
-
- // When we have a double click and the zoom scale is 1 then programmatically zoom the image
- // but let the tap fall through to the parent so we can register a swipe down to dismiss
- if (isDoubleClick) {
- this.zoom.centerOn({
- x: 0,
- y: 0,
- scale: 2,
- duration: 100,
- });
-
- // onMove will be called after the zoom animation.
- // So it's possible to zoom and swipe and stuck in between the images.
- // Sending scale just when we actually trigger the animation makes this nearly impossible.
- // you should be really fast to catch in between state updates.
- // And this lucky case will be fixed by migration to UI thread only code
- // with gesture handler and reanimated.
- this.props.onScaleChanged(2);
- }
-
- // We must be either swiping down or double tapping since we are at zoom scale 1
- return false;
- }}
- onMove={({scale}) => {
- this.props.onScaleChanged(scale);
- this.imageZoomScale = scale;
- }}
- >
-
- {/**
- Create an invisible view on top of the image so we can capture and set the amount of touches before
- the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
- ImageZoom to work
- */}
-
-
- )}
- {shouldShowLoadingIndicator && }
-
- );
- }
+
+ // We must be either swiping down or double tapping since we are at zoom scale 1
+ return false;
+ };
+
+ // Default windowHeight accounts for the modal header height
+ const calculatedWindowHeight = windowHeight - variables.contentHeaderHeight;
+ const hasImageDimensions = imageDimensions.width !== 0 && imageDimensions.height !== 0;
+ const shouldShowLoadingIndicator = isLoading || !hasImageDimensions;
+
+ // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
+ return (
+ {
+ const layout = event.nativeEvent.layout;
+ setContainerHeight(layout.height);
+ }}
+ >
+ {Boolean(containerHeight) && (
+ {
+ onScaleChanged(scale);
+ imageZoomScale.current = scale;
+ }}
+ >
+
+ {/**
+ Create an invisible view on top of the image so we can capture and set the amount of touches before
+ the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
+ ImageZoom to work
+ */}
+
+
+ )}
+ {shouldShowLoadingIndicator && }
+
+ );
}
ImageView.propTypes = propTypes;
ImageView.defaultProps = defaultProps;
+ImageView.displayName = 'ImageView';
-export default withWindowDimensions(ImageView);
+export default ImageView;