Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,15 @@
"../../src/components/MultiGestureCanvas/index.tsx" "no-restricted-syntax" 1
"../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/preserve-manual-memoization" 1
"../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/refs" 2
"../../src/components/MultiGestureCanvas/useCanvasPanGesture.ts" "no-restricted-syntax" 1
"../../src/components/MultiGestureCanvas/useCanvasPinchGesture.ts" "react-hooks/set-state-in-effect" 1
"../../src/components/MultiGestureCanvas/useCanvasTapGestures.ts" "no-restricted-syntax" 1
"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pan" 2
"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/LegacyPanGesture" 1
"../../src/components/MultiGestureCanvas/usePanGesture.ts" "no-restricted-syntax" 1
"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pinch" 2
"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/LegacyPinchGesture" 1
"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "react-hooks/set-state-in-effect" 1
"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 4
"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/LegacyTapGesture" 2
"../../src/components/MultiGestureCanvas/useTapGestures.ts" "no-restricted-syntax" 1
"../../src/components/Navigation/QuickCreationActionsBar/index.tsx" "no-restricted-imports" 1
"../../src/components/NumberWithSymbolForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/NumberWithSymbolForm.tsx" "react-hooks/set-state-in-effect" 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';

import type {ListRenderItemInfo} from 'react-native';
import type {LegacyComposedGesture, GestureType} from 'react-native-gesture-handler';
import type {PanGesture} from 'react-native-gesture-handler';

import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Keyboard, PixelRatio, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {GestureDetector, usePanGesture} from 'react-native-gesture-handler';
import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';

import type AttachmentCarouselViewProps from './types';
Expand All @@ -41,7 +41,7 @@ const MIN_FLING_VELOCITY = 500;

type DeviceAwareGestureDetectorProps = {
canUseTouchScreen: boolean;
gesture: LegacyComposedGesture | GestureType;
gesture: PanGesture;
children: React.ReactNode;
};

Expand Down Expand Up @@ -77,7 +77,6 @@ function AttachmentCarouselView({

const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource | null>(attachmentID ?? source);

const pagerRef = useRef<GestureType | undefined>(undefined);
const scrollRef = useAnimatedRef<Animated.FlatList<Attachment>>();

const {shouldUseNarrowLayout} = useResponsiveLayout();
Expand Down Expand Up @@ -157,15 +156,53 @@ function AttachmentCarouselView({
[cellWidth],
);

/** Pan gesture handling swiping through attachments on touch screen devices */
const pan = usePanGesture({
enabled: canUseTouchScreen,
onUpdate: ({translationX}) => {
if (!isScrollEnabled.get()) {
return;
}

if (translationX !== 0) {
isPagerScrolling.set(true);
}

scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
},
onDeactivate: ({translationX, velocityX}) => {
if (!isScrollEnabled.get()) {
return;
}

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));
}

isPagerScrolling.set(false);
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
},
});

const stateValue = useMemo<AttachmentCarouselPagerStateContextType>(
() => ({
pagerItems: [{source, index: 0, isActive: true}],
activePage: 0,
pagerRef,
pagerRef: scrollRef,
pagerGesture: pan,
isPagerScrolling,
isScrollEnabled,
}),
[source, isPagerScrolling, isScrollEnabled],
[source, scrollRef, pan, isPagerScrolling, isScrollEnabled],
);

const actionsValue = useMemo<AttachmentCarouselPagerActionsContextType>(
Expand Down Expand Up @@ -193,46 +230,6 @@ function AttachmentCarouselView({
),
[activeAttachmentID, canUseTouchScreen, cellWidth, handleTap, report?.reportID, shouldShowArrows, styles.h100],
);
/** Pan gesture handing swiping through attachments on touch screen devices */
const pan = useMemo(
() =>
Gesture.Pan()
.enabled(canUseTouchScreen)
.onUpdate(({translationX}) => {
if (!isScrollEnabled.get()) {
return;
}

if (translationX !== 0) {
isPagerScrolling.set(true);
}

scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
})
.onEnd(({translationX, velocityX}) => {
if (!isScrollEnabled.get()) {
return;
}

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));
}

isPagerScrolling.set(false);
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
})
.withRef(pagerRef),
[attachments.length, canUseTouchScreen, cellWidth, page, isScrollEnabled, scrollRef, isPagerScrolling],
);

