diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js deleted file mode 100644 index 98349b213aa5..000000000000 --- a/src/components/ImageView/index.native.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from '@components/MultiGestureCanvas/propTypes'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - ...imageViewPropTypes, - ...zoomRangePropTypes, - - /** Function for handle on press */ - onPress: PropTypes.func, - - /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - ...imageViewDefaultProps, - ...zoomRangeDefaultProps, - - onPress: () => {}, - style: {}, -}; - -function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style, zoomRange, onError, isUsedInCarousel, isSingleCarouselItem, carouselItemIndex, carouselActiveItemIndex}) { - const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; - - return ( - - ); -} - -ImageView.propTypes = propTypes; -ImageView.defaultProps = defaultProps; -ImageView.displayName = 'ImageView'; - -export default ImageView; diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx new file mode 100644 index 000000000000..e36bb39d2bed --- /dev/null +++ b/src/components/ImageView/index.native.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Lightbox from '@components/Lightbox'; +import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; +import type {ImageViewProps} from './types'; + +function ImageView({ + isAuthTokenRequired = false, + url, + onScaleChanged, + onPress, + style, + zoomRange = zoomRangeDefaultProps.zoomRange, + onError, + isUsedInCarousel = false, + isSingleCarouselItem = false, + carouselItemIndex = 0, + carouselActiveItemIndex = 0, +}: ImageViewProps) { + const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; + + return ( + + ); +} + +ImageView.displayName = 'ImageView'; + +export default ImageView; diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.tsx similarity index 79% rename from src/components/ImageView/index.js rename to src/components/ImageView/index.tsx index f16b37f328f5..ec37abf6d275 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.tsx @@ -1,15 +1,21 @@ +import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; +import RESIZE_MODES from '@components/Image/resizeModes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import {imageViewDefaultProps, imageViewPropTypes} from './propTypes'; +import viewRef from '@src/types/utils/viewRef'; +import type {ImageLoadNativeEventData, ImageViewProps} from './types'; -function ImageView({isAuthTokenRequired, url, fileName, onError}) { +type ZoomDelta = {offsetX: number; offsetY: number}; + +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -25,18 +31,12 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { const [imgWidth, setImgWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); - const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); + const [zoomDelta, setZoomDelta] = useState(); - const scrollableRef = useRef(null); + const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - /** - * @param {Number} newContainerWidth - * @param {Number} newContainerHeight - * @param {Number} newImageWidth - * @param {Number} newImageHeight - */ - const setScale = (newContainerWidth, newContainerHeight, newImageWidth, newImageHeight) => { + const setScale = (newContainerWidth: number, newContainerHeight: number, newImageWidth: number, newImageHeight: number) => { if (!newContainerWidth || !newImageWidth || !newContainerHeight || !newImageHeight) { return; } @@ -44,10 +44,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setZoomScale(newZoomScale); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerLayoutChanged = (e) => { + const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; setScale(width, height, imgWidth, imgHeight); @@ -57,10 +54,8 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { /** * When open image, set image width, height. - * @param {Number} imageWidth - * @param {Number} imageHeight */ - const setImageRegion = (imageWidth, imageHeight) => { + const setImageRegion = (imageWidth: number, imageHeight: number) => { if (imageHeight <= 0) { return; } @@ -78,32 +73,29 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { setIsZoomed(false); }; - const imageLoad = ({nativeEvent}) => { + const imageLoad = ({nativeEvent}: NativeSyntheticEvent) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPressIn = (e) => { + const onContainerPressIn = (e: GestureResponderEvent) => { const {pageX, pageY} = e.nativeEvent; setIsMouseDown(true); setInitialX(pageX); setInitialY(pageY); - setInitialScrollLeft(scrollableRef.current.scrollLeft); - setInitialScrollTop(scrollableRef.current.scrollTop); + setInitialScrollLeft(scrollableRef.current?.scrollLeft ?? 0); + setInitialScrollTop(scrollableRef.current?.scrollTop ?? 0); }; /** * Convert touch point to zoomed point - * @param {Boolean} x x point when click zoom - * @param {Boolean} y y point when click zoom - * @returns {Object} converted touch point + * @param x point when click zoom + * @param y point when click zoom + * @returns converted touch point */ - const getScrollOffset = (x, y) => { - let offsetX; - let offsetY; + const getScrollOffset = (x: number, y: number) => { + let offsetX = 0; + let offsetY = 0; // Container size bigger than clicked position offset if (x <= containerWidth / 2) { @@ -121,12 +113,9 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { return {offsetX, offsetY}; }; - /** - * @param {SyntheticEvent} e - */ - const onContainerPress = (e) => { + const onContainerPress = (e?: GestureResponderEvent | KeyboardEvent | SyntheticEvent) => { if (!isZoomed && !isDragging) { - if (e.nativeEvent) { + if (e && 'nativeEvent' in e && e.nativeEvent instanceof PointerEvent) { const {offsetX, offsetY} = e.nativeEvent; // Dividing clicked positions by the zoom scale to get coordinates @@ -148,13 +137,10 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } }; - /** - * @param {SyntheticEvent} e - */ const trackPointerPosition = useCallback( - (e) => { + (event: MouseEvent) => { // Whether the pointer is released inside the ImageView - const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target); + const isInsideImageView = scrollableRef.current?.contains(event.target as Node); if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { setIsDragging(false); @@ -165,14 +151,14 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); const trackMovement = useCallback( - (e) => { + (event: MouseEvent) => { if (!isZoomed) { return; } - if (isDragging && isMouseDown) { - const x = e.nativeEvent.x; - const y = e.nativeEvent.y; + if (isDragging && isMouseDown && scrollableRef.current) { + const x = event.x; + const y = event.y; const moveX = initialX - x; const moveY = initialY - y; scrollableRef.current.scrollLeft = initialScrollLeft + moveX; @@ -218,7 +204,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { style={isLoading || zoomScale === 0 ? undefined : [styles.w100, styles.h100]} // When Image dimensions are lower than the container boundary(zoomscale <= 1), use `contain` to render the image with natural dimensions. // Both `center` and `contain` keeps the image centered on both x and y axis. - resizeMode={zoomScale > 1 ? Image.resizeMode.center : Image.resizeMode.contain} + resizeMode={zoomScale > 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -229,7 +215,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { } return ( @@ -249,7 +235,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { source={{uri: url}} isAuthTokenRequired={isAuthTokenRequired} style={[styles.h100, styles.w100]} - resizeMode={Image.resizeMode.contain} + resizeMode={RESIZE_MODES.contain} onLoadStart={imageLoadingStart} onLoad={imageLoad} onError={onError} @@ -261,8 +247,6 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { ); } -ImageView.propTypes = imageViewPropTypes; -ImageView.defaultProps = imageViewDefaultProps; ImageView.displayName = 'ImageView'; export default ImageView; diff --git a/src/components/ImageView/propTypes.js b/src/components/ImageView/propTypes.js deleted file mode 100644 index 3809d9aed043..000000000000 --- a/src/components/ImageView/propTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; - -const imageViewPropTypes = { - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** Handles scale changed event in image zoom component. Used on native only */ - // eslint-disable-next-line react/no-unused-prop-types - onScaleChanged: PropTypes.func.isRequired, - - /** URL to full-sized image */ - url: PropTypes.string.isRequired, - - /** image file name */ - fileName: PropTypes.string.isRequired, - - /** Handles errors while displaying the image */ - onError: PropTypes.func, - - /** Whether this view is the active screen */ - isFocused: PropTypes.bool, - - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ - isUsedInCarousel: PropTypes.bool, - - /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ - isSingleCarouselItem: PropTypes.bool, - - /** The index of the carousel item */ - carouselItemIndex: PropTypes.number, - - /** The index of the currently active carousel item */ - carouselActiveItemIndex: PropTypes.number, -}; - -const imageViewDefaultProps = { - isAuthTokenRequired: false, - onError: () => {}, - isFocused: true, - isUsedInCarousel: false, - isSingleCarouselItem: false, - carouselItemIndex: 0, - carouselActiveItemIndex: 0, -}; - -export {imageViewPropTypes, imageViewDefaultProps}; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts new file mode 100644 index 000000000000..bf83bc44d47b --- /dev/null +++ b/src/components/ImageView/types.ts @@ -0,0 +1,47 @@ +import type {StyleProp, ViewStyle} from 'react-native'; +import type ZoomRange from '@components/MultiGestureCanvas/types'; + +type ImageViewProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired?: boolean; + + /** Handles scale changed event in image zoom component. Used on native only */ + onScaleChanged: (scale: number) => void; + + /** URL to full-sized image */ + url: string; + + /** image file name */ + fileName: string; + + /** Handles errors while displaying the image */ + onError?: () => void; + + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ + isUsedInCarousel?: boolean; + + /** When "isUsedInCarousel" is set to true, determines whether there is only one item in the carousel */ + isSingleCarouselItem?: boolean; + + /** The index of the carousel item */ + carouselItemIndex?: number; + + /** The index of the currently active carousel item */ + carouselActiveItemIndex?: number; + + /** Function for handle on press */ + onPress?: () => void; + + /** Additional styles to add to the component */ + style?: StyleProp; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: ZoomRange; +}; + +type ImageLoadNativeEventData = { + width: number; + height: number; +}; + +export type {ImageViewProps, ImageLoadNativeEventData}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 45326edb4610..8b7d68befafd 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; +import stylePropTypes from '@styles/stylePropTypes'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; import MultiGestureCanvas from './MultiGestureCanvas'; @@ -44,7 +45,7 @@ const propTypes = { activeIndex: PropTypes.number, /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: stylePropTypes, }; const defaultProps = { diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 590154b48bca..d967d04ab94b 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -758,7 +758,7 @@ function MoneyRequestConfirmationList(props) { {shouldShowTags && ( { diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts new file mode 100644 index 000000000000..0242f045feef --- /dev/null +++ b/src/components/MultiGestureCanvas/types.ts @@ -0,0 +1,6 @@ +type ZoomRange = { + min: number; + max: number; +}; + +export default ZoomRange; diff --git a/src/styles/stylePropTypes.js b/src/styles/stylePropTypes.js index f9ecdb98ff13..b82db94140ee 100644 --- a/src/styles/stylePropTypes.js +++ b/src/styles/stylePropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); +const stylePropTypes = PropTypes.oneOfType([PropTypes.object, PropTypes.bool, PropTypes.arrayOf(PropTypes.object), PropTypes.func]); export default stylePropTypes;