diff options
author | Aryan Goharzad <arrygoo@gmail.com> | 2023-01-25 18:25:34 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-25 17:25:34 -0600 |
commit | eb33c3fa812cc087db14a6b6ba743e982b26c462 (patch) | |
tree | d098f7a804c67755f39e95bbbfd56887bacf476c /src/view/com/lightbox/ImageViewing/components/ImageItem | |
parent | adf328b50ce98c5ebd3282fe897ddfdcd0de8011 (diff) | |
download | voidsky-eb33c3fa812cc087db14a6b6ba743e982b26c462.tar.zst |
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
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/components/ImageItem')
3 files changed, 341 insertions, 0 deletions
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<ScrollView & NativeMethodsMixin>(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<NativeScrollEvent>) => { + 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<NativeScrollEvent>) => { + const offsetY = nativeEvent?.contentOffset?.y ?? 0 + + scrollValueY.setValue(offsetY) + } + + return ( + <ScrollView + ref={imageContainer} + style={styles.listItem} + pagingEnabled + nestedScrollEnabled + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.imageScrollContainer} + scrollEnabled={swipeToCloseEnabled} + {...(swipeToCloseEnabled && { + onScroll, + onScrollEndDrag, + })}> + <Animated.Image + {...panHandlers} + source={imageSrc} + style={imageStylesWithOpacity} + onLoad={onLoaded} + /> + {(!isLoaded || !imageDimensions) && <ImageLoading />} + </ScrollView> + ) +} + +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<ScrollView>(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<NativeScrollEvent>) => { + 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<NativeScrollEvent>) => { + const offsetY = nativeEvent?.contentOffset?.y ?? 0 + + if (nativeEvent?.zoomScale > 1) { + return + } + + scrollValueY.setValue(offsetY) + } + + const onLongPressHandler = useCallback(() => { + onLongPress(imageSrc) + }, [imageSrc, onLongPress]) + + return ( + <View> + <ScrollView + ref={scrollViewRef} + style={styles.listItem} + pinchGestureEnabled + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + maximumZoomScale={maxScale} + contentContainerStyle={styles.imageScrollContainer} + scrollEnabled={swipeToCloseEnabled} + onScrollEndDrag={onScrollEndDrag} + scrollEventThrottle={1} + {...(swipeToCloseEnabled && { + onScroll, + })}> + {(!loaded || !imageDimensions) && <ImageLoading />} + <TouchableWithoutFeedback + onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined} + onLongPress={onLongPressHandler} + delayLongPress={delayLongPress}> + <Animated.Image + source={imageSrc} + style={imageStylesWithOpacity} + onLoad={() => setLoaded(true)} + /> + </TouchableWithoutFeedback> + </ScrollView> + </View> + ) +} + +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 = () => ( + <View style={styles.loading}> + <ActivityIndicator size="small" color="#FFF" /> + </View> +) + +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, + }, +}) |