// Scroll position is affected when window width is resized, so we readjust it on width changes
useEffect(() => {
Expand Down Expand Up @@ -303,6 +300,4 @@ function AttachmentCarouselView({
);
}

// OXC's React Compiler bails on this file (refs accessed during render in gesture handlers), so it
// is not memoized on web. Memoize it explicitly to keep parent-driven re-renders cheap there.
export default React.memo(AttachmentCarouselView);
export default AttachmentCarouselView;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {NativeSyntheticEvent} from 'react-native';

import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {GestureDetector, useNativeGesture} from 'react-native-gesture-handler';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';

Expand Down Expand Up @@ -85,7 +85,7 @@ function AttachmentCarouselPager({items, activeAttachmentID, initialPage, setSho

const extractItemKey = useCallback((item: Attachment, index: number) => `attachmentID-${item.attachmentID}-${index}`, []);

const nativeGestureHandler = Gesture.Native();
const nativeGestureHandler = useNativeGesture();

const stateValue = useMemo<AttachmentCarouselPagerStateContextType>(
() => ({
Expand Down
15 changes: 9 additions & 6 deletions src/components/Attachments/AttachmentCarousel/Pager/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {Attachment, AttachmentSource} from '@components/Attachments/types';

import type {ForwardedRef} from 'react';
import type {GestureType} from 'react-native-gesture-handler';
import type {BaseGestureConfig} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture';
import type {NativeGesture, PanGesture} from 'react-native-gesture-handler';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
import type Animated from 'react-native-reanimated';
import type {AnimatedRef, SharedValue} from 'react-native-reanimated';

/** The pager items array is used within the pager to render and navigate between the images */
type AttachmentCarouselPagerItems = Pick<Attachment, 'attachmentID'> & {
Expand All @@ -28,8 +28,11 @@ type AttachmentCarouselPagerStateContextType = {
/** Index of the currently active page */
activePage: number;

/** Ref to the active attachment */
pagerRef?: ForwardedRef<PagerView | GestureType> | BaseGestureConfig['ref'];
/** Ref to the pager: the PagerView on native and the attachment list on web */
pagerRef?: ForwardedRef<PagerView> | AnimatedRef<Animated.FlatList<Attachment>>;

/** The pan gesture that swipes between attachments on web. The pan gesture of the attachment needs to work simultaneously with it */
pagerGesture?: PanGesture;

/** Indicates if the pager is currently scrolling */
isPagerScrolling: SharedValue<boolean>;
Expand All @@ -38,7 +41,7 @@ type AttachmentCarouselPagerStateContextType = {
isScrollEnabled: SharedValue<boolean>;

/** In case we need a gesture that should work simultaneously with panning in MultiGestureCanvas */
externalGestureHandler?: GestureType;
externalGestureHandler?: NativeGesture;
};

type AttachmentCarouselPagerActionsContextType = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useThemeStyles from '@hooks/useThemeStyles';

import React, {memo, useMemo} from 'react';
import {StyleSheet, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {GestureDetector, usePanGesture} from 'react-native-gesture-handler';
import Animated, {useSharedValue} from 'react-native-reanimated';

import type AttachmentViewPdfProps from './types';
Expand Down Expand Up @@ -33,9 +33,9 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
const offsetY = useSharedValue(0);
const isPanGestureActive = useSharedValue(false);

const Pan = Gesture.Pan()
.manualActivation(true)
.onTouchesMove((evt) => {
const Pan = usePanGesture({
manualActivation: true,
onTouchesMove: (evt) => {
if (offsetX.get() !== 0 && offsetY.get() !== 0 && isScrollEnabled && scale.get() === 1) {
const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.get());
const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.get());
Expand All @@ -54,14 +54,15 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
isPanGestureActive.set(true);
offsetX.set(evt.allTouches.at(0)?.absoluteX ?? 0);
offsetY.set(evt.allTouches.at(0)?.absoluteY ?? 0);
})
.onTouchesUp(() => {
},
onTouchesUp: () => {
isPanGestureActive.set(false);
if (!isScrollEnabled) {
return;
}
isScrollEnabled.set(scale.get() === 1);
});
},
});

const Content = useMemo(
() => (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Attachments/MultiGestureIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type MultiGestureIconProps = {
/** Renders an icon inside a multi-gesture canvas for pinch, pan, and swipe interactions. */
function MultiGestureIcon({additionalStyles, src, contentSize, fill}: MultiGestureIconProps) {
const {canvasSize, updateCanvasSize, isCanvasLoading} = useCanvasSize();
const {pagerRef, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager();
const {pagerGesture, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager();

return (
<View
Expand All @@ -42,7 +42,7 @@ function MultiGestureIcon({additionalStyles, src, contentSize, fill}: MultiGestu
canvasSize={canvasSize}
contentSize={contentSize}
zoomRange={DEFAULT_ZOOM_RANGE}
pagerRef={pagerRef}
pagerGesture={pagerGesture}
isUsedInCarousel={false}
isPagerScrollEnabled={isScrollEnabled}
onTap={onTap}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {SharedValue} from 'react-native-reanimated';
import {useSharedValue} from 'react-native-reanimated';

type IconCarouselPagerProps = {
pagerRef: AttachmentCarouselPagerStateContextType['pagerRef'];
pagerGesture: AttachmentCarouselPagerStateContextType['pagerGesture'];

/** Whether horizontal pager scrolling is enabled. */
isScrollEnabled: SharedValue<boolean>;
Expand All @@ -26,15 +26,15 @@ function useIconCarouselPager(): IconCarouselPagerProps {

if (state === null || actions === null) {
return {
pagerRef: undefined,
pagerGesture: undefined,
isScrollEnabled: isScrollingEnabledFallback,
onTap: () => {},
onSwipeDown: () => {},
};
}

return {
pagerRef: state.pagerRef,
pagerGesture: state.pagerGesture,
isScrollEnabled: state.isScrollEnabled,
onTap: actions.onTap ?? (() => {}),
onSwipeDown: actions.onSwipeDown ?? (() => {}),
Expand Down
5 changes: 3 additions & 2 deletions src/components/Lightbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange
onScaleChanged: () => {},
onSwipeDown: () => {},
pagerRef: undefined,
pagerGesture: undefined,
externalGestureHandler: undefined,
};
} else {
Expand All @@ -116,7 +117,7 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange
onTap,
onScaleChanged: onScaleChangedContext,
onSwipeDown,
pagerRef,
pagerGesture,
isScrollEnabled,
externalGestureHandler,
} = carouselContext;
Expand Down Expand Up @@ -250,7 +251,7 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange
canvasSize={canvasSize}
contentSize={contentSize}
zoomRange={zoomRange}
pagerRef={pagerRef}
pagerGesture={pagerGesture}
isUsedInCarousel={isUsedInCarousel}
shouldDisableTransformationGestures={isPagerScrolling}
isPagerScrollEnabled={isScrollEnabled}
Expand Down
Loading
Loading