diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 3a7e0f19c4cd..ff1c5fabf01f 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,7 +1,9 @@ import isEqual from 'lodash/isEqual'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; -import {FlatList, Keyboard, PixelRatio, View} from 'react-native'; +import {Keyboard, PixelRatio, View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; import {withOnyx} from 'react-native-onyx'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import BlockingView from '@components/BlockingViews/BlockingView'; @@ -15,7 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import CarouselActions from './CarouselActions'; import CarouselButtons from './CarouselButtons'; import CarouselItem from './CarouselItem'; @@ -29,16 +31,21 @@ const viewabilityConfig = { itemVisiblePercentThreshold: 95, }; +const MIN_FLING_VELOCITY = 500; + function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); + const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {isFullScreenRef} = useFullScreenContext(); - const scrollRef = useRef(null); + const scrollRef = useAnimatedRef>>(); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - const [containerWidth, setContainerWidth] = useState(0); + const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); + const cellWidth = useMemo(() => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2), [windowWidth]); + const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); @@ -120,7 +127,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen}); }, - [attachments, canUseTouchScreen, isFullScreenRef, page], + [attachments, canUseTouchScreen, isFullScreenRef, page, scrollRef], ); const extractItemKey = useCallback( @@ -132,35 +139,56 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /** Calculate items layout information to optimize scrolling performance */ const getItemLayout = useCallback( (data: ArrayLike | null | undefined, index: number) => ({ - length: containerWidth, - offset: containerWidth * index, + length: cellWidth, + offset: cellWidth * index, index, }), - [containerWidth], + [cellWidth], ); /** Defines how a single attachment should be rendered */ const renderItem = useCallback( ({item}: ListRenderItemInfo) => ( - setShouldShowArrows((oldState: boolean) => !oldState) : undefined} - isModalHovered={shouldShowArrows} - /> + + setShouldShowArrows((oldState) => !oldState) : undefined} + isModalHovered={shouldShowArrows} + /> + ), - [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows], + [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + ); + + /** Pan gesture handing swiping through attachments on touch screen devices */ + const pan = useMemo( + () => + Gesture.Pan() + .enabled(canUseTouchScreen) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onEnd(({translationX, velocityX}) => { + let newIndex; + if (velocityX > MIN_FLING_VELOCITY) { + // User flung to the right + newIndex = Math.max(0, page - 1); + } else if (velocityX < -MIN_FLING_VELOCITY) { + // User flung to the left + newIndex = Math.min(attachments.length - 1, page + 1); + } else { + // snap scroll position to the nearest cell (making sure it's within the bounds of the list) + const delta = Math.round(-translationX / cellWidth) + newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta)); + } + + scrollTo(scrollRef, newIndex * cellWidth, 0, true); + }), + [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], ); return ( { - if (isFullScreenRef.current) { - return; - } - setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width)); - }} onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)} onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)} > @@ -184,36 +212,26 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, cancelAutoHideArrow={cancelAutoHideArrows} /> - {containerWidth > 0 && ( - + - )} +