Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS migration] Migrate 'ImageView' component to TypeScript #34202

Merged
merged 16 commits into from
Jan 25, 2024
52 changes: 0 additions & 52 deletions src/components/ImageView/index.native.js

This file was deleted.

39 changes: 39 additions & 0 deletions src/components/ImageView/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Lightbox
source={url}
zoomRange={zoomRange}
isAuthTokenRequired={isAuthTokenRequired}
onScaleChanged={onScaleChanged}
onPress={onPress}
onError={onError}
index={carouselItemIndex}
activeIndex={carouselActiveItemIndex}
hasSiblingCarouselItems={hasSiblingCarouselItems}
style={style}
/>
);
}

ImageView.displayName = 'ImageView';

export default ImageView;
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
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);
Expand All @@ -25,29 +30,20 @@ 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<ZoomDelta>();

const scrollableRef = useRef(null);
const scrollableRef = useRef<HTMLDivElement>(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;
}
const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight);
setZoomScale(newZoomScale);
};

/**
* @param {SyntheticEvent} e
*/
const onContainerLayoutChanged = (e) => {
const onContainerLayoutChanged = (e: LayoutChangeEvent) => {
const {width, height} = e.nativeEvent.layout;
setScale(width, height, imgWidth, imgHeight);

Expand All @@ -57,10 +53,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) => {
tienifr marked this conversation as resolved.
Show resolved Hide resolved
if (imageHeight <= 0) {
return;
}
Expand All @@ -78,32 +72,29 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
setIsZoomed(false);
};

const imageLoad = ({nativeEvent}) => {
const imageLoad = ({nativeEvent}: NativeSyntheticEvent<ImageLoadNativeEventData>) => {
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) => {
tienifr marked this conversation as resolved.
Show resolved Hide resolved
let offsetX = 0;
let offsetY = 0;

// Container size bigger than clicked position offset
if (x <= containerWidth / 2) {
Expand All @@ -121,10 +112,8 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
return {offsetX, offsetY};
};

/**
* @param {SyntheticEvent} e
*/
const onContainerPress = (e) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onContainerPress = (e: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native event here is PointerEvent. Should we just convert the type for this case? I think we should bring this to slack

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!isZoomed && !isDragging) {
if (e.nativeEvent) {
const {offsetX, offsetY} = e.nativeEvent;
Expand All @@ -148,13 +137,11 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
}
};

/**
* @param {SyntheticEvent} e
*/
const trackPointerPosition = useCallback(
(e) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(e: any) => {
// Whether the pointer is released inside the ImageView
const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target);
const isInsideImageView = scrollableRef.current?.contains(e.nativeEvent.target);

if (!isInsideImageView && isZoomed && isDragging && isMouseDown) {
setIsDragging(false);
Expand All @@ -165,14 +152,14 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
);

const trackMovement = useCallback(
(e) => {
(e: MouseEvent) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here:

    const trackMovement = useCallback(
        (e: MouseEvent) => {
            const mouseEvent = e as unknown as ReactMouseEvent;
            if (!isZoomed) {
                return;
            }

            if (isDragging && isMouseDown && scrollableRef.current) {
                const x = mouseEvent.nativeEvent.x;
                const y = mouseEvent.nativeEvent.y;
                const moveX = initialX - x;
                const moveY = initialY - y;
                scrollableRef.current.scrollLeft = initialScrollLeft + moveX;
                scrollableRef.current.scrollTop = initialScrollTop + moveY;
            }

            setIsDragging(isMouseDown);
        },
        [initialScrollLeft, initialScrollTop, initialX, initialY, isDragging, isMouseDown, isZoomed],
    );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabioh8010 Thanks for your help. I just wonder can we remove nativeEvent here? As what I know, e.nativeEvent.x is the same as e.x in web/desktop platform. Fortunately, we're spliting ImageView into 2 files index.tsx and index.native.tsx.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree your suggestion is better than any type, but it's quite workaround. If we know the reason behind using nativeEvent, we'll be all good with your idea ^

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if it's well tested and it's working I believe you can change to remove nativeEvent, same for the other case const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target);.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

    const trackMovement = useCallback(
        (event: MouseEvent) => {
            if (!isZoomed) {
                return;
            }

            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;
                scrollableRef.current.scrollTop = initialScrollTop + moveY;
            }

            setIsDragging(isMouseDown);
        },
        [initialScrollLeft, initialScrollTop, initialX, initialY, isDragging, isMouseDown, isZoomed],
    );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the other function:

    const trackPointerPosition = useCallback(
        (event: MouseEvent) => {
            // Whether the pointer is released inside the ImageView
            const isInsideImageView = scrollableRef.current?.contains(event.target as Node);

            if (!isInsideImageView && isZoomed && isDragging && isMouseDown) {
                setIsDragging(false);
                setIsMouseDown(false);
            }
        },
        [isDragging, isMouseDown, isZoomed],
    );

if (!isZoomed) {
return;
}

if (isDragging && isMouseDown) {
const x = e.nativeEvent.x;
const y = e.nativeEvent.y;
if (isDragging && isMouseDown && scrollableRef.current) {
const x = e.x;
const y = e.y;
const moveX = initialX - x;
const moveY = initialY - y;
scrollableRef.current.scrollLeft = initialScrollLeft + moveX;
Expand Down Expand Up @@ -218,7 +205,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}
Expand All @@ -229,7 +216,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
}
return (
<View
ref={scrollableRef}
ref={viewRef(scrollableRef)}
onLayout={onContainerLayoutChanged}
style={[styles.imageViewContainer, styles.overflowAuto, styles.pRelative]}
>
Expand All @@ -249,7 +236,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}
Expand All @@ -261,8 +248,6 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) {
);
}

ImageView.propTypes = imageViewPropTypes;
ImageView.defaultProps = imageViewDefaultProps;
ImageView.displayName = 'ImageView';

export default ImageView;
46 changes: 0 additions & 46 deletions src/components/ImageView/propTypes.js

This file was deleted.

47 changes: 47 additions & 0 deletions src/components/ImageView/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to MultiGestureCanvas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultiGestureCanvas can be out of scope


/** 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<ViewStyle>;

/** Range of zoom that can be applied to the content by pinching or double tapping. */
zoomRange?: ZoomRange;
};

type ImageLoadNativeEventData = {
width: number;
height: number;
};
tienifr marked this conversation as resolved.
Show resolved Hide resolved

export type {ImageViewProps, ImageLoadNativeEventData};
Loading
Loading