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

Image zoom for mobile browser apps #43620

Merged
merged 34 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bd8910c
Add Handle image zoom for mobile browser apps
ZhenjaHorbach Jun 12, 2024
2bd1a2a
Make a little refactoring
ZhenjaHorbach Jun 12, 2024
ca69b0f
Fix bug with close modal on send attachment screen
ZhenjaHorbach Jun 12, 2024
2e34376
Reset image position when onSwipeDown is undefined
ZhenjaHorbach Jun 12, 2024
5894906
Refactor code
ZhenjaHorbach Jun 13, 2024
e30a3bb
Make some minnor changes
ZhenjaHorbach Jun 13, 2024
2f40b54
Update animation for changing attachment item
ZhenjaHorbach Jun 13, 2024
ebb0eb4
Remove unnecessary type
ZhenjaHorbach Jun 13, 2024
fec109e
Fix minnor issues related with arrows
ZhenjaHorbach Jun 14, 2024
6c6390b
Fix bug with animation for carousel
ZhenjaHorbach Jun 14, 2024
4fa559f
Fix bug with animation for carousel x2
ZhenjaHorbach Jun 15, 2024
1d5f485
Fix conflicts and update branch
ZhenjaHorbach Jun 20, 2024
e303843
Update maxDelay for doubleTapGesture for mobile browsers
ZhenjaHorbach Jun 20, 2024
0bfd329
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jun 23, 2024
7345cf8
Fix bug with double tap for ios chrome
ZhenjaHorbach Jun 24, 2024
5029ed3
Make some code optimization
ZhenjaHorbach Jun 24, 2024
b16ab81
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jun 25, 2024
a5795c4
Fix comments
ZhenjaHorbach Jun 25, 2024
b2b7ab8
Fix comments x2
ZhenjaHorbach Jun 25, 2024
f1c8297
Fix bug with arrows
ZhenjaHorbach Jun 25, 2024
20cb61c
Hide arrows when item carousel is zoomed
ZhenjaHorbach Jun 25, 2024
52cd759
Fix bug with image lines after zoom
ZhenjaHorbach Jun 27, 2024
8a38e0a
Refactor code
ZhenjaHorbach Jun 27, 2024
bd0b6f1
Revert unnecessary changes
ZhenjaHorbach Jun 27, 2024
d17d1ee
Fix bug with image lines after zoom for IOS web
ZhenjaHorbach Jun 27, 2024
ece63e7
Remove unnecessary style
ZhenjaHorbach Jun 27, 2024
ec7ffbe
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jun 27, 2024
b4a70bf
Fix bug with one finger swipe when the image is zoomed
ZhenjaHorbach Jun 28, 2024
fb32193
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jul 1, 2024
531e975
Refactor the code to make it similar to native
ZhenjaHorbach Jul 1, 2024
14175f9
Refactor the code to make it similar to native x2
ZhenjaHorbach Jul 1, 2024
7d0bc47
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jul 1, 2024
edd2751
Merge branch 'main' into handle-image-zoom
ZhenjaHorbach Jul 2, 2024
8511f5f
Fix comments
ZhenjaHorbach Jul 3, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import {createContext} from 'react';
import type {GestureType} from 'react-native-gesture-handler';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
import type {AttachmentSource} from '@components/Attachments/types';
Expand All @@ -17,16 +18,28 @@ type AttachmentCarouselPagerItems = {
};

type AttachmentCarouselPagerContextValue = {
/** The list of items that are shown in the pager */
/** List of items displayed in the attachment */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/** List of items displayed in the attachment */
/** List of attachments displayed in the pager */

Is this more accurate?

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 think yes
But the pager sounds a little unclear 😅
But I don't mind
I'll update now

pagerItems: AttachmentCarouselPagerItems[];

/** The index of the active page */
/** Index of the currently active page */
activePage: number;
pagerRef?: ForwardedRef<PagerView>;

/** Ref to the active attachment */
pagerRef?: ForwardedRef<PagerView | GestureType>;

/** Indicates if the pager is currently scrolling */
isPagerScrolling: SharedValue<boolean>;

/** Indicates if scrolling is enabled for the attachment */
isScrollEnabled: SharedValue<boolean>;

/** Function to call after a tap event */
onTap: () => void;

/** Function to call when the scale changes */
onScaleChanged: (scale: number) => void;

/** Function to call after a swipe down event */
onSwipeDown: () => void;
};

