Skip to content

Commit

Permalink
Merge pull request Expensify#39930 from kidroca/kidroca/fix/carousel-…
Browse files Browse the repository at this point in the history
…opens-on-wrong-attachment

Enhance Carousel Scrolling and Interaction in AttachmentCarousel
  • Loading branch information
deetergp authored May 28, 2024
2 parents 9830f15 + 3089399 commit b7233ce
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 66 deletions.

This file was deleted.

104 changes: 67 additions & 37 deletions src/components/Attachments/AttachmentCarousel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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 {withOnyx} from 'react-native-onyx';
import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
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 CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
Expand All @@ -29,16 +31,23 @@ 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<FlatList>(null);
const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();

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),
[modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth],
);
const [page, setPage] = useState(0);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [activeSource, setActiveSource] = useState<AttachmentSource | null>(source);
Expand Down Expand Up @@ -75,6 +84,17 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
}
}, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]);

// Scroll position is affected when window width is resized, so we readjust it on width changes
useEffect(() => {
if (attachments.length === 0 || scrollRef.current == null) {
return;
}

scrollRef.current.scrollToIndex({index: page, animated: false});
// The hook is not supposed to run on page change, so we keep the page out of the dependencies
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cellWidth]);

/** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
({viewableItems}: UpdatePageProps) => {
Expand Down Expand Up @@ -121,7 +141,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(
Expand All @@ -133,35 +153,55 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
/** Calculate items layout information to optimize scrolling performance */
const getItemLayout = useCallback(
(data: ArrayLike<Attachment> | 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<Attachment>) => (
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState: boolean) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
/>
<View style={[styles.h100, {width: cellWidth}]}>
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
/>
</View>
),
[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 (
<View
style={[styles.flex1, styles.attachmentCarouselContainer]}
onLayout={({nativeEvent}) => {
if (isFullScreenRef.current) {
return;
}
setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width));
}}
onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
>
Expand All @@ -185,36 +225,26 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
cancelAutoHideArrow={cancelAutoHideArrows}
/>

{containerWidth > 0 && (
<FlatList
<GestureDetector gesture={pan}>
<Animated.FlatList
keyboardShouldPersistTaps="handled"
horizontal
decelerationRate="fast"
showsHorizontalScrollIndicator={false}
bounces={false}
// Scroll only one image at a time no matter how fast the user swipes
disableIntervalMomentum
pagingEnabled
snapToAlignment="start"
snapToInterval={containerWidth}
// Enable scrolling by swiping on mobile (touch) devices only
// disable scroll for desktop/browsers because they add their scrollbars
// Enable scrolling FlatList only when PDF is not in a zoomed state
scrollEnabled={canUseTouchScreen}
// scrolling is controlled by the pan gesture
scrollEnabled={false}
ref={scrollRef}
initialScrollIndex={page}
initialNumToRender={3}
windowSize={5}
maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
data={attachments}
CellRendererComponent={AttachmentCarouselCellRenderer}
renderItem={renderItem}
getItemLayout={getItemLayout}
keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
)}
</GestureDetector>

<CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
</>
Expand Down

0 comments on commit b7233ce

Please sign in to comment.