diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js index 4ae55b49ea0a..d19b3f6ce05d 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.js @@ -1,35 +1,145 @@ -import React, {memo} from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {View, Image} from 'react-native'; -import styles from '../../styles/styles'; +import {View, Image, Pressable} from 'react-native'; +import styles, {getZoomCursorStyle, getZoomSizingStyle} from '../../styles/styles'; +import canUseTouchScreen from '../../libs/canUseTouchscreen'; const propTypes = { // URL to full-sized image url: PropTypes.string.isRequired, }; -const ImageView = props => ( - - - -); + ]; + this.state = { + isZoomed: false, + isDragging: false, + isMouseDown: false, + initialScrollLeft: 0, + initialScrollTop: 0, + initialX: 0, + initialY: 0, + }; + } -ImageView.propTypes = propTypes; -ImageView.displayName = 'ImageView'; + componentDidMount() { + if (this.canUseTouchScreen) { + return; + } + + document.addEventListener('mousemove', this.trackMovement.bind(this)); + } + + componentWillUnmount() { + if (this.canUseTouchScreen) { + return; + } + + document.removeEventListener('mousemove', this.trackMovement.bind(this)); + } + + trackMovement(e) { + if (!this.state.isZoomed) { + return; + } + + if (this.state.isDragging && this.state.isMouseDown) { + const x = e.nativeEvent.x; + const y = e.nativeEvent.y; + const moveX = this.state.initialX - x; + const moveY = this.state.initialY - y; + this.scrollableRef.scrollLeft = this.state.initialScrollLeft + moveX; + this.scrollableRef.scrollTop = this.state.initialScrollTop + moveY; + } -export default memo(ImageView); + this.setState(prevState => ({isDragging: prevState.isMouseDown})); + } + + render() { + if (this.canUseTouchScreen) { + return ( + + + + ); + } + + return ( + this.scrollableRef = el} + style={[ + ...this.containerStyles, + styles.overflowScroll, + styles.noScrollbars, + ]} + > + { + const {pageX, pageY} = e.nativeEvent; + this.setState({ + isMouseDown: true, + initialX: pageX, + initialY: pageY, + initialScrollLeft: this.scrollableRef.scrollLeft, + initialScrollTop: this.scrollableRef.scrollTop, + }); + }} + onPress={(e) => { + if (this.state.isZoomed && !this.state.isDragging) { + const {offsetX, offsetY} = e.nativeEvent; + this.scrollableRef.scrollTop = offsetY * 1.5; + this.scrollableRef.scrollLeft = offsetX * 1.5; + } + + if (this.state.isZoomed && this.state.isDragging && this.state.isMouseDown) { + this.setState({isDragging: false, isMouseDown: false}); + } + }} + onPressOut={() => { + if (this.state.isDragging) { + return; + } + + this.setState(prevState => ({ + isZoomed: !prevState.isZoomed, + isMouseDown: false, + })); + }} + > + + + + ); + } +} + +ImageView.propTypes = propTypes; +export default ImageView; diff --git a/src/libs/canUseTouchscreen.js b/src/libs/canUseTouchscreen.js new file mode 100644 index 000000000000..a3310a5d6be1 --- /dev/null +++ b/src/libs/canUseTouchscreen.js @@ -0,0 +1,32 @@ +/** + * Allows us to identify whether the platform has a touchscreen. + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent + * + * @returns {Boolean} + */ +function canUseTouchScreen() { + let hasTouchScreen = false; + if ('maxTouchPoints' in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } else if ('msMaxTouchPoints' in navigator) { + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } else { + const mQ = window.matchMedia && matchMedia('(pointer:coarse)'); + if (mQ && mQ.media === '(pointer:coarse)') { + hasTouchScreen = !!mQ.matches; + } else if ('orientation' in window) { + hasTouchScreen = true; // deprecated, but good fallback + } else { + // Only as a last resort, fall back to user agent sniffing + const UA = navigator.userAgent; + hasTouchScreen = ( + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) + || /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA) + ); + } + } + return hasTouchScreen; +} + +export default canUseTouchScreen; diff --git a/src/styles/styles.js b/src/styles/styles.js index c21bffe0ff83..bd35955c8538 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1185,6 +1185,10 @@ const styles = { fontWeight: fontWeightBold, fontSize: variables.iouAmountTextSize, }, 0), + + noScrollbars: { + scrollbarWidth: 'none', + }, }; const baseCodeTagStyles = { @@ -1322,6 +1326,32 @@ function getNavigationModalCardStyle(isSmallScreenWidth) { }; } +/** + * @param {Boolean} isZoomed + * @param {Boolean} isDragging + * @return {Object} + */ +function getZoomCursorStyle(isZoomed, isDragging) { + if (!isZoomed) { + return {cursor: 'zoom-in'}; + } + + return { + cursor: isDragging ? 'grabbing' : 'zoom-out', + }; +} + +/** + * @param {Boolean} isZoomed + * @return {Object} + */ +function getZoomSizingStyle(isZoomed) { + return { + height: isZoomed ? '250%' : '100%', + width: isZoomed ? '250%' : '100%', + }; +} + export default styles; export { getSafeAreaPadding, @@ -1330,4 +1360,6 @@ export { getNavigationDrawerStyle, getNavigationDrawerType, getNavigationModalCardStyle, + getZoomCursorStyle, + getZoomSizingStyle, };