From eb33c3fa812cc087db14a6b6ba743e982b26c462 Mon Sep 17 00:00:00 2001 From: Aryan Goharzad Date: Wed, 25 Jan 2023 18:25:34 -0500 Subject: Saves image on long press (#83) * Saves image on long press * Adds save on long press * Forking lightbox * move to wrapper only to the bottom sheet to reduce impact of this change * lint * lint * lint * Use official `share` API * Clean up cache after download * comment * comment * Reduce swipe close velocity * Updates per feedback * lint * bugfix * Adds delayed press-in for TouchableOpacity --- .../components/ImageItem/ImageItem.android.tsx | 152 +++++++++++++++++++++ .../components/ImageItem/ImageItem.ios.tsx | 152 +++++++++++++++++++++ .../components/ImageItem/ImageLoading.tsx | 37 +++++ 3 files changed, 341 insertions(+) create mode 100644 src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx create mode 100644 src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx create mode 100644 src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx (limited to 'src/view/com/lightbox/ImageViewing/components/ImageItem') diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx new file mode 100644 index 000000000..01a53ff6f --- /dev/null +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -0,0 +1,152 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, {useCallback, useRef, useState} from 'react' + +import { + Animated, + ScrollView, + Dimensions, + StyleSheet, + NativeScrollEvent, + NativeSyntheticEvent, + NativeMethodsMixin, +} from 'react-native' + +import useImageDimensions from '../../hooks/useImageDimensions' +import usePanResponder from '../../hooks/usePanResponder' + +import {getImageStyles, getImageTransform} from '../../utils' +import {ImageSource} from '../../@types' +import {ImageLoading} from './ImageLoading' + +const SWIPE_CLOSE_OFFSET = 75 +const SWIPE_CLOSE_VELOCITY = 1.75 +const SCREEN = Dimensions.get('window') +const SCREEN_WIDTH = SCREEN.width +const SCREEN_HEIGHT = SCREEN.height + +type Props = { + imageSrc: ImageSource + onRequestClose: () => void + onZoom: (isZoomed: boolean) => void + onLongPress: (image: ImageSource) => void + delayLongPress: number + swipeToCloseEnabled?: boolean + doubleTapToZoomEnabled?: boolean +} + +const ImageItem = ({ + imageSrc, + onZoom, + onRequestClose, + onLongPress, + delayLongPress, + swipeToCloseEnabled = true, + doubleTapToZoomEnabled = true, +}: Props) => { + const imageContainer = useRef(null) + const imageDimensions = useImageDimensions(imageSrc) + const [translate, scale] = getImageTransform(imageDimensions, SCREEN) + const scrollValueY = new Animated.Value(0) + const [isLoaded, setLoadEnd] = useState(false) + + const onLoaded = useCallback(() => setLoadEnd(true), []) + const onZoomPerformed = useCallback( + (isZoomed: boolean) => { + onZoom(isZoomed) + if (imageContainer?.current) { + imageContainer.current.setNativeProps({ + scrollEnabled: !isZoomed, + }) + } + }, + [onZoom], + ) + + const onLongPressHandler = useCallback(() => { + onLongPress(imageSrc) + }, [imageSrc, onLongPress]) + + const [panHandlers, scaleValue, translateValue] = usePanResponder({ + initialScale: scale || 1, + initialTranslate: translate || {x: 0, y: 0}, + onZoom: onZoomPerformed, + doubleTapToZoomEnabled, + onLongPress: onLongPressHandler, + delayLongPress, + }) + + const imagesStyles = getImageStyles( + imageDimensions, + translateValue, + scaleValue, + ) + const imageOpacity = scrollValueY.interpolate({ + inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], + outputRange: [0.7, 1, 0.7], + }) + const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity} + + const onScrollEndDrag = ({ + nativeEvent, + }: NativeSyntheticEvent) => { + const velocityY = nativeEvent?.velocity?.y ?? 0 + const offsetY = nativeEvent?.contentOffset?.y ?? 0 + + if ( + (Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY && + offsetY > SWIPE_CLOSE_OFFSET) || + offsetY > SCREEN_HEIGHT / 2 + ) { + onRequestClose() + } + } + + const onScroll = ({nativeEvent}: NativeSyntheticEvent) => { + const offsetY = nativeEvent?.contentOffset?.y ?? 0 + + scrollValueY.setValue(offsetY) + } + + return ( + + + {(!isLoaded || !imageDimensions) && } + + ) +} + +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + imageScrollContainer: { + height: SCREEN_HEIGHT * 2, + }, +}) + +export default React.memo(ImageItem) diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx new file mode 100644 index 000000000..12d37e283 --- /dev/null +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -0,0 +1,152 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, {useCallback, useRef, useState} from 'react' + +import { + Animated, + Dimensions, + ScrollView, + StyleSheet, + View, + NativeScrollEvent, + NativeSyntheticEvent, + TouchableWithoutFeedback, +} from 'react-native' + +import useDoubleTapToZoom from '../../hooks/useDoubleTapToZoom' +import useImageDimensions from '../../hooks/useImageDimensions' + +import {getImageStyles, getImageTransform} from '../../utils' +import {ImageSource} from '../../@types' +import {ImageLoading} from './ImageLoading' + +const SWIPE_CLOSE_OFFSET = 75 +const SWIPE_CLOSE_VELOCITY = 1 +const SCREEN = Dimensions.get('screen') +const SCREEN_WIDTH = SCREEN.width +const SCREEN_HEIGHT = SCREEN.height + +type Props = { + imageSrc: ImageSource + onRequestClose: () => void + onZoom: (scaled: boolean) => void + onLongPress: (image: ImageSource) => void + delayLongPress: number + swipeToCloseEnabled?: boolean + doubleTapToZoomEnabled?: boolean +} + +const ImageItem = ({ + imageSrc, + onZoom, + onRequestClose, + onLongPress, + delayLongPress, + swipeToCloseEnabled = true, + doubleTapToZoomEnabled = true, +}: Props) => { + const scrollViewRef = useRef(null) + const [loaded, setLoaded] = useState(false) + const [scaled, setScaled] = useState(false) + const imageDimensions = useImageDimensions(imageSrc) + const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN) + + const [translate, scale] = getImageTransform(imageDimensions, SCREEN) + const scrollValueY = new Animated.Value(0) + const scaleValue = new Animated.Value(scale || 1) + const translateValue = new Animated.ValueXY(translate) + const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1 + + const imageOpacity = scrollValueY.interpolate({ + inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], + outputRange: [0.5, 1, 0.5], + }) + const imagesStyles = getImageStyles( + imageDimensions, + translateValue, + scaleValue, + ) + const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity} + + const onScrollEndDrag = useCallback( + ({nativeEvent}: NativeSyntheticEvent) => { + const velocityY = nativeEvent?.velocity?.y ?? 0 + const currentScaled = nativeEvent?.zoomScale > 1 + + onZoom(currentScaled) + setScaled(currentScaled) + + if ( + !currentScaled && + swipeToCloseEnabled && + Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY + ) { + onRequestClose() + } + }, + [onRequestClose, onZoom, swipeToCloseEnabled], + ) + + const onScroll = ({nativeEvent}: NativeSyntheticEvent) => { + const offsetY = nativeEvent?.contentOffset?.y ?? 0 + + if (nativeEvent?.zoomScale > 1) { + return + } + + scrollValueY.setValue(offsetY) + } + + const onLongPressHandler = useCallback(() => { + onLongPress(imageSrc) + }, [imageSrc, onLongPress]) + + return ( + + + {(!loaded || !imageDimensions) && } + + setLoaded(true)} + /> + + + + ) +} + +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + imageScrollContainer: { + height: SCREEN_HEIGHT, + }, +}) + +export default React.memo(ImageItem) diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx new file mode 100644 index 000000000..9667fcaa7 --- /dev/null +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) JOB TODAY S.A. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react' + +import {ActivityIndicator, Dimensions, StyleSheet, View} from 'react-native' + +const SCREEN = Dimensions.get('screen') +const SCREEN_WIDTH = SCREEN.width +const SCREEN_HEIGHT = SCREEN.height + +export const ImageLoading = () => ( + + + +) + +const styles = StyleSheet.create({ + listItem: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + loading: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + alignItems: 'center', + justifyContent: 'center', + }, + imageScrollContainer: { + height: SCREEN_HEIGHT, + }, +}) -- cgit 1.4.1