diff --git a/package.json b/package.json
index 7f88d524..4281b154 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"react-native-language-detection": "^0.1.0",
"react-native-pager-view": "^5.4.25",
"react-native-reanimated": "^2.9.1",
+ "react-native-reanimated-zoom": "^0.3.0",
"react-native-safe-area-context": "^4.3.1",
"react-native-screens": "^3.16.0",
"react-native-share-menu": "^6.0.0",
diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx
index 01db6c04..5afcbff6 100644
--- a/src/components/GracefullyImage.tsx
+++ b/src/components/GracefullyImage.tsx
@@ -67,9 +67,7 @@ const GracefullyImage = ({
const onLoad = () => {
setImageLoaded(true)
if (setImageDimensions && source.uri) {
- Image.getSize(source.uri, (width, height) =>
- setImageDimensions({ width, height })
- )
+ Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
}
}
const onError = () => {
@@ -81,22 +79,9 @@ const GracefullyImage = ({
const blurhashView = useMemo(() => {
if (hidden || !imageLoaded) {
if (blurhash) {
- return (
-
- )
+ return
} else {
- return (
-
- )
+ return
}
} else {
return null
@@ -105,26 +90,17 @@ const GracefullyImage = ({
return (
{uri.preview && !imageLoaded ? (
) : null}
{Platform.OS === 'ios' ? (
diff --git a/src/screens/ImageViewer/@types/extensions.d.ts b/src/screens/ImageViewer/@types/extensions.d.ts
deleted file mode 100644
index fb7fae83..00000000
--- a/src/screens/ImageViewer/@types/extensions.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as rn from "react-native";
-
-declare module "react-native" {
- class VirtualizedList extends React.Component<
- VirtualizedListProps
- > {}
-}
diff --git a/src/screens/ImageViewer/@types/index.ts b/src/screens/ImageViewer/@types/index.ts
deleted file mode 100644
index 780313fd..00000000
--- a/src/screens/ImageViewer/@types/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * 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.
- *
- */
-
-export type Dimensions = {
- width: number
- height: number
-}
-
-export type Position = {
- x: number
- y: number
-}
diff --git a/src/screens/ImageViewer/Root.tsx b/src/screens/ImageViewer/Root.tsx
deleted file mode 100644
index 0603902c..00000000
--- a/src/screens/ImageViewer/Root.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * 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 { RootStackParamList } from '@utils/navigation/navigators'
-import React, { ComponentType, useCallback, useEffect } from 'react'
-import {
- Animated,
- Dimensions,
- StyleSheet,
- View,
- VirtualizedList
-} from 'react-native'
-import ImageItem from './components/ImageItem'
-import useAnimatedComponents from './hooks/useAnimatedComponents'
-import useImageIndexChange from './hooks/useImageIndexChange'
-import useRequestClose from './hooks/useRequestClose'
-
-type Props = {
- images: RootStackParamList['Screen-ImagesViewer']['imageUrls']
- imageIndex: number
- onRequestClose: () => void
- onLongPress?: (
- image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
- ) => void
- onImageIndexChange?: (imageIndex: number) => void
- backgroundColor?: string
- swipeToCloseEnabled?: boolean
- delayLongPress?: number
- HeaderComponent: ComponentType<{ imageIndex: number }>
-}
-
-const DEFAULT_BG_COLOR = '#000'
-const DEFAULT_DELAY_LONG_PRESS = 800
-const SCREEN = Dimensions.get('screen')
-const SCREEN_WIDTH = SCREEN.width
-
-function ImageViewer ({
- images,
- imageIndex,
- onRequestClose,
- onLongPress = () => {},
- onImageIndexChange,
- backgroundColor = DEFAULT_BG_COLOR,
- swipeToCloseEnabled,
- delayLongPress = DEFAULT_DELAY_LONG_PRESS,
- HeaderComponent
-}: Props) {
- const imageList = React.createRef<
- VirtualizedList
- >()
- const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose)
- const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN)
- const [headerTransform, toggleBarsVisible] = useAnimatedComponents()
-
- useEffect(() => {
- if (onImageIndexChange) {
- onImageIndexChange(currentImageIndex)
- }
- }, [currentImageIndex])
-
- const onZoom = useCallback(
- (isScaled: boolean) => {
- // @ts-ignore
- imageList?.current?.setNativeProps({ scrollEnabled: !isScaled })
- toggleBarsVisible(!isScaled)
- },
- [imageList]
- )
-
- return (
-
-
- {React.createElement(HeaderComponent, {
- imageIndex: currentImageIndex
- })}
-
- images.length - 1 ? images.length - 1 : imageIndex
- }
- getItem={(_, index) => images[index]}
- getItemCount={() => images.length}
- getItemLayout={(_, index) => ({
- length: SCREEN_WIDTH,
- offset: SCREEN_WIDTH * index,
- index
- })}
- renderItem={({ item: imageSrc }) => (
-
- )}
- onMomentumScrollEnd={onScroll}
- keyExtractor={imageSrc => imageSrc.url}
- />
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000'
- },
- header: {
- position: 'absolute',
- width: '100%',
- zIndex: 1,
- top: 0
- }
-})
-
-export default ImageViewer
diff --git a/src/screens/ImageViewer/components/ImageItem.android.tsx b/src/screens/ImageViewer/components/ImageItem.android.tsx
deleted file mode 100644
index 93473e02..00000000
--- a/src/screens/ImageViewer/components/ImageItem.android.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * 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 GracefullyImage from '@components/GracefullyImage'
-import { RootStackParamList } from '@utils/navigation/navigators'
-import React, { useState, useCallback } from 'react'
-import { Animated, Dimensions, StyleSheet } from 'react-native'
-import usePanResponder from '../hooks/usePanResponder'
-import { getImageStyles, getImageTransform } from '../utils'
-
-const SCREEN = Dimensions.get('window')
-const SCREEN_WIDTH = SCREEN.width
-const SCREEN_HEIGHT = SCREEN.height
-
-type Props = {
- imageSrc: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
- onRequestClose: () => void
- onZoom: (isZoomed: boolean) => void
- onLongPress: (
- image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
- ) => void
- delayLongPress: number
- swipeToCloseEnabled?: boolean
- doubleTapToZoomEnabled?: boolean
-}
-
-const ImageItem = ({
- imageSrc,
- onZoom,
- onRequestClose,
- onLongPress,
- delayLongPress,
- doubleTapToZoomEnabled = true
-}: Props) => {
- const imageContainer = React.createRef()
- const [imageDimensions, setImageDimensions] = useState({
- width: imageSrc.width || 0,
- height: imageSrc.height || 0
- })
- const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
-
- const onZoomPerformed = (isZoomed: boolean) => {
- onZoom(isZoomed)
- if (imageContainer?.current) {
- // @ts-ignore
- imageContainer.current.setNativeProps({
- scrollEnabled: !isZoomed
- })
- }
- }
-
- 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,
- onRequestClose
- })
-
- const imagesStyles = getImageStyles(
- imageDimensions,
- translateValue,
- scaleValue
- )
-
- return (
-
-
- }
- />
-
- )
-}
-
-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/screens/ImageViewer/components/ImageItem.d.ts b/src/screens/ImageViewer/components/ImageItem.d.ts
deleted file mode 100644
index b1bd632f..00000000
--- a/src/screens/ImageViewer/components/ImageItem.d.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * 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 { GestureResponderEvent } from "react-native";
-import { ImageSource } from "../@types";
-
-declare type Props = {
- imageSrc: ImageSource;
- onRequestClose: () => void;
- onZoom: (isZoomed: boolean) => void;
- onLongPress: (image: ImageSource) => void;
- delayLongPress: number;
- swipeToCloseEnabled?: boolean;
- doubleTapToZoomEnabled?: boolean;
-};
-
-declare const _default: React.MemoExoticComponent<({
- imageSrc,
- onZoom,
- onRequestClose,
- onLongPress,
- delayLongPress,
- swipeToCloseEnabled,
-}: Props) => JSX.Element>;
-
-export default _default;
diff --git a/src/screens/ImageViewer/components/ImageItem.ios.tsx b/src/screens/ImageViewer/components/ImageItem.ios.tsx
deleted file mode 100644
index 25932987..00000000
--- a/src/screens/ImageViewer/components/ImageItem.ios.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-/**
- * 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 GracefullyImage from '@components/GracefullyImage'
-import { RootStackParamList } from '@utils/navigation/navigators'
-import React, { createRef, useCallback, useRef, useState } from 'react'
-import {
- Animated,
- Dimensions,
- NativeScrollEvent,
- NativeSyntheticEvent,
- ScrollView,
- StyleSheet
-} from 'react-native'
-import {
- LongPressGestureHandler,
- State,
- TapGestureHandler
-} from 'react-native-gesture-handler'
-import useDoubleTapToZoom from '../hooks/useDoubleTapToZoom'
-import { getImageStyles, getImageTransform } from '../utils'
-
-const SWIPE_CLOSE_OFFSET = 75
-const SWIPE_CLOSE_VELOCITY = 0.55
-const SCREEN = Dimensions.get('screen')
-const SCREEN_WIDTH = SCREEN.width
-const SCREEN_HEIGHT = SCREEN.height
-
-type Props = {
- imageSrc: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
- onRequestClose: () => void
- onZoom: (scaled: boolean) => void
- onLongPress: (
- image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
- ) => void
- swipeToCloseEnabled?: boolean
-}
-
-const doubleTap = createRef()
-
-const ImageItem = ({
- imageSrc,
- onZoom,
- onRequestClose,
- onLongPress,
- swipeToCloseEnabled = true
-}: Props) => {
- const scrollViewRef = useRef(null)
- const [scaled, setScaled] = useState(false)
- const [imageDimensions, setImageDimensions] = useState({
- width: imageSrc.width || 1,
- height: imageSrc.height || 1
- })
- 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 scaled = nativeEvent?.zoomScale > 1
-
- onZoom(scaled)
- setScaled(scaled)
-
- if (
- !scaled &&
- swipeToCloseEnabled &&
- Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY
- ) {
- onRequestClose()
- }
- },
- [scaled]
- )
-
- const onScroll = ({
- nativeEvent
- }: NativeSyntheticEvent) => {
- const offsetY = nativeEvent?.contentOffset?.y ?? 0
-
- if (nativeEvent?.zoomScale > 1) {
- return
- }
-
- scrollValueY.setValue(offsetY)
- }
-
- return (
- {
- if (nativeEvent.state === State.ACTIVE) {
- onLongPress(imageSrc)
- }
- }}
- >
-
- nativeEvent.state === State.ACTIVE && onRequestClose()
- }
- waitFor={doubleTap}
- >
-
-
-
- }
- />
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- listItem: {
- width: SCREEN_WIDTH,
- height: SCREEN_HEIGHT
- },
- imageScrollContainer: {
- height: SCREEN_HEIGHT
- }
-})
-
-export default React.memo(ImageItem)
diff --git a/src/screens/ImageViewer/hooks/useAnimatedComponents.ts b/src/screens/ImageViewer/hooks/useAnimatedComponents.ts
deleted file mode 100644
index 099a8ea6..00000000
--- a/src/screens/ImageViewer/hooks/useAnimatedComponents.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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 { Animated } from 'react-native'
-
-const INITIAL_POSITION = { x: 0, y: 0 }
-const ANIMATION_CONFIG = {
- duration: 200,
- useNativeDriver: true
-}
-
-const useAnimatedComponents = () => {
- const headerTranslate = new Animated.ValueXY(INITIAL_POSITION)
-
- const toggleVisible = (isVisible: boolean) => {
- if (isVisible) {
- Animated.parallel([
- Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 })
- ]).start()
- } else {
- Animated.parallel([
- Animated.timing(headerTranslate.y, {
- ...ANIMATION_CONFIG,
- toValue: -300
- })
- ]).start()
- }
- }
-
- const headerTransform = headerTranslate.getTranslateTransform()
-
- return [headerTransform, toggleVisible] as const
-}
-
-export default useAnimatedComponents
diff --git a/src/screens/ImageViewer/hooks/useDoubleTapToZoom.ts b/src/screens/ImageViewer/hooks/useDoubleTapToZoom.ts
deleted file mode 100644
index 80620b44..00000000
--- a/src/screens/ImageViewer/hooks/useDoubleTapToZoom.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * 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 } from 'react'
-import { ScrollView } from 'react-native'
-import {
- HandlerStateChangeEvent,
- State,
- TapGestureHandlerEventPayload
-} from 'react-native-gesture-handler'
-import { Dimensions } from '../@types'
-
-/**
- * This is iOS only.
- * Same functionality for Android implemented inside usePanResponder hook.
- */
-function useDoubleTapToZoom (
- scrollViewRef: React.RefObject,
- scaled: boolean,
- screen: Dimensions
-) {
- const handleDoubleTap = useCallback(
- ({
- nativeEvent
- }: HandlerStateChangeEvent) => {
- if (nativeEvent.state === State.ACTIVE) {
- const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
-
- const { absoluteX, absoluteY } = nativeEvent
- let targetX = 0
- let targetY = 0
- let targetWidth = screen.width
- let targetHeight = screen.height
-
- // Zooming in
- // TODO: Add more precise calculation of targetX, targetY based on touch
- if (!scaled) {
- targetX = absoluteX / 2
- targetY = absoluteY / 2
- targetWidth = screen.width / 2
- targetHeight = screen.height / 2
- }
-
- scrollResponderRef?.scrollResponderZoomTo({
- x: targetX,
- y: targetY,
- width: targetWidth,
- height: targetHeight,
- animated: true
- })
- }
- },
- [scaled]
- )
-
- return handleDoubleTap
-}
-
-export default useDoubleTapToZoom
diff --git a/src/screens/ImageViewer/hooks/useImageIndexChange.ts b/src/screens/ImageViewer/hooks/useImageIndexChange.ts
deleted file mode 100644
index 3b02e34a..00000000
--- a/src/screens/ImageViewer/hooks/useImageIndexChange.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * 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 { useState } from 'react'
-import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native'
-import { Dimensions } from '../@types'
-
-const useImageIndexChange = (imageIndex: number, screen: Dimensions) => {
- const [currentImageIndex, setImageIndex] = useState(imageIndex)
- const onScroll = (event: NativeSyntheticEvent) => {
- const {
- nativeEvent: {
- contentOffset: { x: scrollX }
- }
- } = event
-
- if (screen.width) {
- const nextIndex = Math.round(scrollX / screen.width)
- setImageIndex(nextIndex < 0 ? 0 : nextIndex)
- }
- }
-
- return [currentImageIndex, onScroll] as const
-}
-
-export default useImageIndexChange
diff --git a/src/screens/ImageViewer/hooks/usePanResponder.ts b/src/screens/ImageViewer/hooks/usePanResponder.ts
deleted file mode 100644
index 69f0eb9a..00000000
--- a/src/screens/ImageViewer/hooks/usePanResponder.ts
+++ /dev/null
@@ -1,406 +0,0 @@
-/**
- * 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 { useMemo, useEffect } from 'react'
-import {
- Animated,
- Dimensions,
- GestureResponderEvent,
- GestureResponderHandlers,
- NativeTouchEvent,
- PanResponderGestureState
-} from 'react-native'
-import { Position } from '../@types'
-import {
- createPanResponder,
- getDistanceBetweenTouches,
- getImageTranslate,
- getImageDimensionsByTranslate
-} from '../utils'
-
-const SCREEN = Dimensions.get('window')
-const SCREEN_WIDTH = SCREEN.width
-const SCREEN_HEIGHT = SCREEN.height
-const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT)
-
-const SCALE_MAX = 1
-const DOUBLE_TAP_DELAY = 300
-const OUT_BOUND_MULTIPLIER = 0.75
-
-type Props = {
- initialScale: number
- initialTranslate: Position
- onZoom: (isZoomed: boolean) => void
- doubleTapToZoomEnabled: boolean
- onLongPress: () => void
- delayLongPress: number
- onRequestClose: () => void
-}
-
-const usePanResponder = ({
- initialScale,
- initialTranslate,
- onZoom,
- doubleTapToZoomEnabled,
- onLongPress,
- delayLongPress,
- onRequestClose
-}: Props): Readonly<
- [GestureResponderHandlers, Animated.Value, Animated.ValueXY]
-> => {
- let numberInitialTouches = 1
- let initialTouches: NativeTouchEvent[] = []
- let currentScale = initialScale
- let currentTranslate = initialTranslate
- let tmpScale = 0
- let tmpTranslate: Position | null = null
- let isDoubleTapPerformed = false
- let lastTapTS: number | null = null
- let timer: number | null = null
- let longPressHandlerRef: number | null = null
-
- const meaningfulShift = MIN_DIMENSION * 0.01
- const scaleValue = new Animated.Value(initialScale)
- const translateValue = new Animated.ValueXY(initialTranslate)
-
- const imageDimensions = getImageDimensionsByTranslate(
- initialTranslate,
- SCREEN
- )
-
- const getBounds = (scale: number) => {
- const scaledImageDimensions = {
- width: imageDimensions.width * scale,
- height: imageDimensions.height * scale
- }
- const translateDelta = getImageTranslate(scaledImageDimensions, SCREEN)
-
- const left = initialTranslate.x - translateDelta.x
- const right = left - (scaledImageDimensions.width - SCREEN.width)
- const top = initialTranslate.y - translateDelta.y
- const bottom = top - (scaledImageDimensions.height - SCREEN.height)
-
- return [top, left, bottom, right]
- }
-
- const getTranslateInBounds = (translate: Position, scale: number) => {
- const inBoundTranslate = { x: translate.x, y: translate.y }
- const [topBound, leftBound, bottomBound, rightBound] = getBounds(scale)
-
- if (translate.x > leftBound) {
- inBoundTranslate.x = leftBound
- } else if (translate.x < rightBound) {
- inBoundTranslate.x = rightBound
- }
-
- if (translate.y > topBound) {
- inBoundTranslate.y = topBound
- } else if (translate.y < bottomBound) {
- inBoundTranslate.y = bottomBound
- }
-
- return inBoundTranslate
- }
-
- const fitsScreenByWidth = () =>
- imageDimensions.width * currentScale < SCREEN_WIDTH
- const fitsScreenByHeight = () =>
- imageDimensions.height * currentScale < SCREEN_HEIGHT
-
- useEffect(() => {
- scaleValue.addListener(({ value }) => {
- if (typeof onZoom === 'function') {
- onZoom(value !== initialScale)
- }
- })
-
- return () => scaleValue.removeAllListeners()
- })
-
- const cancelLongPressHandle = () => {
- longPressHandlerRef && clearTimeout(longPressHandlerRef)
- }
-
- const handlers = {
- onGrant: (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState
- ) => {
- numberInitialTouches = gestureState.numberActiveTouches
-
- if (gestureState.numberActiveTouches > 1) return
-
- // @ts-ignore
- longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
- },
- onStart: (
- event: GestureResponderEvent,
- gestureState: PanResponderGestureState
- ) => {
- initialTouches = event.nativeEvent.touches
- numberInitialTouches = gestureState.numberActiveTouches
-
- if (gestureState.numberActiveTouches > 1) return
-
- const tapTS = Date.now()
- !timer &&
- // @ts-ignore
- (timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
- // Handle double tap event by calculating diff between first and second taps timestamps
-
- isDoubleTapPerformed = Boolean(
- lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY
- )
-
- if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
- // @ts-ignore
- clearTimeout(timer)
- const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
- const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
- const targetScale = SCALE_MAX
- const nextScale = isScaled ? initialScale : targetScale
- const nextTranslate = isScaled
- ? initialTranslate
- : getTranslateInBounds(
- {
- x:
- initialTranslate.x +
- (SCREEN_WIDTH / 2 - touchX) * (targetScale / currentScale),
- y:
- initialTranslate.y +
- (SCREEN_HEIGHT / 2 - -touchY) * (targetScale / currentScale)
- },
- targetScale
- )
-
- onZoom(!isScaled)
-
- Animated.parallel(
- [
- Animated.timing(translateValue.x, {
- toValue: nextTranslate.x,
- duration: 300,
- useNativeDriver: true
- }),
- Animated.timing(translateValue.y, {
- toValue: nextTranslate.y,
- duration: 300,
- useNativeDriver: true
- }),
- Animated.timing(scaleValue, {
- toValue: nextScale,
- duration: 300,
- useNativeDriver: true
- })
- ],
- { stopTogether: false }
- ).start(() => {
- currentScale = nextScale
- currentTranslate = nextTranslate
- })
-
- lastTapTS = null
- timer = null
- } else {
- lastTapTS = Date.now()
- }
- },
- onMove: (
- event: GestureResponderEvent,
- gestureState: PanResponderGestureState
- ) => {
- const { dx, dy } = gestureState
-
- if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
- cancelLongPressHandle()
- timer && clearTimeout(timer)
- }
-
- // Don't need to handle move because double tap in progress (was handled in onStart)
- if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
- cancelLongPressHandle()
- timer && clearTimeout(timer)
- return
- }
-
- if (
- numberInitialTouches === 1 &&
- gestureState.numberActiveTouches === 2
- ) {
- numberInitialTouches = 2
- initialTouches = event.nativeEvent.touches
- }
-
- const isTapGesture =
- numberInitialTouches == 1 && gestureState.numberActiveTouches === 1
- const isPinchGesture =
- numberInitialTouches === 2 && gestureState.numberActiveTouches === 2
-
- if (isPinchGesture) {
- cancelLongPressHandle()
- timer && clearTimeout(timer)
-
- const initialDistance = getDistanceBetweenTouches(initialTouches)
- const currentDistance = getDistanceBetweenTouches(
- event.nativeEvent.touches
- )
-
- let nextScale = (currentDistance / initialDistance) * currentScale
-
- /**
- * In case image is scaling smaller than initial size ->
- * slow down this transition by applying OUT_BOUND_MULTIPLIER
- */
- if (nextScale < initialScale) {
- nextScale =
- nextScale + (initialScale - nextScale) * OUT_BOUND_MULTIPLIER
- }
-
- /**
- * In case image is scaling down -> move it in direction of initial position
- */
- if (currentScale > initialScale && currentScale > nextScale) {
- const k = (currentScale - initialScale) / (currentScale - nextScale)
-
- const nextTranslateX =
- nextScale < initialScale
- ? initialTranslate.x
- : currentTranslate.x -
- (currentTranslate.x - initialTranslate.x) / k
-
- const nextTranslateY =
- nextScale < initialScale
- ? initialTranslate.y
- : currentTranslate.y -
- (currentTranslate.y - initialTranslate.y) / k
-
- translateValue.x.setValue(nextTranslateX)
- translateValue.y.setValue(nextTranslateY)
-
- tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
- }
-
- scaleValue.setValue(nextScale)
- tmpScale = nextScale
- }
-
- if (isTapGesture && currentScale > initialScale) {
- const { x, y } = currentTranslate
- const { dx, dy } = gestureState
- const [topBound, leftBound, bottomBound, rightBound] =
- getBounds(currentScale)
-
- let nextTranslateX = x + dx
- let nextTranslateY = y + dy
-
- if (nextTranslateX > leftBound) {
- nextTranslateX =
- nextTranslateX - (nextTranslateX - leftBound) * OUT_BOUND_MULTIPLIER
- }
-
- if (nextTranslateX < rightBound) {
- nextTranslateX =
- nextTranslateX -
- (nextTranslateX - rightBound) * OUT_BOUND_MULTIPLIER
- }
-
- if (nextTranslateY > topBound) {
- nextTranslateY =
- nextTranslateY - (nextTranslateY - topBound) * OUT_BOUND_MULTIPLIER
- }
-
- if (nextTranslateY < bottomBound) {
- nextTranslateY =
- nextTranslateY -
- (nextTranslateY - bottomBound) * OUT_BOUND_MULTIPLIER
- }
-
- if (fitsScreenByWidth()) {
- nextTranslateX = x
- }
-
- if (fitsScreenByHeight()) {
- nextTranslateY = y
- }
-
- translateValue.x.setValue(nextTranslateX)
- translateValue.y.setValue(nextTranslateY)
-
- tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
- }
- },
- onRelease: () => {
- cancelLongPressHandle()
-
- if (isDoubleTapPerformed) {
- isDoubleTapPerformed = false
- }
-
- if (tmpScale > 0) {
- if (tmpScale < initialScale || tmpScale > SCALE_MAX) {
- tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX
- Animated.timing(scaleValue, {
- toValue: tmpScale,
- duration: 100,
- useNativeDriver: true
- }).start()
- }
-
- currentScale = tmpScale
- tmpScale = 0
- }
-
- if (tmpTranslate) {
- const { x, y } = tmpTranslate
- const [topBound, leftBound, bottomBound, rightBound] =
- getBounds(currentScale)
-
- let nextTranslateX = x
- let nextTranslateY = y
-
- if (!fitsScreenByWidth()) {
- if (nextTranslateX > leftBound) {
- nextTranslateX = leftBound
- } else if (nextTranslateX < rightBound) {
- nextTranslateX = rightBound
- }
- }
-
- if (!fitsScreenByHeight()) {
- if (nextTranslateY > topBound) {
- nextTranslateY = topBound
- } else if (nextTranslateY < bottomBound) {
- nextTranslateY = bottomBound
- }
- }
-
- Animated.parallel([
- Animated.timing(translateValue.x, {
- toValue: nextTranslateX,
- duration: 100,
- useNativeDriver: true
- }),
- Animated.timing(translateValue.y, {
- toValue: nextTranslateY,
- duration: 100,
- useNativeDriver: true
- })
- ]).start()
-
- currentTranslate = { x: nextTranslateX, y: nextTranslateY }
- tmpTranslate = null
- }
- }
- }
-
- const panResponder = useMemo(() => createPanResponder(handlers), [handlers])
-
- return [panResponder.panHandlers, scaleValue, translateValue]
-}
-
-export default usePanResponder
diff --git a/src/screens/ImageViewer/hooks/useRequestClose.ts b/src/screens/ImageViewer/hooks/useRequestClose.ts
deleted file mode 100644
index 9bdf114d..00000000
--- a/src/screens/ImageViewer/hooks/useRequestClose.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * 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 { useState } from 'react'
-
-const useRequestClose = (onRequestClose: () => void) => {
- const [opacity, setOpacity] = useState(1)
-
- return [
- opacity,
- () => {
- setOpacity(0)
- onRequestClose()
- }
- ] as const
-}
-
-export default useRequestClose
diff --git a/src/screens/ImageViewer/utils.ts b/src/screens/ImageViewer/utils.ts
deleted file mode 100644
index c75a8ae8..00000000
--- a/src/screens/ImageViewer/utils.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * 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 {
- Animated,
- GestureResponderEvent,
- PanResponder,
- PanResponderGestureState,
- PanResponderInstance,
- NativeTouchEvent
-} from 'react-native'
-import { Dimensions, Position } from './@types'
-
-export const getImageTransform = (
- image: Dimensions | null,
- screen: Dimensions
-) => {
- if (!image?.width || !image?.height) {
- return [] as const
- }
-
- const wScale = screen.width / image.width
- const hScale = screen.height / image.height
- const scale = Math.min(wScale, hScale)
- const { x, y } = getImageTranslate(image, screen)
-
- return [{ x, y }, scale] as const
-}
-
-export const getImageStyles = (
- image: Dimensions | null,
- translate: Animated.ValueXY,
- scale?: Animated.Value
-) => {
- if (!image?.width || !image?.height) {
- return { width: 0, height: 0 }
- }
-
- const transform = translate.getTranslateTransform()
-
- if (scale) {
- // @ts-ignore
- transform.push({ scale }, { perspective: new Animated.Value(1000) })
- }
-
- return {
- width: image.width,
- height: image.height,
- transform
- }
-}
-
-export const getImageTranslate = (
- image: Dimensions,
- screen: Dimensions
-): Position => {
- const getTranslateForAxis = (axis: 'x' | 'y'): number => {
- const imageSize = axis === 'x' ? image.width : image.height
- const screenSize = axis === 'x' ? screen.width : screen.height
-
- return (screenSize - imageSize) / 2
- }
-
- return {
- x: getTranslateForAxis('x'),
- y: getTranslateForAxis('y')
- }
-}
-
-export const getImageDimensionsByTranslate = (
- translate: Position,
- screen: Dimensions
-): Dimensions => ({
- width: screen.width - translate.x * 2,
- height: screen.height - translate.y * 2
-})
-
-export const getImageTranslateForScale = (
- currentTranslate: Position,
- targetScale: number,
- screen: Dimensions
-): Position => {
- const { width, height } = getImageDimensionsByTranslate(
- currentTranslate,
- screen
- )
-
- const targetImageDimensions = {
- width: width * targetScale,
- height: height * targetScale
- }
-
- return getImageTranslate(targetImageDimensions, screen)
-}
-
-type HandlerType = (
- event: GestureResponderEvent,
- state: PanResponderGestureState
-) => void
-
-type PanResponderProps = {
- onGrant: HandlerType
- onStart?: HandlerType
- onMove: HandlerType
- onRelease?: HandlerType
- onTerminate?: HandlerType
-}
-
-export const createPanResponder = ({
- onGrant,
- onStart,
- onMove,
- onRelease,
- onTerminate
-}: PanResponderProps): PanResponderInstance =>
- PanResponder.create({
- onStartShouldSetPanResponder: () => true,
- onStartShouldSetPanResponderCapture: () => true,
- onMoveShouldSetPanResponder: () => true,
- onMoveShouldSetPanResponderCapture: () => true,
- onPanResponderGrant: onGrant,
- onPanResponderStart: onStart,
- onPanResponderMove: onMove,
- onPanResponderRelease: onRelease,
- onPanResponderTerminate: onTerminate,
- onPanResponderTerminationRequest: () => false,
- onShouldBlockNativeResponder: () => false
- })
-
-export const getDistanceBetweenTouches = (
- touches: NativeTouchEvent[]
-): number => {
- const [a, b] = touches
-
- if (a == null || b == null) {
- return 0
- }
-
- return Math.sqrt(
- Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2)
- )
-}
diff --git a/src/screens/ImagesViewer.tsx b/src/screens/ImagesViewer.tsx
index 555c5353..757ae8d0 100644
--- a/src/screens/ImagesViewer.tsx
+++ b/src/screens/ImagesViewer.tsx
@@ -1,4 +1,5 @@
import analytics from '@components/analytics'
+import GracefullyImage from '@components/GracefullyImage'
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { Message } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet'
@@ -6,15 +7,24 @@ import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { Platform, Share, StatusBar, View } from 'react-native'
-import FlashMessage from 'react-native-flash-message'
import {
- SafeAreaProvider,
- useSafeAreaInsets
-} from 'react-native-safe-area-context'
-import ImageViewer from './ImageViewer/Root'
+ Dimensions,
+ FlatList,
+ PixelRatio,
+ Platform,
+ Share,
+ StatusBar,
+ View,
+ ViewToken
+} from 'react-native'
+import FlashMessage from 'react-native-flash-message'
+import { LongPressGestureHandler } from 'react-native-gesture-handler'
+import { Zoom, createZoomListComponent } from 'react-native-reanimated-zoom'
+import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
import saveImage from './ImageViewer/save'
+const ZoomFlatList = createZoomListComponent(FlatList)
+
const ScreenImagesViewer = ({
route: {
params: { imageUrls, id }
@@ -26,6 +36,9 @@ const ScreenImagesViewer = ({
return null
}
+ const SCREEN_WIDTH = Dimensions.get('screen').width
+ const SCREEN_HEIGHT = Dimensions.get('screen').height
+
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
@@ -34,6 +47,7 @@ const ScreenImagesViewer = ({
const initialIndex = imageUrls.findIndex(image => image.id === id)
const [currentIndex, setCurrentIndex] = useState(initialIndex)
+ const listRef = useRef(null)
const messageRef = useRef(null)
const { showActionSheetWithOptions } = useActionSheet()
@@ -71,15 +85,84 @@ const ScreenImagesViewer = ({
)
}, [currentIndex])
+ const renderItem = React.useCallback(
+ ({
+ item
+ }: {
+ item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
+ }) => {
+ const screenRatio = SCREEN_WIDTH / SCREEN_HEIGHT
+ const imageRatio = item.width && item.height ? item.width / item.height : 1
+
+ const maxWidthScale = item.width ? (item.width / SCREEN_WIDTH / PixelRatio.get()) * 4 : 0
+ const maxHeightScale = item.height ? (item.height / SCREEN_WIDTH / PixelRatio.get()) * 4 : 0
+ const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
+
+ return (
+ 8 ? 8 : max}
+ children={
+
+ imageRatio ? (item.height || 100) / screenRatio : SCREEN_WIDTH,
+ height:
+ screenRatio > imageRatio ? SCREEN_HEIGHT : (item.width || 100) * screenRatio
+ }}
+ />
+
+ }
+ />
+ )
+ },
+ []
+ )
+
+ const onViewableItemsChanged = useCallback(
+ ({ viewableItems }: { viewableItems: ViewToken[] }) => {
+ setCurrentIndex(viewableItems[0].index || 0)
+ },
+ []
+ )
+
return (
-
+
- setCurrentIndex(index)}
- onRequestClose={() => navigation.goBack()}
- onLongPress={() => {
+
+ navigation.goBack()} />
+
+
+
+ {
analytics('imageviewer_more_press')
showActionSheetWithOptions(
{
@@ -118,37 +201,26 @@ const ScreenImagesViewer = ({
}
)
}}
- HeaderComponent={() => (
-
- navigation.goBack()}
- />
-
-
-
- )}
- />
+ >
+ item.id}
+ renderItem={renderItem}
+ onViewableItemsChanged={onViewableItemsChanged}
+ viewabilityConfig={{
+ itemVisiblePercentThreshold: 50
+ }}
+ initialScrollIndex={initialIndex}
+ getItemLayout={(_, index) => ({
+ length: SCREEN_WIDTH,
+ offset: SCREEN_WIDTH * index,
+ index
+ })}
+ />
+
)
diff --git a/yarn.lock b/yarn.lock
index 5b0f2e48..4f7b68c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7081,6 +7081,11 @@ react-native-pager-view@^5.4.25:
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-5.4.25.tgz#cd639d5387a7f3d5581b55a33c5faa1cbc200f97"
integrity sha512-3drrYwaLat2fYszymZe3nHMPASJ4aJMaxiejfA1V5SK3OygYmdtmV2u5prX7TnjueJzGSyyaCYEr2JlrRt4YPg==
+react-native-reanimated-zoom@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated-zoom/-/react-native-reanimated-zoom-0.3.0.tgz#181825c1854853a4db33a86e521057e2e757d290"
+ integrity sha512-kvYVbLayX8Tj52oDmKE78gnEmZD5KsCHxkTSrMfahq9KyqU6aHWistfocFtzBBT+I0puzcHpivzy3dxYL1SL5Q==
+
react-native-reanimated@^2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.9.1.tgz#d9a932e312c13c05b4f919e43ebbf76d996e0bc1"