about summary refs log tree commit diff
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-10-05 23:28:56 +0100
committerGitHub <noreply@github.com>2023-10-05 15:28:56 -0700
commit260b03a05c22232373cbf8cb0d7dda41a3302343 (patch)
treef7ce8b72c80fbdc723245dc34d1db56288b7b176
parenteb7306b16512e317f477c7a28e1e3b0ce5c65ff8 (diff)
downloadvoidsky-260b03a05c22232373cbf8cb0d7dda41a3302343.tar.zst
Remove unused lightbox options (#1616)
* Inline lightbox helpers

* Delete unused useImagePrefetch

* Delete unused long press gesture

* Always enable double tap

* Always enable swipe to close

* Remove unused onImageIndexChange

* Inline custom Hooks into ImageViewing

* Declare LightboxFooter outside Lightbox

* Add more TODO comments

* Inline useDoubleTapToZoom

* Remove dead utils, move utils used only once
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx29
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx175
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx4
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useAnimatedComponents.ts47
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts150
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts22
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useImageIndexChange.ts32
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useImagePrefetch.ts25
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts60
-rw-r--r--src/view/com/lightbox/ImageViewing/hooks/useRequestClose.ts24
-rw-r--r--src/view/com/lightbox/ImageViewing/index.tsx98
-rw-r--r--src/view/com/lightbox/ImageViewing/utils.ts75
-rw-r--r--src/view/com/lightbox/Lightbox.tsx170
13 files changed, 335 insertions, 576 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
index f5e858209..927657baf 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -36,23 +36,11 @@ type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
   onZoom: (isZoomed: boolean) => void
-  onLongPress: (image: ImageSource) => void
-  delayLongPress: number
-  swipeToCloseEnabled?: boolean
-  doubleTapToZoomEnabled?: boolean
 }
 
 const AnimatedImage = Animated.createAnimatedComponent(Image)
 