Expand Down
52 changes: 7 additions & 45 deletions src/components/Attachments/AttachmentCarousel/Pager/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ForwardedRef} from 'react';
import type {ForwardedRef, SetStateAction} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent} from 'react-native';
import {View} from 'react-native';
Expand All @@ -8,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
import useCarouselContextEvents from '@components/Attachments/AttachmentCarousel/useCarouselContextEvents';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import useThemeStyles from '@hooks/useThemeStyles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
Expand Down Expand Up @@ -41,24 +42,21 @@ type AttachmentCarouselPagerProps = {
>,
) => void;

/**
* A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes.
* @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows.
*/
onRequestToggleArrows: (showArrows?: boolean) => void;

/** A callback that is called when swipe-down-to-close gesture happens */
onClose: () => void;

/** Sets the visibility of the arrows. */
setShouldShowArrows: (show?: SetStateAction<boolean>) => void;
};

function AttachmentCarouselPager(
{items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps,
{items, activeSource, initialPage, setShouldShowArrows, onPageSelected, onClose}: AttachmentCarouselPagerProps,
ref: ForwardedRef<AttachmentCarouselPagerHandle>,
) {
const {handleTap, handleScaleChange} = useCarouselContextEvents(setShouldShowArrows);
const styles = useThemeStyles();
const pagerRef = useRef<PagerView>(null);

const scale = useRef(1);
const isPagerScrolling = useSharedValue(false);
const isScrollEnabled = useSharedValue(true);

Expand All @@ -80,42 +78,6 @@ function AttachmentCarouselPager(
/** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */
const pagerItems = useMemo(() => items.map((item, index) => ({source: item.source, index, isActive: index === activePageIndex})), [activePageIndex, items]);

/**
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
* It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
* as well as enabling/disabling the carousel buttons.
*/
const handleScaleChange = useCallback(
(newScale: number) => {
if (newScale === scale.current) {
return;
}

scale.current = newScale;

const newIsScrollEnabled = newScale === 1;
if (isScrollEnabled.value === newIsScrollEnabled) {
return;
}

isScrollEnabled.value = newIsScrollEnabled;
onRequestToggleArrows(newIsScrollEnabled);
},
[isScrollEnabled, onRequestToggleArrows],
);

/**
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
* It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
*/
const handleTap = useCallback(() => {
if (!isScrollEnabled.value) {
return;
}

onRequestToggleArrows();
}, [isScrollEnabled.value, onRequestToggleArrows]);

const extractItemKey = useCallback(
(item: Attachment, index: number) =>
typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
Expand Down
18 changes: 1 addition & 17 deletions src/components/Attachments/AttachmentCarousel/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[autoHideArrows, page, updatePage],
);

/**
* Toggles the arrows visibility
* @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value
*/
const toggleArrows = useCallback(
(showArrows?: boolean) => {
if (showArrows === undefined) {
setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
return;
}

setShouldShowArrows(showArrows);
},
[setShouldShowArrows],
);

const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];

if (page == null) {
Expand Down Expand Up @@ -147,7 +131,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
items={attachments}
initialPage={page}
activeSource={activeSource}
onRequestToggleArrows={toggleArrows}
setShouldShowArrows={setShouldShowArrows}
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
onClose={onClose}
ref={pagerRef}
Expand Down
98 changes: 69 additions & 29 deletions src/components/Attachments/AttachmentCarousel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import isEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {MutableRefObject} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ListRenderItemInfo} from 'react-native';
import {Keyboard, PixelRatio, View} from 'react-native';
import type {GestureType} from 'react-native-gesture-handler';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated';
import Animated, {scrollTo, useAnimatedRef, useSharedValue} 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';
Expand All @@ -22,8 +24,10 @@ import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
import extractAttachments from './extractAttachments';
import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext';
import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types';
import useCarouselArrows from './useCarouselArrows';
import useCarouselContextEvents from './useCarouselContextEvents';

const viewabilityConfig = {
// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
Expand All @@ -33,13 +37,15 @@ const viewabilityConfig = {

const MIN_FLING_VELOCITY = 500;

function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) {
function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose}: AttachmentCarouselProps) {
const theme = useTheme();
const {translate} = useLocalize();
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {isFullScreenRef} = useFullScreenContext();
const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();
const nope = useSharedValue(false);
const pagerRef = useRef<GestureType>(null);

const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();

Expand All @@ -52,6 +58,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [activeSource, setActiveSource] = useState<AttachmentSource | null>(source);
const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
const {handleTap, handleScaleChange, scale} = useCarouselContextEvents(setShouldShowArrows);

useEffect(() => {
if (!canUseTouchScreen) {
return;
}
setShouldShowArrows(true);
}, [canUseTouchScreen, page, setShouldShowArrows]);

const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);

Expand Down Expand Up @@ -169,27 +183,51 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[cellWidth],
);

const context = useMemo(
() => ({
pagerItems: [{source, index: 0, isActive: true}],
activePage: 0,
pagerRef,
isPagerScrolling: nope,
isScrollEnabled: nope,
onTap: handleTap,
onScaleChanged: handleScaleChange,
onSwipeDown: onClose,
}),
[source, nope, handleTap, handleScaleChange, onClose],
);

/** Defines how a single attachment should be rendered */
const renderItem = useCallback(
({item}: ListRenderItemInfo<Attachment>) => (
<View style={[styles.h100, {width: cellWidth}]}>
<CarouselItem
item={item}
isFocused={activeSource === item.source}
onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState) => !oldState) : undefined}
onPress={canUseTouchScreen ? handleTap : undefined}
isModalHovered={shouldShowArrows}
/>
</View>
),
[activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100],
[activeSource, canUseTouchScreen, cellWidth, handleTap, 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))
.onUpdate(({translationX}) => {
if (scale.current !== 1) {
return;
}

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

let newIndex;
if (velocityX > MIN_FLING_VELOCITY) {
// User flung to the right
Expand All @@ -204,8 +242,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
}

scrollTo(scrollRef, newIndex * cellWidth, 0, true);
}),
[attachments.length, canUseTouchScreen, cellWidth, page, scrollRef],
})
.withRef(pagerRef as MutableRefObject<GestureType | undefined>),
[attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef],
);

return (
Expand Down Expand Up @@ -233,27 +272,28 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
autoHideArrow={autoHideArrows}
cancelAutoHideArrow={cancelAutoHideArrows}
/>

<GestureDetector gesture={pan}>
<Animated.FlatList
keyboardShouldPersistTaps="handled"
horizontal
showsHorizontalScrollIndicator={false}
// 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}
renderItem={renderItem}
getItemLayout={getItemLayout}
keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
</GestureDetector>
<AttachmentCarouselPagerContext.Provider value={context}>
<GestureDetector gesture={pan}>
<Animated.FlatList
keyboardShouldPersistTaps="handled"
horizontal
showsHorizontalScrollIndicator={false}
// 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}
renderItem={renderItem}
getItemLayout={getItemLayout}
keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
</GestureDetector>
</AttachmentCarouselPagerContext.Provider>

<CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function useCarouselArrows() {
}, CONST.ARROW_HIDE_DELAY);
}, [canUseTouchScreen, cancelAutoHideArrows]);

/**
* Sets the visibility of the arrows.
*/
const setShouldShowArrows = useCallback(
(show: SetStateAction<boolean> = true) => {
setShouldShowArrowsInternal(show);
Expand Down
Loading
Loading