-const ImageItem = ({
-  imageSrc,
-  onZoom,
-  onRequestClose,
-  onLongPress,
-  delayLongPress,
-  swipeToCloseEnabled = true,
-  doubleTapToZoomEnabled = true,
-}: Props) => {
+const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => {
   const imageContainer = useRef<ScrollView & NativeMethodsMixin>(null)
   const imageDimensions = useImageDimensions(imageSrc)
   const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
@@ -72,17 +60,10 @@ const ImageItem = ({
     [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(
@@ -126,11 +107,9 @@ const ImageItem = ({
       showsHorizontalScrollIndicator={false}
       showsVerticalScrollIndicator={false}
       contentContainerStyle={styles.imageScrollContainer}
-      scrollEnabled={swipeToCloseEnabled}
-      {...(swipeToCloseEnabled && {
-        onScroll,
-        onScrollEndDrag,
-      })}>
+      scrollEnabled={true}
+      onScroll={onScroll}
+      onScrollEndDrag={onScrollEndDrag}>
       <AnimatedImage
         {...panHandlers}
         source={imageSrc}
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index 03bf45af1..f379df22f 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -16,57 +16,45 @@ import {
   View,
   NativeScrollEvent,
   NativeSyntheticEvent,
+  NativeTouchEvent,
   TouchableWithoutFeedback,
 } from 'react-native'
 import {Image} from 'expo-image'
 
-import useDoubleTapToZoom from '../../hooks/useDoubleTapToZoom'
 import useImageDimensions from '../../hooks/useImageDimensions'
 
 import {getImageStyles, getImageTransform} from '../../utils'
 import {ImageSource} from '../../@types'
 import {ImageLoading} from './ImageLoading'
 
+const DOUBLE_TAP_DELAY = 300
 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
+const MIN_ZOOM = 2
 const MAX_SCALE = 2
 
 type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
   onZoom: (scaled: boolean) => void
-  onLongPress: (image: ImageSource) => void
-  delayLongPress: number
-  swipeToCloseEnabled?: boolean
-  doubleTapToZoomEnabled?: boolean
 }
 
 const AnimatedImage = Animated.createAnimatedComponent(Image)
 
-const ImageItem = ({
-  imageSrc,
-  onZoom,
-  onRequestClose,
-  onLongPress,
-  delayLongPress,
-  swipeToCloseEnabled = true,
-  doubleTapToZoomEnabled = true,
-}: Props) => {
+let lastTapTS: number | null = null
+
+const ImageItem = ({imageSrc, onZoom, onRequestClose}: 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,
-    imageDimensions,
-  )
-
   const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
+
+  // TODO: It's not valid to reinitialize Animated values during render.
+  // This is a bug.
   const scrollValueY = new Animated.Value(0)
   const scaleValue = new Animated.Value(scale || 1)
   const translateValue = new Animated.ValueXY(translate)
@@ -91,15 +79,11 @@ const ImageItem = ({
       onZoom(currentScaled)
       setScaled(currentScaled)
 
-      if (
-        !currentScaled &&
-        swipeToCloseEnabled &&
-        Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY
-      ) {
+      if (!currentScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
         onRequestClose()
       }
     },
-    [onRequestClose, onZoom, swipeToCloseEnabled],
+    [onRequestClose, onZoom],
   )
 
   const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -112,9 +96,40 @@ const ImageItem = ({
     scrollValueY.setValue(offsetY)
   }
 
-  const onLongPressHandler = useCallback(() => {
-    onLongPress(imageSrc)
-  }, [imageSrc, onLongPress])
+  const handleDoubleTap = useCallback(
+    (event: NativeSyntheticEvent<NativeTouchEvent>) => {
+      const nowTS = new Date().getTime()
+      const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
+
+      if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) {
+        let nextZoomRect = {
+          x: 0,
+          y: 0,
+          width: SCREEN.width,
+          height: SCREEN.height,
+        }
+
+        const willZoom = !scaled
+        if (willZoom) {
+          const {pageX, pageY} = event.nativeEvent
+          nextZoomRect = getZoomRectAfterDoubleTap(
+            imageDimensions,
+            pageX,
+            pageY,
+          )
+        }
+
+        // @ts-ignore
+        scrollResponderRef?.scrollResponderZoomTo({
+          ...nextZoomRect, // This rect is in screen coordinates
+          animated: true,
+        })
+      } else {
+        lastTapTS = nowTS
+      }
+    },
+    [imageDimensions, scaled],
+  )
 
   return (
     <View>
@@ -126,17 +141,13 @@ const ImageItem = ({
         showsVerticalScrollIndicator={false}
         maximumZoomScale={maxScrollViewZoom}
         contentContainerStyle={styles.imageScrollContainer}
-        scrollEnabled={swipeToCloseEnabled}
+        scrollEnabled={true}
+        onScroll={onScroll}
         onScrollEndDrag={onScrollEndDrag}
-        scrollEventThrottle={1}
-        {...(swipeToCloseEnabled && {
-          onScroll,
-        })}>
+        scrollEventThrottle={1}>
         {(!loaded || !imageDimensions) && <ImageLoading />}
         <TouchableWithoutFeedback
-          onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
-          onLongPress={onLongPressHandler}
-          delayLongPress={delayLongPress}
+          onPress={handleDoubleTap}
           accessibilityRole="image"
           accessibilityLabel={imageSrc.alt}
           accessibilityHint="">
@@ -161,4 +172,92 @@ const styles = StyleSheet.create({
   },
 })
 
+const getZoomRectAfterDoubleTap = (
+  imageDimensions: {width: number; height: number} | null,
+  touchX: number,
+  touchY: number,
+): {
+  x: number
+  y: number
+  width: number
+  height: number
+} => {
+  if (!imageDimensions) {
+    return {
+      x: 0,
+      y: 0,
+      width: SCREEN.width,
+      height: SCREEN.height,
+    }
+  }
+
+  // First, let's figure out how much we want to zoom in.
+  // We want to try to zoom in at least close enough to get rid of black bars.
+  const imageAspect = imageDimensions.width / imageDimensions.height
+  const screenAspect = SCREEN.width / SCREEN.height
+  const zoom = Math.max(
+    imageAspect / screenAspect,
+    screenAspect / imageAspect,
+    MIN_ZOOM,
+  )
+  // Unlike in the Android version, we don't constrain the *max* zoom level here.
+  // Instead, this is done in the ScrollView props so that it constraints pinch too.
+
+  // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates.
+  // We already know the zoom level, so this gives us the rectangle size.
+  let rectWidth = SCREEN.width / zoom
+  let rectHeight = SCREEN.height / zoom
+
+  // Before we settle on the zoomed rect, figure out the safe area it has to be inside.
+  // We don't want to introduce new black bars or make existing black bars unbalanced.
+  let minX = 0
+  let minY = 0
+  let maxX = SCREEN.width - rectWidth
+  let maxY = SCREEN.height - rectHeight
+  if (imageAspect >= screenAspect) {
+    // The image has horizontal black bars. Exclude them from the safe area.
+    const renderedHeight = SCREEN.width / imageAspect
+    const horizontalBarHeight = (SCREEN.height - renderedHeight) / 2
+    minY += horizontalBarHeight
+    maxY -= horizontalBarHeight
+  } else {
+    // The image has vertical black bars. Exclude them from the safe area.
+    const renderedWidth = SCREEN.height * imageAspect
+    const verticalBarWidth = (SCREEN.width - renderedWidth) / 2
+    minX += verticalBarWidth
+    maxX -= verticalBarWidth
+  }
+
+  // Finally, we can position the rect according to its size and the safe area.
+  let rectX
+  if (maxX >= minX) {
+    // Content fills the screen horizontally so we have horizontal wiggle room.
+    // Try to keep the tapped point under the finger after zoom.
+    rectX = touchX - touchX / zoom
+    rectX = Math.min(rectX, maxX)
+    rectX = Math.max(rectX, minX)
+  } else {
+    // Keep the rect centered on the screen so that black bars are balanced.
+    rectX = SCREEN.width / 2 - rectWidth / 2
+  }
+  let rectY
+  if (maxY >= minY) {
+    // Content fills the screen vertically so we have vertical wiggle room.
+    // Try to keep the tapped point under the finger after zoom.
+    rectY = touchY - touchY / zoom
+    rectY = Math.min(rectY, maxY)
+    rectY = Math.max(rectY, minY)
+  } else {
+    // Keep the rect centered on the screen so that black bars are balanced.
+    rectY = SCREEN.height / 2 - rectHeight / 2
+  }
+
+  return {
+    x: rectX,
+    y: rectY,
+    height: rectHeight,
+    width: rectWidth,
+  }
+}
+
 export default React.memo(ImageItem)
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
index fd377dde2..82ee86d7d 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
@@ -8,10 +8,6 @@ type Props = {
   imageSrc: ImageSource
   onRequestClose: () => void
   onZoom: (scaled: boolean) => void
-  onLongPress: (image: ImageSource) => void
-  delayLongPress: number
-  swipeToCloseEnabled?: boolean
-  doubleTapToZoomEnabled?: boolean
 }
 
 const ImageItem = (_props: Props) => {
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useAnimatedComponents.ts b/src/view/com/lightbox/ImageViewing/hooks/useAnimatedComponents.ts
deleted file mode 100644
index c21cd7f2c..000000000
--- a/src/view/com/lightbox/ImageViewing/hooks/useAnimatedComponents.ts
+++ /dev/null
@@ -1,47 +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 footerTranslate = new Animated.ValueXY(INITIAL_POSITION)
-
-  const toggleVisible = (isVisible: boolean) => {
-    if (isVisible) {
-      Animated.parallel([
-        Animated.timing(headerTranslate.y, {...ANIMATION_CONFIG, toValue: 0}),
-        Animated.timing(footerTranslate.y, {...ANIMATION_CONFIG, toValue: 0}),
-      ]).start()
-    } else {
-      Animated.parallel([
-        Animated.timing(headerTranslate.y, {
-          ...ANIMATION_CONFIG,
-          toValue: -300,
-        }),
-        Animated.timing(footerTranslate.y, {
-          ...ANIMATION_CONFIG,
-          toValue: 300,
-        }),
-      ]).start()
-    }
-  }
-
-  const headerTransform = headerTranslate.getTranslateTransform()
-  const footerTransform = footerTranslate.getTranslateTransform()
-
-  return [headerTransform, footerTransform, toggleVisible] as const
-}
-
-export default useAnimatedComponents
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts b/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts
deleted file mode 100644
index ea81d9f1c..000000000
--- a/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts
+++ /dev/null
@@ -1,150 +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, NativeTouchEvent, NativeSyntheticEvent} from 'react-native'
-
-import {Dimensions} from '../@types'
-
-const DOUBLE_TAP_DELAY = 300
-const MIN_ZOOM = 2
-
-let lastTapTS: number | null = null
-
-/**
- * This is iOS only.
- * Same functionality for Android implemented inside usePanResponder hook.
- */
-function useDoubleTapToZoom(
-  scrollViewRef: React.RefObject<ScrollView>,
-  scaled: boolean,
-  screen: Dimensions,
-  imageDimensions: Dimensions | null,
-) {
-  const handleDoubleTap = useCallback(
-    (event: NativeSyntheticEvent<NativeTouchEvent>) => {
-      const nowTS = new Date().getTime()
-      const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
-
-      const getZoomRectAfterDoubleTap = (
-        touchX: number,
-        touchY: number,
-      ): {
-        x: number
-        y: number
-        width: number
-        height: number
-      } => {
-        if (!imageDimensions) {
-          return {
-            x: 0,
-            y: 0,
-            width: screen.width,
-            height: screen.height,
-          }
-        }
-
-        // First, let's figure out how much we want to zoom in.
-        // We want to try to zoom in at least close enough to get rid of black bars.
-        const imageAspect = imageDimensions.width / imageDimensions.height
-        const screenAspect = screen.width / screen.height
-        const zoom = Math.max(
-          imageAspect / screenAspect,
-          screenAspect / imageAspect,
-          MIN_ZOOM,
-        )
-        // Unlike in the Android version, we don't constrain the *max* zoom level here.
-        // Instead, this is done in the ScrollView props so that it constraints pinch too.
-
-        // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates.
-        // We already know the zoom level, so this gives us the rectangle size.
-        let rectWidth = screen.width / zoom
-        let rectHeight = screen.height / zoom
-
-        // Before we settle on the zoomed rect, figure out the safe area it has to be inside.
-        // We don't want to introduce new black bars or make existing black bars unbalanced.
-        let minX = 0
-        let minY = 0
-        let maxX = screen.width - rectWidth
-        let maxY = screen.height - rectHeight
-        if (imageAspect >= screenAspect) {
-          // The image has horizontal black bars. Exclude them from the safe area.
-          const renderedHeight = screen.width / imageAspect
-          const horizontalBarHeight = (screen.height - renderedHeight) / 2
-          minY += horizontalBarHeight
-          maxY -= horizontalBarHeight
-        } else {
-          // The image has vertical black bars. Exclude them from the safe area.
-          const renderedWidth = screen.height * imageAspect
-          const verticalBarWidth = (screen.width - renderedWidth) / 2
-          minX += verticalBarWidth
-          maxX -= verticalBarWidth
-        }
-
-        // Finally, we can position the rect according to its size and the safe area.
-        let rectX
-        if (maxX >= minX) {
-          // Content fills the screen horizontally so we have horizontal wiggle room.
-          // Try to keep the tapped point under the finger after zoom.
-          rectX = touchX - touchX / zoom
-          rectX = Math.min(rectX, maxX)
-          rectX = Math.max(rectX, minX)
-        } else {
-          // Keep the rect centered on the screen so that black bars are balanced.
-          rectX = screen.width / 2 - rectWidth / 2
-        }
-        let rectY
-        if (maxY >= minY) {
-          // Content fills the screen vertically so we have vertical wiggle room.
-          // Try to keep the tapped point under the finger after zoom.
-          rectY = touchY - touchY / zoom
-          rectY = Math.min(rectY, maxY)
-          rectY = Math.max(rectY, minY)
-        } else {
-          // Keep the rect centered on the screen so that black bars are balanced.
-          rectY = screen.height / 2 - rectHeight / 2
-        }
-
-        return {
-          x: rectX,
-          y: rectY,
-          height: rectHeight,
-          width: rectWidth,
-        }
-      }
-
-      if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) {
-        let nextZoomRect = {
-          x: 0,
-          y: 0,
-          width: screen.width,
-          height: screen.height,
-        }
-
-        const willZoom = !scaled
-        if (willZoom) {
-          const {pageX, pageY} = event.nativeEvent
-          nextZoomRect = getZoomRectAfterDoubleTap(pageX, pageY)
-        }
-
-        // @ts-ignore
-        scrollResponderRef?.scrollResponderZoomTo({
-          ...nextZoomRect, // This rect is in screen coordinates
-          animated: true,
-        })
-      } else {
-        lastTapTS = nowTS
-      }
-    },
-    [imageDimensions, scaled, screen.height, screen.width, scrollViewRef],
-  )
-
-  return handleDoubleTap
-}
-
-export default useDoubleTapToZoom
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts b/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts
index a5b0b6bd4..7f0851af3 100644
--- a/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts
+++ b/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts
@@ -8,11 +8,29 @@
 
 import {useEffect, useState} from 'react'
 import {Image, ImageURISource} from 'react-native'
-
-import {createCache} from '../utils'
 import {Dimensions, ImageSource} from '../@types'
 
 const CACHE_SIZE = 50
+
+type CacheStorageItem = {key: string; value: any}
+
+const createCache = (cacheSize: number) => ({
+  _storage: [] as CacheStorageItem[],
+  get(key: string): any {
+    const {value} =
+      this._storage.find(({key: storageKey}) => storageKey === key) || {}
+
+    return value
+  },
+  set(key: string, value: any) {
+    if (this._storage.length >= cacheSize) {
+      this._storage.shift()
+    }
+
+    this._storage.push({key, value})
+  },
+})
+
 const imageDimensionsCache = createCache(CACHE_SIZE)
 
 const useImageDimensions = (image: ImageSource): Dimensions | null => {
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useImageIndexChange.ts b/src/view/com/lightbox/ImageViewing/hooks/useImageIndexChange.ts
deleted file mode 100644
index 16430f3aa..000000000
--- a/src/view/com/lightbox/ImageViewing/hooks/useImageIndexChange.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 {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<NativeScrollEvent>) => {
-    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/view/com/lightbox/ImageViewing/hooks/useImagePrefetch.ts b/src/view/com/lightbox/ImageViewing/hooks/useImagePrefetch.ts
deleted file mode 100644
index 3969945bb..000000000
--- a/src/view/com/lightbox/ImageViewing/hooks/useImagePrefetch.ts
+++ /dev/null
@@ -1,25 +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 {useEffect} from 'react'
-import {Image} from 'react-native'
-import {ImageSource} from '../@types'
-
-const useImagePrefetch = (images: ImageSource[]) => {
-  useEffect(() => {
-    images.forEach(image => {
-      //@ts-ignore
-      if (image.uri) {
-        //@ts-ignore
-        return Image.prefetch(image.uri)
-      }
-    })
-  }, [images])
-}
-
-export default useImagePrefetch
diff --git a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts
index 7908504ea..85454e37e 100644
--- a/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts
+++ b/src/view/com/lightbox/ImageViewing/hooks/usePanResponder.ts
@@ -18,16 +18,11 @@ import {
 } from 'react-native'
 
 import {Position} from '../@types'
-import {
-  getDistanceBetweenTouches,
-  getImageTranslate,
-  getImageDimensionsByTranslate,
-} from '../utils'
+import {getImageTranslate} 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 ANDROID_BAR_HEIGHT = 24
 
 const MIN_ZOOM = 2
@@ -39,18 +34,12 @@ type Props = {
   initialScale: number
   initialTranslate: Position
   onZoom: (isZoomed: boolean) => void
-  doubleTapToZoomEnabled: boolean
-  onLongPress: () => void
-  delayLongPress: number
 }
 
 const usePanResponder = ({
   initialScale,
   initialTranslate,
   onZoom,
-  doubleTapToZoomEnabled,
-  onLongPress,
-  delayLongPress,
 }: Props): Readonly<
   [GestureResponderHandlers, Animated.Value, Animated.ValueXY]
 > => {
@@ -62,9 +51,9 @@ const usePanResponder = ({
   let tmpTranslate: Position | null = null
   let isDoubleTapPerformed = false
   let lastTapTS: number | null = null
-  let longPressHandlerRef: NodeJS.Timeout | null = null
 
-  const meaningfulShift = MIN_DIMENSION * 0.01
+  // TODO: It's not valid to reinitialize Animated values during render.
+  // This is a bug.
   const scaleValue = new Animated.Value(initialScale)
   const translateValue = new Animated.ValueXY(initialTranslate)
 
@@ -155,10 +144,6 @@ const usePanResponder = ({
     return () => scaleValue.removeAllListeners()
   })
 
-  const cancelLongPressHandle = () => {
-    longPressHandlerRef && clearTimeout(longPressHandlerRef)
-  }
-
   const panResponder = PanResponder.create({
     onStartShouldSetPanResponder: () => true,
     onStartShouldSetPanResponderCapture: () => true,
@@ -173,8 +158,6 @@ const usePanResponder = ({
       if (gestureState.numberActiveTouches > 1) {
         return
       }
-
-      longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
     },
     onPanResponderStart: (
       event: GestureResponderEvent,
@@ -194,7 +177,7 @@ const usePanResponder = ({
         lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY,
       )
 
-      if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
+      if (isDoubleTapPerformed) {
         let nextScale = initialScale
         let nextTranslate = initialTranslate
 
@@ -241,15 +224,8 @@ const usePanResponder = ({
       event: GestureResponderEvent,
       gestureState: PanResponderGestureState,
     ) => {
-      const {dx, dy} = gestureState
-
-      if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
-        cancelLongPressHandle()
-      }
-
       // Don't need to handle move because double tap in progress (was handled in onStart)
-      if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
-        cancelLongPressHandle()
+      if (isDoubleTapPerformed) {
         return
       }
 
@@ -267,8 +243,6 @@ const usePanResponder = ({
         numberInitialTouches === 2 && gestureState.numberActiveTouches === 2
 
       if (isPinchGesture) {
-        cancelLongPressHandle()
-
         const initialDistance = getDistanceBetweenTouches(initialTouches)
         const currentDistance = getDistanceBetweenTouches(
           event.nativeEvent.touches,
@@ -315,7 +289,7 @@ const usePanResponder = ({
 
       if (isTapGesture && currentScale > initialScale) {
         const {x, y} = currentTranslate
-        // eslint-disable-next-line @typescript-eslint/no-shadow
+
         const {dx, dy} = gestureState
         const [topBound, leftBound, bottomBound, rightBound] =
           getBounds(currentScale)
@@ -360,8 +334,6 @@ const usePanResponder = ({
       }
     },
     onPanResponderRelease: () => {
-      cancelLongPressHandle()
-
       if (isDoubleTapPerformed) {
         isDoubleTapPerformed = false
       }
@@ -428,4 +400,24 @@ const usePanResponder = ({
   return [panResponder.panHandlers, scaleValue, translateValue]
 }
 
+const getImageDimensionsByTranslate = (
+  translate: Position,
+  screen: {width: number; height: number},
+): {width: number; height: number} => ({
+  width: screen.width - translate.x * 2,
+  height: screen.height - translate.y * 2,
+})
+
+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),
+  )
+}
+
 export default usePanResponder
diff --git a/src/view/com/lightbox/ImageViewing/hooks/useRequestClose.ts b/src/view/com/lightbox/ImageViewing/hooks/useRequestClose.ts
deleted file mode 100644
index 4cd03fe71..000000000
--- a/src/view/com/lightbox/ImageViewing/hooks/useRequestClose.ts
+++ /dev/null
@@ -1,24 +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()
-      setTimeout(() => setOpacity(1), 0)
-    },
-  ] as const
-}
-
-export default useRequestClose
diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx
index 1a64fb3af..3b659e2db 100644
--- a/src/view/com/lightbox/ImageViewing/index.tsx
+++ b/src/view/com/lightbox/ImageViewing/index.tsx
@@ -12,12 +12,14 @@ import React, {
   ComponentType,
   useCallback,
   useRef,
-  useEffect,
   useMemo,
+  useState,
 } from 'react'
 import {
   Animated,
   Dimensions,
+  NativeSyntheticEvent,
+  NativeScrollEvent,
   StyleSheet,
   View,
   VirtualizedList,
@@ -29,9 +31,6 @@ import {ModalsContainer} from '../../modals/Modal'
 import ImageItem from './components/ImageItem/ImageItem'
 import ImageDefaultHeader from './components/ImageDefaultHeader'
 
-import useAnimatedComponents from './hooks/useAnimatedComponents'
-import useImageIndexChange from './hooks/useImageIndexChange'
-import useRequestClose from './hooks/useRequestClose'
 import {ImageSource} from './@types'
 import {Edge, SafeAreaView} from 'react-native-safe-area-context'
 
@@ -41,22 +40,21 @@ type Props = {
   imageIndex: number
   visible: boolean
   onRequestClose: () => void
-  onLongPress?: (image: ImageSource) => void
-  onImageIndexChange?: (imageIndex: number) => void
   presentationStyle?: ModalProps['presentationStyle']
   animationType?: ModalProps['animationType']
   backgroundColor?: string
-  swipeToCloseEnabled?: boolean
-  doubleTapToZoomEnabled?: boolean
-  delayLongPress?: number
   HeaderComponent?: ComponentType<{imageIndex: number}>
   FooterComponent?: ComponentType<{imageIndex: number}>
 }
 
 const DEFAULT_BG_COLOR = '#000'
-const DEFAULT_DELAY_LONG_PRESS = 800
 const SCREEN = Dimensions.get('screen')
 const SCREEN_WIDTH = SCREEN.width
+const INITIAL_POSITION = {x: 0, y: 0}
+const ANIMATION_CONFIG = {
+  duration: 200,
+  useNativeDriver: true,
+}
 
 function ImageViewing({
   images,
@@ -64,35 +62,63 @@ function ImageViewing({
   imageIndex,
   visible,
   onRequestClose,
-  onLongPress = () => {},
-  onImageIndexChange,
   backgroundColor = DEFAULT_BG_COLOR,
-  swipeToCloseEnabled,
-  doubleTapToZoomEnabled,
-  delayLongPress = DEFAULT_DELAY_LONG_PRESS,
   HeaderComponent,
   FooterComponent,
 }: Props) {
   const imageList = useRef<VirtualizedList<ImageSource>>(null)
-  const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose)
-  const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN)
-  const [headerTransform, footerTransform, toggleBarsVisible] =
-    useAnimatedComponents()
-
-  useEffect(() => {
-    if (onImageIndexChange) {
-      onImageIndexChange(currentImageIndex)
+  const [opacity, setOpacity] = useState(1)
+  const [currentImageIndex, setImageIndex] = useState(imageIndex)
+
+  // TODO: It's not valid to reinitialize Animated values during render.
+  // This is a bug.
+  const headerTranslate = new Animated.ValueXY(INITIAL_POSITION)
+  const footerTranslate = new Animated.ValueXY(INITIAL_POSITION)
+
+  const toggleBarsVisible = (isVisible: boolean) => {
+    if (isVisible) {
+      Animated.parallel([
+        Animated.timing(headerTranslate.y, {...ANIMATION_CONFIG, toValue: 0}),
+        Animated.timing(footerTranslate.y, {...ANIMATION_CONFIG, toValue: 0}),
+      ]).start()
+    } else {
+      Animated.parallel([
+        Animated.timing(headerTranslate.y, {
+          ...ANIMATION_CONFIG,
+          toValue: -300,
+        }),
+        Animated.timing(footerTranslate.y, {
+          ...ANIMATION_CONFIG,
+          toValue: 300,
+        }),
+      ]).start()
     }
-  }, [currentImageIndex, onImageIndexChange])
-
-  const onZoom = useCallback(
-    (isScaled: boolean) => {
-      // @ts-ignore
-      imageList?.current?.setNativeProps({scrollEnabled: !isScaled})
-      toggleBarsVisible(!isScaled)
-    },
-    [toggleBarsVisible],
-  )
+  }
+
+  const onRequestCloseEnhanced = () => {
+    setOpacity(0)
+    onRequestClose()
+    setTimeout(() => setOpacity(1), 0)
+  }
+
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
+    const {
+      nativeEvent: {
+        contentOffset: {x: scrollX},
+      },
+    } = event
+
+    if (SCREEN.width) {
+      const nextIndex = Math.round(scrollX / SCREEN.width)
+      setImageIndex(nextIndex < 0 ? 0 : nextIndex)
+    }
+  }
+
+  const onZoom = (isScaled: boolean) => {
+    // @ts-ignore
+    imageList?.current?.setNativeProps({scrollEnabled: !isScaled})
+    toggleBarsVisible(!isScaled)
+  }
 
   const edges = useMemo(() => {
     if (Platform.OS === 'android') {
@@ -111,6 +137,8 @@ function ImageViewing({
     return null
   }
 
+  const headerTransform = headerTranslate.getTranslateTransform()
+  const footerTransform = footerTranslate.getTranslateTransform()
   return (
     <SafeAreaView
       style={styles.screen}
@@ -148,10 +176,6 @@ function ImageViewing({
               onZoom={onZoom}
               imageSrc={imageSrc}
               onRequestClose={onRequestCloseEnhanced}
-              onLongPress={onLongPress}
-              delayLongPress={delayLongPress}
-              swipeToCloseEnabled={swipeToCloseEnabled}
-              doubleTapToZoomEnabled={doubleTapToZoomEnabled}
             />
           )}
           onMomentumScrollEnd={onScroll}
diff --git a/src/view/com/lightbox/ImageViewing/utils.ts b/src/view/com/lightbox/ImageViewing/utils.ts
index d56eea4f4..03f28d61a 100644
--- a/src/view/com/lightbox/ImageViewing/utils.ts
+++ b/src/view/com/lightbox/ImageViewing/utils.ts
@@ -6,42 +6,9 @@
  *
  */
 
-import {Animated, NativeTouchEvent} from 'react-native'
+import {Animated} from 'react-native'
 import {Dimensions, Position} from './@types'
 
-type CacheStorageItem = {key: string; value: any}
-
-export const createCache = (cacheSize: number) => ({
-  _storage: [] as CacheStorageItem[],
-  get(key: string): any {
-    const {value} =
-      this._storage.find(({key: storageKey}) => storageKey === key) || {}
-
-    return value
-  },
-  set(key: string, value: any) {
-    if (this._storage.length >= cacheSize) {
-      this._storage.shift()
-    }
-
-    this._storage.push({key, value})
-  },
-})
-
-export const splitArrayIntoBatches = (arr: any[], batchSize: number): any[] =>
-  arr.reduce((result, item) => {
-    const batch = result.pop() || []
-
-    if (batch.length < batchSize) {
-      batch.push(item)
-      result.push(batch)
-    } else {
-      result.push(batch, [item])
-    }
-
-    return result
-  }, [])
-
 export const getImageTransform = (
   image: Dimensions | null,
   screen: Dimensions,
@@ -97,43 +64,3 @@ export const getImageTranslate = (
     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)
-}
-
-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/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 072bfebfa..ad66dce32 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -15,94 +15,10 @@ import * as MediaLibrary from 'expo-media-library'
 
 export const Lightbox = observer(function Lightbox() {
   const store = useStores()
-  const [isAltExpanded, setAltExpanded] = React.useState(false)
-  const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
-
   const onClose = React.useCallback(() => {
     store.shell.closeLightbox()
   }, [store])
 
-  const saveImageToAlbumWithToasts = React.useCallback(
-    async (uri: string) => {
-      if (!permissionResponse || permissionResponse.granted === false) {
-        Toast.show('Permission to access camera roll is required.')
-        if (permissionResponse?.canAskAgain) {
-          requestPermission()
-        } else {
-          Toast.show(
-            'Permission to access camera roll was denied. Please enable it in your system settings.',
-          )
-        }
-        return
-      }
-
-      try {
-        await saveImageToMediaLibrary({uri})
-        Toast.show('Saved to your camera roll.')
-      } catch (e: any) {
-        Toast.show(`Failed to save image: ${String(e)}`)
-      }
-    },
-    [permissionResponse, requestPermission],
-  )
-
-  const LightboxFooter = React.useCallback(
-    ({imageIndex}: {imageIndex: number}) => {
-      const lightbox = store.shell.activeLightbox
-      if (!lightbox) {
-        return null
-      }
-
-      let altText = ''
-      let uri = ''
-      if (lightbox.name === 'images') {
-        const opts = lightbox as models.ImagesLightbox
-        uri = opts.images[imageIndex].uri
-        altText = opts.images[imageIndex].alt || ''
-      } else if (lightbox.name === 'profile-image') {
-        const opts = lightbox as models.ProfileImageLightbox
-        uri = opts.profileView.avatar || ''
-      }
-
-      return (
-        <View style={[styles.footer]}>
-          {altText ? (
-            <Pressable
-              onPress={() => setAltExpanded(!isAltExpanded)}
-              accessibilityRole="button">
-              <Text
-                style={[s.gray3, styles.footerText]}
-                numberOfLines={isAltExpanded ? undefined : 3}>
-                {altText}
-              </Text>
-            </Pressable>
-          ) : null}
-          <View style={styles.footerBtns}>
-            <Button
-              type="primary-outline"
-              style={styles.footerBtn}
-              onPress={() => saveImageToAlbumWithToasts(uri)}>
-              <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} />
-              <Text type="xl" style={s.white}>
-                Save
-              </Text>
-            </Button>
-            <Button
-              type="primary-outline"
-              style={styles.footerBtn}
-              onPress={() => shareImageModal({uri})}>
-              <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} />
-              <Text type="xl" style={s.white}>
-                Share
-              </Text>
-            </Button>
-          </View>
-        </View>
-      )
-    },
-    [store.shell.activeLightbox, isAltExpanded, saveImageToAlbumWithToasts],
-  )
-
   if (!store.shell.activeLightbox) {
     return null
   } else if (store.shell.activeLightbox.name === 'profile-image') {
@@ -132,6 +48,92 @@ export const Lightbox = observer(function Lightbox() {
   }
 })
 
+const LightboxFooter = observer(function LightboxFooter({
+  imageIndex,
+}: {
+  imageIndex: number
+}) {
+  const store = useStores()
+  const [isAltExpanded, setAltExpanded] = React.useState(false)
+  const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
+
+  const saveImageToAlbumWithToasts = React.useCallback(
+    async (uri: string) => {
+      if (!permissionResponse || permissionResponse.granted === false) {
+        Toast.show('Permission to access camera roll is required.')
+        if (permissionResponse?.canAskAgain) {
+          requestPermission()
+        } else {
+          Toast.show(
+            'Permission to access camera roll was denied. Please enable it in your system settings.',
+          )
+        }
+        return
+      }
+
+      try {
+        await saveImageToMediaLibrary({uri})
+        Toast.show('Saved to your camera roll.')
+      } catch (e: any) {
+        Toast.show(`Failed to save image: ${String(e)}`)
+      }
+    },
+    [permissionResponse, requestPermission],
+  )
+
+  const lightbox = store.shell.activeLightbox
+  if (!lightbox) {
+    return null
+  }
+
+  let altText = ''
+  let uri = ''
+  if (lightbox.name === 'images') {
+    const opts = lightbox as models.ImagesLightbox
+    uri = opts.images[imageIndex].uri
+    altText = opts.images[imageIndex].alt || ''
+  } else if (lightbox.name === 'profile-image') {
+    const opts = lightbox as models.ProfileImageLightbox
+    uri = opts.profileView.avatar || ''
+  }
+
+  return (
+    <View style={[styles.footer]}>
+      {altText ? (
+        <Pressable
+          onPress={() => setAltExpanded(!isAltExpanded)}
+          accessibilityRole="button">
+          <Text
+            style={[s.gray3, styles.footerText]}
+            numberOfLines={isAltExpanded ? undefined : 3}>
+            {altText}
+          </Text>
+        </Pressable>
+      ) : null}
+      <View style={styles.footerBtns}>
+        <Button
+          type="primary-outline"
+          style={styles.footerBtn}
+          onPress={() => saveImageToAlbumWithToasts(uri)}>
+          <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} />
+          <Text type="xl" style={s.white}>
+            Save
+          </Text>
+        </Button>
+        <Button
+          type="primary-outline"
+          style={styles.footerBtn}
+          onPress={() => shareImageModal({uri})}>
+          <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} />
+          <Text type="xl" style={s.white}>
+            Share
+          </Text>
+        </Button>
+      </View>
+    </View>
+  )
+})
+
 const styles = StyleSheet.create({
   footer: {
     paddingTop: 16,