From 44df7a7268dbb143b9bbb6816d9656b3b1b2119d Mon Sep 17 00:00:00 2001
From: Terry Sahaidak <tasssik@gmail.com>
Date: Mon, 15 May 2023 18:27:48 +0300
Subject: [PATCH 001/133] wip new pager

---
 ios/Podfile.lock                              |  10 +-
 package-lock.json                             |  16 +
 package.json                                  |   3 +-
 src/components/AttachmentCarousel/Lightbox.js | 773 ++++++++++++++++++
 src/components/AttachmentCarousel/index.js    |  47 +-
 src/components/AttachmentModal.js             | 123 +--
 src/components/Image/index.native.js          |  24 +-
 7 files changed, 910 insertions(+), 86 deletions(-)
 create mode 100644 src/components/AttachmentCarousel/Lightbox.js

diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index e81f2d2c640c..4e6356d143d3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -518,6 +518,8 @@ PODS:
     - React-Core
   - react-native-netinfo (8.3.1):
     - React-Core
+  - react-native-pager-view (6.2.0):
+    - React-Core
   - react-native-pdf (6.6.2):
     - React-Core
   - react-native-performance (4.0.0):
@@ -773,6 +775,7 @@ DEPENDENCIES:
   - react-native-image-picker (from `../node_modules/react-native-image-picker`)
   - react-native-key-command (from `../node_modules/react-native-key-command`)
   - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
+  - react-native-pager-view (from `../node_modules/react-native-pager-view`)
   - react-native-pdf (from `../node_modules/react-native-pdf`)
   - react-native-performance (from `../node_modules/react-native-performance`)
   - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`)
@@ -928,6 +931,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-key-command"
   react-native-netinfo:
     :path: "../node_modules/@react-native-community/netinfo"
+  react-native-pager-view:
+    :path: "../node_modules/react-native-pager-view"
   react-native-pdf:
     :path: "../node_modules/react-native-pdf"
   react-native-performance:
@@ -1014,7 +1019,7 @@ EXTERNAL SOURCES:
 SPEC CHECKSUMS:
   Airship: c70eed50e429f97f5adb285423c7291fb7a032ae
   AirshipFrameworkProxy: 2eefb77bb77b5120b0f48814b0d44439aa3ad415
-  boost: a7c83b31436843459a1961bfd74b96033dc77234
+  boost: 57d2868c099736d80fcd648bf211b4431e51a558
   CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
   DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
   FBLazyVector: ff54429f0110d3c722630a98096ba689c39f6d5f
@@ -1057,7 +1062,7 @@ SPEC CHECKSUMS:
   Permission-LocationWhenInUse: 3ba99e45c852763f730eabecec2870c2382b7bd4
   Plaid: 7d340abeadb46c7aa1a91f896c5b22395a31fcf2
   PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
-  RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
+  RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
   RCTRequired: e9e7b8b45aa9bedb2fdad71740adf07a7265b9be
   RCTTypeSafety: 9ae0e9206625e995f0df4d5b9ddc94411929fb30
   React: a71c8e1380f07e01de721ccd52bcf9c03e81867d
@@ -1081,6 +1086,7 @@ SPEC CHECKSUMS:
   react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
   react-native-key-command: 0b3aa7c9f5c052116413e81dce33a3b2153a6c5d
   react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658
+  react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df
   react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
   react-native-performance: 224bd53e6a835fda4353302cf891d088a0af7406
   react-native-plaid-link-sdk: 9eb0f71dad94b3bdde649c7a384cba93024af46c
diff --git a/package-lock.json b/package-lock.json
index 8520c058c17d..ed6bfefd4122 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -78,6 +78,7 @@
         "react-native-localize": "^2.2.6",
         "react-native-modal": "^13.0.0",
         "react-native-onyx": "1.0.41",
+        "react-native-pager-view": "^6.2.0",
         "react-native-pdf": "^6.6.2",
         "react-native-performance": "^4.0.0",
         "react-native-permissions": "^3.0.1",
@@ -34886,6 +34887,15 @@
         }
       }
     },
+    "node_modules/react-native-pager-view": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.0.tgz",
+      "integrity": "sha512-pf9OnL/Tkr+5s4Gjmsn7xh91PtJLDa6qxYa/bmtUhd/+s4cQdWQ8DIFoOFghwZIHHHwVdWtoXkp6HtpjN+r20g==",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-pdf": {
       "version": "6.6.2",
       "license": "MIT",
@@ -64685,6 +64695,12 @@
         "underscore": "^1.13.1"
       }
     },
+    "react-native-pager-view": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.0.tgz",
+      "integrity": "sha512-pf9OnL/Tkr+5s4Gjmsn7xh91PtJLDa6qxYa/bmtUhd/+s4cQdWQ8DIFoOFghwZIHHHwVdWtoXkp6HtpjN+r20g==",
+      "requires": {}
+    },
     "react-native-pdf": {
       "version": "6.6.2",
       "requires": {
diff --git a/package.json b/package.json
index 0328a8450192..10c2c78e9113 100644
--- a/package.json
+++ b/package.json
@@ -113,6 +113,7 @@
     "react-native-localize": "^2.2.6",
     "react-native-modal": "^13.0.0",
     "react-native-onyx": "1.0.41",
+    "react-native-pager-view": "^6.2.0",
     "react-native-pdf": "^6.6.2",
     "react-native-performance": "^4.0.0",
     "react-native-permissions": "^3.0.1",
@@ -196,8 +197,8 @@
     "metro-react-native-babel-preset": "^0.73.3",
     "mock-fs": "^4.13.0",
     "onchange": "^7.1.0",
-    "prettier": "^2.8.8",
     "portfinder": "^1.0.28",
+    "prettier": "^2.8.8",
     "pusher-js-mock": "^0.3.3",
     "react-native-clean-project": "^4.0.0-alpha4.0",
     "react-native-flipper": "https://gitpkg.now.sh/facebook/flipper/react-native/react-native-flipper?9cacc9b59402550eae866e0e81e5f0c2f8203e6b",
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
new file mode 100644
index 000000000000..54e88f42a434
--- /dev/null
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -0,0 +1,773 @@
+import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
+import { Image, ActivityIndicator, StyleSheet, View, useWindowDimensions } from 'react-native';
+import { Gesture, GestureDetector, createNativeWrapper } from 'react-native-gesture-handler';
+import Animated, {
+    cancelAnimation,
+    runOnJS,
+    runOnUI,
+    useAnimatedProps,
+    useAnimatedReaction,
+    useAnimatedStyle,
+    useDerivedValue,
+    useEvent,
+    useHandler,
+    useSharedValue,
+    useWorkletCallback,
+    withDecay,
+    withSpring,
+} from 'react-native-reanimated';
+import PagerView from 'react-native-pager-view';
+// import Image from '../Image';
+
+const Context = createContext(null);
+
+function clamp(value, lowerBound, upperBound) {
+    'worklet';
+
+    return Math.min(Math.max(lowerBound, value), upperBound);
+}
+
+const DOUBLE_TAP_SCALE = 3;
+const MAX_SCALE = 3;
+const MIN_SCALE = 0.7;
+
+const config = {
+    mass: 3,
+    stiffness: 1000,
+    damping: 500,
+};
+
+function getScaledDimensions({ canvasWidth, canvasHeight, imageWidth, imageHeight }) {
+    const scaleFactor = imageWidth / canvasWidth;
+
+    const width = canvasWidth;
+    const height = imageHeight / scaleFactor;
+
+    return { width, height };
+}
+
+const defaultDimensions = {
+    width: 1,
+    height: 1,
+};
+
+function ImageTransformer({ canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback }) {
+    const { pagerRef, shouldPagerScroll, isScrolling } = useContext(Context);
+
+    const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
+
+    const [imageDimensions, setImageDimensions] = useState(
+        showFallback
+            ? defaultDimensions
+            : getScaledDimensions({
+                canvasWidth,
+                canvasHeight,
+                imageWidth,
+                imageHeight,
+            }),
+    );
+
+    const canvasX = useSharedValue(canvasWidth);
+    const canvasY = useSharedValue(canvasHeight);
+
+    useEffect(() => {
+        runOnUI(() => {
+            'worklet';
+
+            canvasX.value = canvasWidth;
+            canvasY.value = canvasHeight;
+        })();
+    }, [canvasX, canvasY, canvasWidth, canvasHeight]);
+
+    const targetHeight = useSharedValue(0);
+
+    const onLoad = (resolvedDimensions) => {
+        const { width, height } = getScaledDimensions({
+            canvasWidth,
+            canvasHeight,
+            imageWidth: resolvedDimensions.width,
+            imageHeight: resolvedDimensions.height,
+        });
+
+        targetHeight.value = height;
+
+        setImageDimensions({
+            width,
+            height,
+        });
+
+        setShowFallback(false);
+    };
+
+    // used for pan gesture
+
+    const translateY = useSharedValue(0);
+    const translateX = useSharedValue(0);
+    const offsetX = useSharedValue(0);
+    const offsetY = useSharedValue(0);
+    const isSwiping = useSharedValue(false);
+
+    // used for moving fingers when pinching
+    const scaleTranslateX = useSharedValue(0);
+    const scaleTranslateY = useSharedValue(0);
+
+    // storage for the the origin of the gesture
+    const origin = {
+        x: useSharedValue(0),
+        y: useSharedValue(0),
+    };
+
+    // storage for the pan velocity to calculate the decay
+    const panVelocityX = useSharedValue(0);
+    const panVelocityY = useSharedValue(0);
+
+    const scale = useSharedValue(1);
+    // store scale in between gestures
+    const scaleOffset = useSharedValue(1);
+
+    // disable pan vertically when image is smaller than screen
+    const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
+
+    // calculates bounds of the scaled image
+    // can we pan left/right/up/down
+    // can be used to limit gesture or implementing tension effect
+    const getBounds = useWorkletCallback(() => {
+        const target = {
+            x: 0,
+            y: 0,
+        };
+
+        const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
+        const scaledImage = targetHeight.value * fixedScale;
+
+        const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
+
+        let topBoundary = 0;
+
+        if (canvasY.value < scaledImage) {
+            topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
+        }
+
+        const maxVector = { x: rightBoundary, y: topBoundary };
+        const minVector = { x: -rightBoundary, y: -topBoundary };
+
+        target.x = clamp(offsetX.value, minVector.x, maxVector.x);
+        target.y = clamp(offsetY.value, minVector.y, maxVector.y);
+
+        const isInBoundaryX = target.x === offsetX.value;
+        const isInBoundaryY = target.y === offsetY.value;
+
+        return {
+            target,
+            isInBoundaryX,
+            isInBoundaryY,
+            minVector,
+            maxVector,
+            canPanLeft: target.x < maxVector.x,
+            canPanRight: target.x > minVector.x,
+        };
+    });
+
+    const afterGesture = useWorkletCallback(() => {
+        const { target, isInBoundaryX, isInBoundaryY, minVector, maxVector } = getBounds();
+
+        if (!canPanVertically.value) {
+            offsetY.value = withSpring(target.y, config);
+        }
+
+        if (
+            scale.value === 1 &&
+            offsetX.value === 0 &&
+            offsetY.value === 0 &&
+            translateX.value === 0 &&
+            translateY.value === 0 &&
+            scaleTranslateX.value === 0 &&
+            scaleTranslateY.value === 0
+        ) {
+            // we don't need to run any animations
+            return;
+        }
+
+        if (scale.value <= 1) {
+            // just center it
+            // reset(true);
+            offsetX.value = withSpring(0, config);
+            offsetY.value = withSpring(0, config);
+            return;
+        }
+
+        const deceleration = 0.9915;
+
+        if (isInBoundaryX) {
+            if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
+                offsetX.value = withDecay({
+                    velocity: panVelocityX.value,
+                    clamp: [minVector.x, maxVector.x],
+                    deceleration,
+                    rubberBandEffect: false,
+                });
+            }
+        } else {
+            offsetX.value = withSpring(target.x, config);
+        }
+
+        if (isInBoundaryY) {
+            if (
+                Math.abs(panVelocityY.value) > 0 &&
+                scale.value <= MAX_SCALE &&
+                // limit vertical pan only when image is smaller than screen
+                offsetY.value !== minVector.y &&
+                offsetY.value !== maxVector.y
+            ) {
+                offsetY.value = withDecay({
+                    velocity: panVelocityY.value,
+                    clamp: [minVector.y, maxVector.y],
+                    deceleration,
+                });
+            }
+        } else {
+            offsetY.value = withSpring(target.y, config, () => {
+                isSwiping.value = false;
+            });
+        }
+    });
+
+    const stopAnimation = useWorkletCallback(() => {
+        cancelAnimation(offsetX);
+        cancelAnimation(offsetY);
+    });
+
+    const zoomToCoordinates = useWorkletCallback((x, y) => {
+        'worklet';
+
+        stopAnimation();
+
+        const usableImage = {
+            x: canvasWidth,
+            y: targetHeight.value,
+        };
+
+        const targetImageSize = {
+            x: usableImage.x * DOUBLE_TAP_SCALE,
+            y: usableImage.y * DOUBLE_TAP_SCALE,
+        };
+
+        const CENTER = {
+            x: canvasX.value / 2,
+            y: canvasY.value / 2,
+        };
+
+        const imageCenter = {
+            x: usableImage.x / 2,
+            y: usableImage.y / 2,
+        };
+
+        const focal = { x, y };
+
+        const currentOrigin = {
+            x: (targetImageSize.x / 2 - CENTER.x) * -1,
+            y: (targetImageSize.y / 2 - CENTER.y) * -1,
+        };
+
+        const koef = {
+            x: (1 / imageCenter.x) * focal.x - 1,
+            y: (1 / imageCenter.y) * focal.y - 1,
+        };
+
+        const target = {
+            x: currentOrigin.x * koef.x,
+            y: currentOrigin.y * koef.y,
+        };
+
+        if (targetImageSize.y < canvasY.value) {
+            target.y = 0;
+        }
+
+        offsetX.value = withSpring(target.x, config);
+        offsetY.value = withSpring(target.y, config);
+        scale.value = withSpring(DOUBLE_TAP_SCALE, config);
+        scaleOffset.value = DOUBLE_TAP_SCALE;
+    });
+
+    const reset = useWorkletCallback((animated) => {
+        scaleOffset.value = 1;
+
+        stopAnimation();
+
+        if (animated) {
+            offsetX.value = withSpring(0, config);
+            offsetY.value = withSpring(0, config);
+            scale.value = withSpring(1, config);
+        } else {
+            scale.value = 1;
+            translateX.value = 0;
+            translateY.value = 0;
+            offsetX.value = 0;
+            offsetY.value = 0;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+        }
+    });
+
+    const doubleTap = Gesture.Tap()
+        .numberOfTaps(2)
+        .maxDelay(150)
+        .maxDistance(20)
+        .onEnd((evt) => {
+            if (scale.value > 1) {
+                reset(true);
+            } else {
+                zoomToCoordinates(evt.x, evt.y);
+            }
+        });
+
+    const panGestureRef = useRef(Gesture.Pan());
+
+    const singleTap = Gesture.Tap()
+        .numberOfTaps(1)
+        .maxDuration(50)
+        .requireExternalGestureToFail(doubleTap, panGestureRef)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onFinalize((evt, success) => {
+            if (success) {
+                console.log('single tap');
+            }
+        });
+
+    const previousTouch = useSharedValue(null);
+
+    const panGesture = Gesture.Pan()
+        .manualActivation(true)
+        .averageTouches(true)
+        .onTouchesMove((evt, state) => {
+            if (scale.value > 1) {
+                state.activate();
+            }
+
+            // this is for swipe down to close
+            // needs fine tuning to work properly
+            if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
+                const velocityX = evt.allTouches[0].x - previousTouch.value.x;
+                const velocityY = evt.allTouches[0].y - previousTouch.value.y;
+
+                // this needs tuning
+                if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
+                    state.activate();
+
+                    isSwiping.value = true;
+                    previousTouch.value = null;
+                }
+            }
+
+            if (previousTouch.value == null) {
+                previousTouch.value = {
+                    x: evt.allTouches[0].x,
+                    y: evt.allTouches[0].y,
+                };
+            }
+        })
+        .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onChange((evt) => {
+            // since we running both pinch and pan gesture handlers simultaneously
+            // we need to make sure that we don't pan when we pinch and move fingers
+            // since we track it as pinch focal gesture
+            if (evt.numberOfPointers > 1 || isScrolling.value) {
+                return;
+            }
+
+            panVelocityX.value = evt.velocityX;
+
+            panVelocityY.value = evt.velocityY;
+
+            if (!isSwiping.value) {
+                translateX.value += evt.changeX;
+            }
+
+            if (canPanVertically.value || isSwiping.value) {
+                translateY.value += evt.changeY;
+            }
+        })
+        .onEnd((evt) => {
+            previousTouch.value = null;
+
+            if (isScrolling.value) {
+                return;
+            }
+
+            offsetX.value += translateX.value;
+            offsetY.value += translateY.value;
+            translateX.value = 0;
+            translateY.value = 0;
+
+            if (isSwiping.value) {
+                const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
+                const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
+
+                if (enoughVelocity && rightDirection) {
+                    const maybeInvert = (v) => {
+                        const invert = evt.velocityY < 0;
+                        return invert ? -v : v;
+                    };
+
+                    offsetY.value = withSpring(
+                        maybeInvert(targetHeight.value * 2),
+                        {
+                            stiffness: 50,
+                            damping: 30,
+                            mass: 1,
+                            overshootClamping: true,
+                            restDisplacementThreshold: 300,
+                            restSpeedThreshold: 300,
+                            velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
+                        },
+                        () => {
+                            console.log('success');
+                            runOnJS(onSwipeSuccess)();
+                        },
+                    );
+                    return;
+                }
+            }
+
+            afterGesture();
+
+            panVelocityX.value = 0;
+            panVelocityY.value = 0;
+        })
+        .withRef(panGestureRef);
+
+    const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
+        x: focalX - (canvasX.value / 2 + offsetX.value),
+        y: focalY - (canvasY.value / 2 + offsetY.value),
+    }));
+
+    // used to store event scale value when we limit scale
+    const gestureScale = useSharedValue(1);
+
+    const pinchGesture = Gesture.Pinch()
+        .onTouchesDown((evt, state) => {
+            // we don't want to activate pinch gesture when we are scrolling pager
+            if (isScrolling.value) {
+                state.fail();
+            }
+        })
+        .simultaneousWithExternalGesture(panGesture, doubleTap)
+        .onStart((evt) => {
+            stopAnimation();
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            origin.x.value = adjustFocal.x;
+            origin.y.value = adjustFocal.y;
+        })
+        .onChange((evt) => {
+            scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+
+            if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
+                gestureScale.value = evt.scale;
+            }
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
+            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
+        })
+        .onEnd(() => {
+            offsetX.value += scaleTranslateX.value;
+            offsetY.value += scaleTranslateY.value;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+            scaleOffset.value = scale.value;
+            gestureScale.value = 1;
+
+            if (scaleOffset.value < 1) {
+                // make sure we don't add stuff below the 1
+                scaleOffset.value = 1;
+
+                // this runs the timing animation
+                scale.value = withSpring(1, config);
+            } else if (scaleOffset.value > MAX_SCALE) {
+                scaleOffset.value = MAX_SCALE;
+                scale.value = withSpring(MAX_SCALE, config);
+            }
+
+            afterGesture();
+        });
+
+    const animatedStyles = useAnimatedStyle(() => {
+        const x = scaleTranslateX.value + translateX.value + offsetX.value;
+        const y = scaleTranslateY.value + translateY.value + offsetY.value;
+
+        if (isSwiping.value) {
+            onSwipe(y);
+        }
+
+        return {
+            transform: [
+                {
+                    translateX: x,
+                },
+                {
+                    translateY: y,
+                },
+                { scale: scale.value },
+            ],
+        };
+    }, []);
+
+    // reacts to scale change and enables/disables pager scroll
+    useAnimatedReaction(
+        () => scale.value,
+        () => {
+            shouldPagerScroll.value = scale.value === 1;
+        },
+    );
+
+    const mounted = useRef(false);
+    useEffect(() => {
+        if (!mounted.current) {
+            mounted.current = true;
+            return;
+        }
+
+        if (!isActive) {
+            runOnUI(reset)(false);
+        }
+    }, [isActive, mounted, reset]);
+
+    return (
+        <View
+            collapsable={false}
+            style={{
+                flex: 1,
+                width: canvasWidth,
+            }}
+        >
+            <GestureDetector gesture={pinchGesture}>
+                <Animated.View
+                    collapsable
+                    style={StyleSheet.absoluteFill}
+                >
+                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
+                        <Animated.View
+                            collapsable
+                            style={[StyleSheet.absoluteFill]}
+                        >
+                            <ImageWrapper>
+                                <GestureDetector gesture={doubleTap}>
+                                    <Animated.View
+                                        collapsable={false}
+                                        style={[animatedStyles]}
+                                    >
+                                        {showFallback && renderFallback()}
+
+                                        {renderImage({
+                                            onResolveImageDimensions: onLoad,
+                                            ...imageDimensions,
+                                            style: showFallback ? { opacity: 0, position: 'absolute' } : {},
+                                        })}
+                                    </Animated.View>
+                                </GestureDetector>
+                            </ImageWrapper>
+                        </Animated.View>
+                    </GestureDetector>
+                </Animated.View>
+            </GestureDetector>
+        </View>
+    );
+}
+
+function ImageWrapper({ children }) {
+    return (
+        <Animated.View
+            collapsable={false}
+            style={[
+                {
+                    ...StyleSheet.absoluteFillObject,
+                    justifyContent: 'center',
+                    alignItems: 'center',
+                },
+            ]}
+        >
+            {children}
+        </Animated.View>
+    );
+}
+
+const cachedDimensions = new Map();
+
+function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight }) {
+    console.log
+
+    const dimensions = cachedDimensions.get(item.url);
+
+    if (!isActive) {
+        return (
+            <ImageWrapper>
+                <Image
+                    source={{ uri: item.url }}
+                    onLoad={(evt) => {
+                        cachedDimensions.set(item.url, {
+                            width: evt.nativeEvent.source.width,
+                            height: evt.nativeEvent.source.height,
+                        });
+                    }}
+                    style={
+                        dimensions
+                            ? getScaledDimensions({
+                                imageHeight: dimensions?.height,
+                                imageWidth: dimensions?.width,
+                                canvasHeight,
+                                canvasWidth,
+                            })
+                            : {}
+                    }
+                />
+            </ImageWrapper>
+        );
+    }
+
+    return (
+        <ImageTransformer
+            onSwipe={onSwipe}
+            onSwipeSuccess={onSwipeSuccess}
+            isActive
+            imageHeight={dimensions?.height}
+            imageWidth={dimensions?.width}
+            canvasHeight={canvasHeight}
+            canvasWidth={canvasWidth}
+            renderFallback={() => <ActivityIndicator />}
+            renderImage={({ onResolveImageDimensions, width, height, style }) => (
+                <Image
+                    source={{ uri: item.url }}
+                    style={[style, { width, height }]}
+                    onLoad={(evt) => {
+                        cachedDimensions.set(item.url, {
+                            width: evt.nativeEvent.source.width,
+                            height: evt.nativeEvent.source.height,
+                        });
+
+
+                        onResolveImageDimensions({
+                            width: evt.nativeEvent.source.width,
+                            height: evt.nativeEvent.source.height,
+                        });
+                    }}
+                />
+            )}
+        />
+    );
+}
+
+const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
+
+function usePageScrollHandler(handlers, dependencies) {
+    const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
+    const subscribeForEvents = ['onPageScroll'];
+
+    return useEvent(
+        (event) => {
+            'worklet';
+
+            const { onPageScroll } = handlers;
+            if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
+                onPageScroll(event, context);
+            }
+        },
+        subscribeForEvents,
+        doDependenciesDiffer,
+    );
+}
+
+const noopWorklet = () => {
+    'worklet';
+
+    // noop
+};
+
+export default function Pager({ items, initialIndex = 0, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => { } }) {
+    const windowDimensions = useWindowDimensions();
+
+    const shouldPagerScroll = useSharedValue(true);
+    const pagerRef = useRef(null);
+
+    const isScrolling = useSharedValue(false);
+    const activeIndex = useSharedValue(initialIndex);
+
+    const pageScrollHandler = usePageScrollHandler(
+        {
+            onPageScroll: (e) => {
+                'worklet';
+
+                activeIndex.value = e.position;
+                isScrolling.value = e.offset !== 0;
+            },
+        },
+        [],
+    );
+
+    const [activePage, setActivePage] = useState(initialIndex);
+
+    // we use reanimated for this since onPageSelected is called
+    // in the middle of the pager animation
+    useAnimatedReaction(
+        () => isScrolling.value,
+        (stillScrolling) => {
+            if (stillScrolling) {
+                return;
+            }
+
+            runOnJS(setActivePage)(activeIndex.value);
+        },
+    );
+
+    const animatedProps = useAnimatedProps(() => ({
+        scrollEnabled: shouldPagerScroll.value,
+    }));
+
+    return (
+        <View style={{ flex: 1 }}>
+            <View style={{ flex: 1 }}>
+                <Context.Provider
+                    value={{
+                        isScrolling,
+                        pagerRef,
+                        shouldPagerScroll,
+                    }}
+                >
+                    <AnimatedPagerView
+                        pageMargin={40}
+                        onPageScroll={pageScrollHandler}
+                        animatedProps={animatedProps}
+                        ref={pagerRef}
+                        style={{ flex: 1 }}
+                        initialPage={initialIndex}
+                    >
+                        {items
+                            .map((item, index) => itemExtractor({ item, index }))
+                            .map((item, index) => (
+                                <View
+                                    key={item.key}
+                                    style={{ flex: 1 }}
+                                >
+                                    <Page
+                                        onSwipe={onSwipe}
+                                        onSwipeSuccess={onSwipeSuccess}
+                                        isActive={index === activePage}
+                                        item={item}
+                                        canvasHeight={windowDimensions.height}
+                                        canvasWidth={windowDimensions.width}
+                                    />
+                                </View>
+                            ))}
+                    </AnimatedPagerView>
+                </Context.Provider>
+            </View>
+        </View>
+    );
+}
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index d2435c363203..d16458e88e8c 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -1,7 +1,7 @@
 import React from 'react';
-import {View, FlatList} from 'react-native';
+import { View, FlatList } from 'react-native';
 import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
+import { withOnyx } from 'react-native-onyx';
 import _ from 'underscore';
 import * as Expensicons from '../Icon/Expensicons';
 import styles from '../../styles/styles';
@@ -17,9 +17,10 @@ import ONYXKEYS from '../../ONYXKEYS';
 import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
 import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
 import Tooltip from '../Tooltip';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import withLocalize, { withLocalizePropTypes } from '../withLocalize';
 import compose from '../../libs/compose';
 import withWindowDimensions from '../withWindowDimensions';
+import Pager from './Lightbox';
 
 const propTypes = {
     /** source is used to determine the starting index in the array of attachments */
@@ -37,7 +38,7 @@ const propTypes = {
 const defaultProps = {
     source: '',
     reportActions: {},
-    onNavigate: () => {},
+    onNavigate: () => { },
 };
 
 class AttachmentCarousel extends React.Component {
@@ -76,7 +77,7 @@ class AttachmentCarousel extends React.Component {
      */
     getAttachment(attachmentItem) {
         const source = _.get(attachmentItem, 'source', '');
-        const file = _.get(attachmentItem, 'file', {name: ''});
+        const file = _.get(attachmentItem, 'file', { name: '' });
 
         return {
             source,
@@ -131,7 +132,7 @@ class AttachmentCarousel extends React.Component {
         this.setState(
             (current) => {
                 const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-                return {shouldShowArrow: newShouldShowArrow};
+                return { shouldShowArrow: newShouldShowArrow };
             },
             () => {
                 if (this.state.shouldShowArrow) {
@@ -155,7 +156,7 @@ class AttachmentCarousel extends React.Component {
         if (isZoomed) {
             this.toggleArrowsVisibility(false);
         }
-        this.setState({isZoomed});
+        this.setState({ isZoomed });
     }
 
     /**
@@ -170,7 +171,7 @@ class AttachmentCarousel extends React.Component {
          * Looping to filter out attachments and retrieve the src URL and name of attachments.
          */
         const attachments = [];
-        _.forEach(actions, ({originalMessage, message}) => {
+        _.forEach(actions, ({ originalMessage, message }) => {
             // Check for attachment which hasn't been deleted
             if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
                 return;
@@ -188,7 +189,7 @@ class AttachmentCarousel extends React.Component {
                     page = attachments.length;
                 }
 
-                attachments.push({source, file: {name}});
+                attachments.push({ source, file: { name } });
             }
         });
 
@@ -215,14 +216,14 @@ class AttachmentCarousel extends React.Component {
 
         // The sliding transition is a bit too much on web, because of the wider and bigger images,
         // so we only enable it for mobile
-        this.scrollRef.current.scrollToIndex({index: nextIndex, animated: this.canUseTouchScreen});
+        this.scrollRef.current.scrollToIndex({ index: nextIndex, animated: this.canUseTouchScreen });
     }
 
     /**
      * Updates the page state when the user navigates between attachments
      * @param {Array<{item: *, index: Number}>} viewableItems
      */
-    updatePage({viewableItems}) {
+    updatePage({ viewableItems }) {
         // Since we can have only one item in view at a time, we can use the first item in the array
         // to get the index of the current page
         const entry = _.first(viewableItems);
@@ -231,9 +232,9 @@ class AttachmentCarousel extends React.Component {
         }
 
         const page = entry.index;
-        const {source, file} = this.getAttachment(entry.item);
-        this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
-        this.setState({page, isZoomed: false});
+        const { source, file } = this.getAttachment(entry.item);
+        this.props.onNavigate({ source: addEncryptedAuthTokenToURL(source), file });
+        this.setState({ page, isZoomed: false });
     }
 
     /**
@@ -242,7 +243,7 @@ class AttachmentCarousel extends React.Component {
      * @returns {JSX.Element}
      */
     renderCell(props) {
-        const style = [props.style, styles.h100, {width: this.state.containerWidth}];
+        const style = [props.style, styles.h100, { width: this.state.containerWidth }];
 
         return (
             <View
@@ -258,7 +259,7 @@ class AttachmentCarousel extends React.Component {
      * @param {{ source: String, file: { name: String } }} item
      * @returns {JSX.Element}
      */
-    renderItem({item}) {
+    renderItem({ item }) {
         const authSource = addEncryptedAuthTokenToURL(item.source);
         if (!this.canUseTouchScreen) {
             return (
@@ -286,7 +287,7 @@ class AttachmentCarousel extends React.Component {
         return (
             <View
                 style={[styles.attachmentModalArrowsContainer, styles.flex1]}
-                onLayout={({nativeEvent}) => this.setState({containerWidth: nativeEvent.layout.width + 1})}
+                onLayout={({ nativeEvent }) => this.setState({ containerWidth: nativeEvent.layout.width + 1 })}
                 onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
                 onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
             >
@@ -333,7 +334,13 @@ class AttachmentCarousel extends React.Component {
                     </>
                 )}
 
-                {this.state.containerWidth > 0 && (
+                <Pager
+                    items={this.state.attachments}
+                    initialIndex={this.state.page}
+                    itemExtractor={({ item }) => ({ key: item.source, url: addEncryptedAuthTokenToURL(item.source) })}
+                />
+
+                {/* {this.state.containerWidth > 0 && (
                     <FlatList
                         listKey="AttachmentCarousel"
                         horizontal
@@ -367,7 +374,7 @@ class AttachmentCarousel extends React.Component {
                         viewabilityConfig={this.viewabilityConfig}
                         onViewableItemsChanged={this.updatePage}
                     />
-                )}
+                )} */}
 
                 <CarouselActions onCycleThroughAttachments={this.cycleThroughAttachments} />
             </View>
@@ -381,7 +388,7 @@ AttachmentCarousel.defaultProps = defaultProps;
 export default compose(
     withOnyx({
         reportActions: {
-            key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+            key: ({ reportID }) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
             canEvict: false,
         },
     }),
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 00f745da0769..0cc26dbc14f2 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -1,10 +1,11 @@
-import React, {PureComponent} from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
-import {View, Animated, Keyboard} from 'react-native';
+import { View, Animated, Keyboard } from 'react-native';
 import Str from 'expensify-common/lib/str';
 import lodashGet from 'lodash/get';
 import lodashExtend from 'lodash/extend';
 import _ from 'underscore';
+import { GestureHandlerRootView } from 'react-native-gesture-handler'
 import CONST from '../CONST';
 import Modal from './Modal';
 import AttachmentView from './AttachmentView';
@@ -14,11 +15,11 @@ import * as StyleUtils from '../styles/StyleUtils';
 import * as FileUtils from '../libs/fileDownload/FileUtils';
 import themeColors from '../styles/themes/default';
 import compose from '../libs/compose';
-import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
+import withWindowDimensions, { windowDimensionsPropTypes } from './withWindowDimensions';
 import Button from './Button';
 import HeaderWithCloseButton from './HeaderWithCloseButton';
 import fileDownload from '../libs/fileDownload';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
+import withLocalize, { withLocalizePropTypes } from './withLocalize';
 import ConfirmModal from './ConfirmModal';
 import HeaderGap from './HeaderGap';
 import SafeAreaConsumer from './SafeAreaConsumer';
@@ -69,7 +70,7 @@ const defaultProps = {
     allowDownload: false,
     headerTitle: null,
     reportID: '',
-    onModalHide: () => {},
+    onModalHide: () => { },
 };
 
 class AttachmentModal extends PureComponent {
@@ -88,8 +89,8 @@ class AttachmentModal extends PureComponent {
             confirmButtonFadeAnimation: new Animated.Value(1),
             file: props.originalFileName
                 ? {
-                      name: props.originalFileName,
-                  }
+                    name: props.originalFileName,
+                }
                 : undefined,
         };
 
@@ -144,17 +145,17 @@ class AttachmentModal extends PureComponent {
         }
 
         if (this.props.onConfirm) {
-            this.props.onConfirm(lodashExtend(this.state.file, {source: this.state.source}));
+            this.props.onConfirm(lodashExtend(this.state.file, { source: this.state.source }));
         }
 
-        this.setState({isModalOpen: false});
+        this.setState({ isModalOpen: false });
     }
 
     /**
      * Close the confirm modal.
      */
     closeConfirmModal() {
-        this.setState({isAttachmentInvalid: false});
+        this.setState({ isAttachmentInvalid: false });
     }
 
     /**
@@ -162,7 +163,7 @@ class AttachmentModal extends PureComponent {
      * @returns {Boolean}
      */
     isValidFile(file) {
-        const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
+        const { fileExtension } = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
         if (!_.contains(CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
             const invalidReason = `${this.props.translate('attachmentPicker.notAllowedExtension')} ${CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS.join(', ')}`;
             this.setState({
@@ -235,7 +236,7 @@ class AttachmentModal extends PureComponent {
      * @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in.
      */
     updateConfirmButtonVisibility(shouldFadeOut) {
-        this.setState({isConfirmButtonDisabled: shouldFadeOut});
+        this.setState({ isConfirmButtonDisabled: shouldFadeOut });
         const toValue = shouldFadeOut ? 0 : 1;
 
         Animated.timing(this.state.confirmButtonFadeAnimation, {
@@ -247,67 +248,73 @@ class AttachmentModal extends PureComponent {
 
     render() {
         const source = this.state.source;
+        console.log('render attachment modal')
+
         return (
             <>
                 <Modal
                     type={this.state.modalType}
                     onSubmit={this.submitAndClose}
-                    onClose={() => this.setState({isModalOpen: false})}
+                    onClose={() => this.setState({ isModalOpen: false })}
                     isVisible={this.state.isModalOpen}
                     backgroundColor={themeColors.componentBG}
-                    onModalShow={() => this.setState({shouldLoadAttachment: true})}
+                    onModalShow={() => this.setState({ shouldLoadAttachment: true })}
                     onModalHide={(e) => {
                         this.props.onModalHide(e);
-                        this.setState({shouldLoadAttachment: false});
+                        this.setState({ shouldLoadAttachment: false });
                     }}
                     propagateSwipe
                 >
-                    {this.props.isSmallScreenWidth && <HeaderGap />}
-                    <HeaderWithCloseButton
-                        title={this.props.headerTitle || this.props.translate('common.attachment')}
-                        shouldShowBorderBottom
-                        shouldShowDownloadButton={this.props.allowDownload}
-                        onDownloadButtonPress={() => this.downloadAttachment(source)}
-                        onCloseButtonPress={() => this.setState({isModalOpen: false})}
-                    />
-                    <View style={styles.imageModalImageCenterContainer}>
-                        {this.props.reportID ? (
-                            <AttachmentCarousel
-                                reportID={this.props.reportID}
-                                onNavigate={this.onNavigate}
-                                source={this.props.source}
-                                onToggleKeyboard={this.updateConfirmButtonVisibility}
-                            />
-                        ) : (
-                            Boolean(this.state.source) &&
-                            this.state.shouldLoadAttachment && (
-                                <AttachmentView
-                                    source={source}
-                                    isAuthTokenRequired={this.props.isAuthTokenRequired}
-                                    file={this.state.file}
+                    <GestureHandlerRootView style={{ flex: 1 }}>
+                        {this.props.isSmallScreenWidth && <HeaderGap />}
+                        <HeaderWithCloseButton
+                            title={this.props.headerTitle || this.props.translate('common.attachment')}
+                            shouldShowBorderBottom
+                            shouldShowDownloadButton={this.props.allowDownload}
+                            onDownloadButtonPress={() => this.downloadAttachment(source)}
+                            onCloseButtonPress={() => this.setState({ isModalOpen: false })}
+                        />
+                        <View style={styles.imageModalImageCenterContainer}>
+                            {this.props.reportID ? (
+
+                                <AttachmentCarousel
+                                    reportID={this.props.reportID}
+                                    onNavigate={this.onNavigate}
+                                    source={this.props.source}
                                     onToggleKeyboard={this.updateConfirmButtonVisibility}
                                 />
-                            )
-                        )}
-                    </View>
-                    {/* If we have an onConfirm method show a confirmation button */}
-                    {Boolean(this.props.onConfirm) && (
-                        <SafeAreaConsumer>
-                            {({safeAreaPaddingBottomStyle}) => (
-                                <Animated.View style={[StyleUtils.fade(this.state.confirmButtonFadeAnimation), safeAreaPaddingBottomStyle]}>
-                                    <Button
-                                        success
-                                        style={[styles.buttonConfirm, this.props.isSmallScreenWidth ? {} : styles.attachmentButtonBigScreen]}
-                                        textStyles={[styles.buttonConfirmText]}
-                                        text={this.props.translate('common.send')}
-                                        onPress={this.submitAndClose}
-                                        disabled={this.state.isConfirmButtonDisabled}
-                                        pressOnEnter
+
+                            ) : (
+                                Boolean(this.state.source) &&
+                                this.state.shouldLoadAttachment && (
+                                    <AttachmentView
+                                        source={source}
+                                        isAuthTokenRequired={this.props.isAuthTokenRequired}
+                                        file={this.state.file}
+                                        onToggleKeyboard={this.updateConfirmButtonVisibility}
                                     />
-                                </Animated.View>
+                                )
                             )}
-                        </SafeAreaConsumer>
-                    )}
+                        </View>
+                        {/* If we have an onConfirm method show a confirmation button */}
+                        {Boolean(this.props.onConfirm) && (
+                            <SafeAreaConsumer>
+                                {({ safeAreaPaddingBottomStyle }) => (
+                                    <Animated.View style={[StyleUtils.fade(this.state.confirmButtonFadeAnimation), safeAreaPaddingBottomStyle]}>
+                                        <Button
+                                            success
+                                            style={[styles.buttonConfirm, this.props.isSmallScreenWidth ? {} : styles.attachmentButtonBigScreen]}
+                                            textStyles={[styles.buttonConfirmText]}
+                                            text={this.props.translate('common.send')}
+                                            onPress={this.submitAndClose}
+                                            disabled={this.state.isConfirmButtonDisabled}
+                                            pressOnEnter
+                                        />
+                                    </Animated.View>
+                                )}
+                            </SafeAreaConsumer>
+                        )}
+                    </GestureHandlerRootView>
                 </Modal>
 
                 <ConfirmModal
@@ -323,7 +330,7 @@ class AttachmentModal extends PureComponent {
                 {this.props.children({
                     displayFileInModal: this.validateAndDisplayFileToUpload,
                     show: () => {
-                        this.setState({isModalOpen: true});
+                        this.setState({ isModalOpen: true });
                     },
                 })}
             </>
diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js
index c7cc2e026063..447690c81ae0 100644
--- a/src/components/Image/index.native.js
+++ b/src/components/Image/index.native.js
@@ -1,15 +1,21 @@
 import React from 'react';
 import RNFastImage from 'react-native-fast-image';
-import {withOnyx} from 'react-native-onyx';
+import { withOnyx } from 'react-native-onyx';
 import lodashGet from 'lodash/get';
 import CONST from '../../CONST';
 import ONYXKEYS from '../../ONYXKEYS';
-import {defaultProps, imagePropTypes} from './imagePropTypes';
+import { defaultProps, imagePropTypes } from './imagePropTypes';
 import RESIZE_MODES from './resizeModes';
 
+const dimensionsCache = new Map();
+
+function resolveDimensions(key) {
+    return dimensionsCache.get(key);
+}
+
 const Image = (props) => {
     // eslint-disable-next-line react/destructuring-assignment
-    const {source, isAuthTokenRequired, session, ...rest} = props;
+    const { source, isAuthTokenRequired, session, ...rest } = props;
 
     let imageSource = source;
     if (typeof source !== 'number' && isAuthTokenRequired) {
@@ -18,8 +24,8 @@ const Image = (props) => {
             ...source,
             headers: authToken
                 ? {
-                      [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken,
-                  }
+                    [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken,
+                }
                 : null,
         };
     }
@@ -29,6 +35,13 @@ const Image = (props) => {
             // eslint-disable-next-line react/jsx-props-no-spreading
             {...rest}
             source={imageSource}
+            onLoad={(evt) => {
+                const { width, height } = evt.nativeEvent;
+                dimensionsCache.set(source.uri, { width, height });
+                if (props.onLoad) {
+                    props.onLoad(evt);
+                }
+            }}
         />
     );
 };
@@ -42,4 +55,5 @@ const ImageWithOnyx = withOnyx({
     },
 })(Image);
 ImageWithOnyx.resizeMode = RESIZE_MODES;
+ImageWithOnyx.resolveDimensions = resolveDimensions;
 export default ImageWithOnyx;

From 2c38af466e0abe3d0a6e177ab171b9e829575fa2 Mon Sep 17 00:00:00 2001
From: Terry Sahaidak <tasssik@gmail.com>
Date: Tue, 16 May 2023 13:12:12 +0300
Subject: [PATCH 002/133] use Image component

---
 src/components/AttachmentCarousel/Lightbox.js | 75 ++++++++++++-------
 1 file changed, 47 insertions(+), 28 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 54e88f42a434..319252067171 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,5 +1,5 @@
 import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
-import { Image, ActivityIndicator, StyleSheet, View, useWindowDimensions } from 'react-native';
+import { ActivityIndicator, StyleSheet, View, useWindowDimensions } from 'react-native';
 import { Gesture, GestureDetector, createNativeWrapper } from 'react-native-gesture-handler';
 import Animated, {
     cancelAnimation,
@@ -17,7 +17,7 @@ import Animated, {
     withSpring,
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
-// import Image from '../Image';
+import Image from '../Image';
 
 const Context = createContext(null);
 
@@ -38,11 +38,28 @@ const config = {
 };
 
 function getScaledDimensions({ canvasWidth, canvasHeight, imageWidth, imageHeight }) {
+    console.log({
+        canvasWidth,
+        canvasHeight,
+        imageWidth,
+        imageHeight,
+    })
+
     const scaleFactor = imageWidth / canvasWidth;
 
     const width = canvasWidth;
     const height = imageHeight / scaleFactor;
 
+    console.log({
+        canvasWidth,
+        canvasHeight,
+        imageWidth,
+        imageHeight,
+        width,
+        height,
+        scaleFactor,
+    })
+
     return { width, height };
 }
 
@@ -348,25 +365,25 @@ function ImageTransformer({ canvasWidth, canvasHeight, imageWidth, imageHeight,
 
             // this is for swipe down to close
             // needs fine tuning to work properly
-            if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
-                const velocityX = evt.allTouches[0].x - previousTouch.value.x;
-                const velocityY = evt.allTouches[0].y - previousTouch.value.y;
-
-                // this needs tuning
-                if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
-                    state.activate();
-
-                    isSwiping.value = true;
-                    previousTouch.value = null;
-                }
-            }
-
-            if (previousTouch.value == null) {
-                previousTouch.value = {
-                    x: evt.allTouches[0].x,
-                    y: evt.allTouches[0].y,
-                };
-            }
+            // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
+            //     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
+            //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
+
+            //     // this needs tuning
+            //     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
+            //         state.activate();
+
+            //         isSwiping.value = true;
+            //         previousTouch.value = null;
+            //     }
+            // }
+
+            // if (previousTouch.value == null) {
+            //     previousTouch.value = {
+            //         x: evt.allTouches[0].x,
+            //         y: evt.allTouches[0].y,
+            //     };
+            // }
         })
         .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
         .onBegin(() => {
@@ -612,9 +629,11 @@ function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeig
                 <Image
                     source={{ uri: item.url }}
                     onLoad={(evt) => {
+
+
                         cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent.source.width,
-                            height: evt.nativeEvent.source.height,
+                            width: evt.nativeEvent?.width,
+                            height: evt.nativeEvent?.height,
                         });
                     }}
                     style={
@@ -644,18 +663,18 @@ function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeig
             renderFallback={() => <ActivityIndicator />}
             renderImage={({ onResolveImageDimensions, width, height, style }) => (
                 <Image
-                    source={{ uri: item.url }}
+                    source={{ uri: item.url, }}
                     style={[style, { width, height }]}
                     onLoad={(evt) => {
                         cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent.source.width,
-                            height: evt.nativeEvent.source.height,
+                            width: evt.nativeEvent.width,
+                            height: evt.nativeEvent.height,
                         });
 
 
                         onResolveImageDimensions({
-                            width: evt.nativeEvent.source.width,
-                            height: evt.nativeEvent.source.height,
+                            width: evt.nativeEvent.width,
+                            height: evt.nativeEvent.height,
                         });
                     }}
                 />

From feb406c9f7ceaf9301a9944bb02cbfb076ce0caf Mon Sep 17 00:00:00 2001
From: Terry Sahaidak <tasssik@gmail.com>
Date: Wed, 17 May 2023 10:05:13 +0300
Subject: [PATCH 003/133] wip

---
 src/components/AttachmentCarousel/Lightbox.js | 1494 +++++++++--------
 src/components/AttachmentCarousel/index.js    |  707 ++++----
 .../AttachmentCarousel/index.native.js        |  357 ++++
 src/components/AttachmentModal.js             |  582 +++----
 src/components/ImageView/index.native.js      |   18 +-
 5 files changed, 1756 insertions(+), 1402 deletions(-)
 create mode 100644 src/components/AttachmentCarousel/index.native.js

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 319252067171..2fbe91c7c682 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,20 +1,20 @@
 import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
-import { ActivityIndicator, StyleSheet, View, useWindowDimensions } from 'react-native';
+import { ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions } from 'react-native';
 import { Gesture, GestureDetector, createNativeWrapper } from 'react-native-gesture-handler';
 import Animated, {
-    cancelAnimation,
-    runOnJS,
-    runOnUI,
-    useAnimatedProps,
-    useAnimatedReaction,
-    useAnimatedStyle,
-    useDerivedValue,
-    useEvent,
-    useHandler,
-    useSharedValue,
-    useWorkletCallback,
-    withDecay,
-    withSpring,
+	cancelAnimation,
+	runOnJS,
+	runOnUI,
+	useAnimatedProps,
+	useAnimatedReaction,
+	useAnimatedStyle,
+	useDerivedValue,
+	useEvent,
+	useHandler,
+	useSharedValue,
+	useWorkletCallback,
+	withDecay,
+	withSpring,
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import Image from '../Image';
@@ -22,771 +22,773 @@ import Image from '../Image';
 const Context = createContext(null);
 
 function clamp(value, lowerBound, upperBound) {
-    'worklet';
+	'worklet';
 
-    return Math.min(Math.max(lowerBound, value), upperBound);
+	return Math.min(Math.max(lowerBound, value), upperBound);
 }
 
 const DOUBLE_TAP_SCALE = 3;
-const MAX_SCALE = 3;
+const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
 
 const config = {
-    mass: 3,
-    stiffness: 1000,
-    damping: 500,
+	mass: 3,
+	stiffness: 1000,
+	damping: 500,
 };
 
 function getScaledDimensions({ canvasWidth, canvasHeight, imageWidth, imageHeight }) {
-    console.log({
-        canvasWidth,
-        canvasHeight,
-        imageWidth,
-        imageHeight,
-    })
-
-    const scaleFactor = imageWidth / canvasWidth;
-
-    const width = canvasWidth;
-    const height = imageHeight / scaleFactor;
-
-    console.log({
-        canvasWidth,
-        canvasHeight,
-        imageWidth,
-        imageHeight,
-        width,
-        height,
-        scaleFactor,
-    })
-
-    return { width, height };
+	console.log({
+		canvasWidth,
+		canvasHeight,
+		imageWidth,
+		imageHeight,
+	})
+
+	const scaleFactor = imageWidth / canvasWidth;
+
+	const width = canvasWidth;
+	const height = imageHeight / scaleFactor;
+
+	console.log({
+		canvasWidth,
+		canvasHeight,
+		imageWidth,
+		imageHeight,
+		width,
+		height,
+		scaleFactor,
+	})
+
+	return { width, height };
 }
 
 const defaultDimensions = {
-    width: 1,
-    height: 1,
+	width: 1,
+	height: 1,
 };
 
-function ImageTransformer({ canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback }) {
-    const { pagerRef, shouldPagerScroll, isScrolling } = useContext(Context);
+function ImageTransformer({ canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap }) {
+	const { pagerRef, shouldPagerScroll, isScrolling } = useContext(Context);
 
-    const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
+	const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
 
-    const [imageDimensions, setImageDimensions] = useState(
-        showFallback
-            ? defaultDimensions
-            : getScaledDimensions({
-                canvasWidth,
-                canvasHeight,
-                imageWidth,
-                imageHeight,
-            }),
-    );
-
-    const canvasX = useSharedValue(canvasWidth);
-    const canvasY = useSharedValue(canvasHeight);
-
-    useEffect(() => {
-        runOnUI(() => {
-            'worklet';
-
-            canvasX.value = canvasWidth;
-            canvasY.value = canvasHeight;
-        })();
-    }, [canvasX, canvasY, canvasWidth, canvasHeight]);
-
-    const targetHeight = useSharedValue(0);
-
-    const onLoad = (resolvedDimensions) => {
-        const { width, height } = getScaledDimensions({
-            canvasWidth,
-            canvasHeight,
-            imageWidth: resolvedDimensions.width,
-            imageHeight: resolvedDimensions.height,
-        });
-
-        targetHeight.value = height;
-
-        setImageDimensions({
-            width,
-            height,
-        });
-
-        setShowFallback(false);
-    };
-
-    // used for pan gesture
-
-    const translateY = useSharedValue(0);
-    const translateX = useSharedValue(0);
-    const offsetX = useSharedValue(0);
-    const offsetY = useSharedValue(0);
-    const isSwiping = useSharedValue(false);
-
-    // used for moving fingers when pinching
-    const scaleTranslateX = useSharedValue(0);
-    const scaleTranslateY = useSharedValue(0);
-
-    // storage for the the origin of the gesture
-    const origin = {
-        x: useSharedValue(0),
-        y: useSharedValue(0),
-    };
-
-    // storage for the pan velocity to calculate the decay
-    const panVelocityX = useSharedValue(0);
-    const panVelocityY = useSharedValue(0);
-
-    const scale = useSharedValue(1);
-    // store scale in between gestures
-    const scaleOffset = useSharedValue(1);
-
-    // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
-
-    // calculates bounds of the scaled image
-    // can we pan left/right/up/down
-    // can be used to limit gesture or implementing tension effect
-    const getBounds = useWorkletCallback(() => {
-        const target = {
-            x: 0,
-            y: 0,
-        };
-
-        const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
-        const scaledImage = targetHeight.value * fixedScale;
-
-        const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
-
-        let topBoundary = 0;
-
-        if (canvasY.value < scaledImage) {
-            topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
-        }
-
-        const maxVector = { x: rightBoundary, y: topBoundary };
-        const minVector = { x: -rightBoundary, y: -topBoundary };
-
-        target.x = clamp(offsetX.value, minVector.x, maxVector.x);
-        target.y = clamp(offsetY.value, minVector.y, maxVector.y);
-
-        const isInBoundaryX = target.x === offsetX.value;
-        const isInBoundaryY = target.y === offsetY.value;
-
-        return {
-            target,
-            isInBoundaryX,
-            isInBoundaryY,
-            minVector,
-            maxVector,
-            canPanLeft: target.x < maxVector.x,
-            canPanRight: target.x > minVector.x,
-        };
-    });
-
-    const afterGesture = useWorkletCallback(() => {
-        const { target, isInBoundaryX, isInBoundaryY, minVector, maxVector } = getBounds();
-
-        if (!canPanVertically.value) {
-            offsetY.value = withSpring(target.y, config);
-        }
-
-        if (
-            scale.value === 1 &&
-            offsetX.value === 0 &&
-            offsetY.value === 0 &&
-            translateX.value === 0 &&
-            translateY.value === 0 &&
-            scaleTranslateX.value === 0 &&
-            scaleTranslateY.value === 0
-        ) {
-            // we don't need to run any animations
-            return;
-        }
-
-        if (scale.value <= 1) {
-            // just center it
-            // reset(true);
-            offsetX.value = withSpring(0, config);
-            offsetY.value = withSpring(0, config);
-            return;
-        }
-
-        const deceleration = 0.9915;
-
-        if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
-                offsetX.value = withDecay({
-                    velocity: panVelocityX.value,
-                    clamp: [minVector.x, maxVector.x],
-                    deceleration,
-                    rubberBandEffect: false,
-                });
-            }
-        } else {
-            offsetX.value = withSpring(target.x, config);
-        }
-
-        if (isInBoundaryY) {
-            if (
-                Math.abs(panVelocityY.value) > 0 &&
-                scale.value <= MAX_SCALE &&
-                // limit vertical pan only when image is smaller than screen
-                offsetY.value !== minVector.y &&
-                offsetY.value !== maxVector.y
-            ) {
-                offsetY.value = withDecay({
-                    velocity: panVelocityY.value,
-                    clamp: [minVector.y, maxVector.y],
-                    deceleration,
-                });
-            }
-        } else {
-            offsetY.value = withSpring(target.y, config, () => {
-                isSwiping.value = false;
-            });
-        }
-    });
-
-    const stopAnimation = useWorkletCallback(() => {
-        cancelAnimation(offsetX);
-        cancelAnimation(offsetY);
-    });
-
-    const zoomToCoordinates = useWorkletCallback((x, y) => {
-        'worklet';
-
-        stopAnimation();
-
-        const usableImage = {
-            x: canvasWidth,
-            y: targetHeight.value,
-        };
-
-        const targetImageSize = {
-            x: usableImage.x * DOUBLE_TAP_SCALE,
-            y: usableImage.y * DOUBLE_TAP_SCALE,
-        };
-
-        const CENTER = {
-            x: canvasX.value / 2,
-            y: canvasY.value / 2,
-        };
-
-        const imageCenter = {
-            x: usableImage.x / 2,
-            y: usableImage.y / 2,
-        };
-
-        const focal = { x, y };
-
-        const currentOrigin = {
-            x: (targetImageSize.x / 2 - CENTER.x) * -1,
-            y: (targetImageSize.y / 2 - CENTER.y) * -1,
-        };
-
-        const koef = {
-            x: (1 / imageCenter.x) * focal.x - 1,
-            y: (1 / imageCenter.y) * focal.y - 1,
-        };
-
-        const target = {
-            x: currentOrigin.x * koef.x,
-            y: currentOrigin.y * koef.y,
-        };
-
-        if (targetImageSize.y < canvasY.value) {
-            target.y = 0;
-        }
-
-        offsetX.value = withSpring(target.x, config);
-        offsetY.value = withSpring(target.y, config);
-        scale.value = withSpring(DOUBLE_TAP_SCALE, config);
-        scaleOffset.value = DOUBLE_TAP_SCALE;
-    });
-
-    const reset = useWorkletCallback((animated) => {
-        scaleOffset.value = 1;
-
-        stopAnimation();
-
-        if (animated) {
-            offsetX.value = withSpring(0, config);
-            offsetY.value = withSpring(0, config);
-            scale.value = withSpring(1, config);
-        } else {
-            scale.value = 1;
-            translateX.value = 0;
-            translateY.value = 0;
-            offsetX.value = 0;
-            offsetY.value = 0;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
-        }
-    });
-
-    const doubleTap = Gesture.Tap()
-        .numberOfTaps(2)
-        .maxDelay(150)
-        .maxDistance(20)
-        .onEnd((evt) => {
-            if (scale.value > 1) {
-                reset(true);
-            } else {
-                zoomToCoordinates(evt.x, evt.y);
-            }
-        });
-
-    const panGestureRef = useRef(Gesture.Pan());
-
-    const singleTap = Gesture.Tap()
-        .numberOfTaps(1)
-        .maxDuration(50)
-        .requireExternalGestureToFail(doubleTap, panGestureRef)
-        .onBegin(() => {
-            stopAnimation();
-        })
-        .onFinalize((evt, success) => {
-            if (success) {
-                console.log('single tap');
-            }
-        });
-
-    const previousTouch = useSharedValue(null);
-
-    const panGesture = Gesture.Pan()
-        .manualActivation(true)
-        .averageTouches(true)
-        .onTouchesMove((evt, state) => {
-            if (scale.value > 1) {
-                state.activate();
-            }
-
-            // this is for swipe down to close
-            // needs fine tuning to work properly
-            // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
-            //     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
-            //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
-
-            //     // this needs tuning
-            //     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
-            //         state.activate();
-
-            //         isSwiping.value = true;
-            //         previousTouch.value = null;
-            //     }
-            // }
-
-            // if (previousTouch.value == null) {
-            //     previousTouch.value = {
-            //         x: evt.allTouches[0].x,
-            //         y: evt.allTouches[0].y,
-            //     };
-            // }
-        })
-        .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
-        .onBegin(() => {
-            stopAnimation();
-        })
-        .onChange((evt) => {
-            // since we running both pinch and pan gesture handlers simultaneously
-            // we need to make sure that we don't pan when we pinch and move fingers
-            // since we track it as pinch focal gesture
-            if (evt.numberOfPointers > 1 || isScrolling.value) {
-                return;
-            }
-
-            panVelocityX.value = evt.velocityX;
-
-            panVelocityY.value = evt.velocityY;
-
-            if (!isSwiping.value) {
-                translateX.value += evt.changeX;
-            }
-
-            if (canPanVertically.value || isSwiping.value) {
-                translateY.value += evt.changeY;
-            }
-        })
-        .onEnd((evt) => {
-            previousTouch.value = null;
-
-            if (isScrolling.value) {
-                return;
-            }
-
-            offsetX.value += translateX.value;
-            offsetY.value += translateY.value;
-            translateX.value = 0;
-            translateY.value = 0;
-
-            if (isSwiping.value) {
-                const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
-                const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
-
-                if (enoughVelocity && rightDirection) {
-                    const maybeInvert = (v) => {
-                        const invert = evt.velocityY < 0;
-                        return invert ? -v : v;
-                    };
-
-                    offsetY.value = withSpring(
-                        maybeInvert(targetHeight.value * 2),
-                        {
-                            stiffness: 50,
-                            damping: 30,
-                            mass: 1,
-                            overshootClamping: true,
-                            restDisplacementThreshold: 300,
-                            restSpeedThreshold: 300,
-                            velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
-                        },
-                        () => {
-                            console.log('success');
-                            runOnJS(onSwipeSuccess)();
-                        },
-                    );
-                    return;
-                }
-            }
-
-            afterGesture();
-
-            panVelocityX.value = 0;
-            panVelocityY.value = 0;
-        })
-        .withRef(panGestureRef);
-
-    const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
-        x: focalX - (canvasX.value / 2 + offsetX.value),
-        y: focalY - (canvasY.value / 2 + offsetY.value),
-    }));
-
-    // used to store event scale value when we limit scale
-    const gestureScale = useSharedValue(1);
-
-    const pinchGesture = Gesture.Pinch()
-        .onTouchesDown((evt, state) => {
-            // we don't want to activate pinch gesture when we are scrolling pager
-            if (isScrolling.value) {
-                state.fail();
-            }
-        })
-        .simultaneousWithExternalGesture(panGesture, doubleTap)
-        .onStart((evt) => {
-            stopAnimation();
-
-            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-            origin.x.value = adjustFocal.x;
-            origin.y.value = adjustFocal.y;
-        })
-        .onChange((evt) => {
-            scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
-
-            if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
-                gestureScale.value = evt.scale;
-            }
-
-            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
-            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
-        })
-        .onEnd(() => {
-            offsetX.value += scaleTranslateX.value;
-            offsetY.value += scaleTranslateY.value;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
-            scaleOffset.value = scale.value;
-            gestureScale.value = 1;
-
-            if (scaleOffset.value < 1) {
-                // make sure we don't add stuff below the 1
-                scaleOffset.value = 1;
-
-                // this runs the timing animation
-                scale.value = withSpring(1, config);
-            } else if (scaleOffset.value > MAX_SCALE) {
-                scaleOffset.value = MAX_SCALE;
-                scale.value = withSpring(MAX_SCALE, config);
-            }
-
-            afterGesture();
-        });
-
-    const animatedStyles = useAnimatedStyle(() => {
-        const x = scaleTranslateX.value + translateX.value + offsetX.value;
-        const y = scaleTranslateY.value + translateY.value + offsetY.value;
-
-        if (isSwiping.value) {
-            onSwipe(y);
-        }
-
-        return {
-            transform: [
-                {
-                    translateX: x,
-                },
-                {
-                    translateY: y,
-                },
-                { scale: scale.value },
-            ],
-        };
-    }, []);
-
-    // reacts to scale change and enables/disables pager scroll
-    useAnimatedReaction(
-        () => scale.value,
-        () => {
-            shouldPagerScroll.value = scale.value === 1;
-        },
-    );
-
-    const mounted = useRef(false);
-    useEffect(() => {
-        if (!mounted.current) {
-            mounted.current = true;
-            return;
-        }
-
-        if (!isActive) {
-            runOnUI(reset)(false);
-        }
-    }, [isActive, mounted, reset]);
-
-    return (
-        <View
-            collapsable={false}
-            style={{
-                flex: 1,
-                width: canvasWidth,
-            }}
-        >
-            <GestureDetector gesture={pinchGesture}>
-                <Animated.View
-                    collapsable
-                    style={StyleSheet.absoluteFill}
-                >
-                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
-                        <Animated.View
-                            collapsable
-                            style={[StyleSheet.absoluteFill]}
-                        >
-                            <ImageWrapper>
-                                <GestureDetector gesture={doubleTap}>
-                                    <Animated.View
-                                        collapsable={false}
-                                        style={[animatedStyles]}
-                                    >
-                                        {showFallback && renderFallback()}
-
-                                        {renderImage({
-                                            onResolveImageDimensions: onLoad,
-                                            ...imageDimensions,
-                                            style: showFallback ? { opacity: 0, position: 'absolute' } : {},
-                                        })}
-                                    </Animated.View>
-                                </GestureDetector>
-                            </ImageWrapper>
-                        </Animated.View>
-                    </GestureDetector>
-                </Animated.View>
-            </GestureDetector>
-        </View>
-    );
+	const [imageDimensions, setImageDimensions] = useState(
+		showFallback
+			? defaultDimensions
+			: getScaledDimensions({
+				canvasWidth,
+				canvasHeight,
+				imageWidth,
+				imageHeight,
+			}),
+	);
+
+	const canvasX = useSharedValue(canvasWidth);
+	const canvasY = useSharedValue(canvasHeight);
+
+	useEffect(() => {
+		runOnUI(() => {
+			'worklet';
+
+			canvasX.value = canvasWidth;
+			canvasY.value = canvasHeight;
+		})();
+	}, [canvasX, canvasY, canvasWidth, canvasHeight]);
+
+	const targetHeight = useSharedValue(0);
+
+	const onLoad = (resolvedDimensions) => {
+		const { width, height } = getScaledDimensions({
+			canvasWidth,
+			canvasHeight,
+			imageWidth: resolvedDimensions.width,
+			imageHeight: resolvedDimensions.height,
+		});
+
+		targetHeight.value = height;
+
+		setImageDimensions({
+			width,
+			height,
+		});
+
+		setShowFallback(false);
+	};
+
+	// used for pan gesture
+
+	const translateY = useSharedValue(0);
+	const translateX = useSharedValue(0);
+	const offsetX = useSharedValue(0);
+	const offsetY = useSharedValue(0);
+	const isSwiping = useSharedValue(false);
+
+	// used for moving fingers when pinching
+	const scaleTranslateX = useSharedValue(0);
+	const scaleTranslateY = useSharedValue(0);
+
+	// storage for the the origin of the gesture
+	const origin = {
+		x: useSharedValue(0),
+		y: useSharedValue(0),
+	};
+
+	// storage for the pan velocity to calculate the decay
+	const panVelocityX = useSharedValue(0);
+	const panVelocityY = useSharedValue(0);
+
+	const scale = useSharedValue(1);
+	// store scale in between gestures
+	const scaleOffset = useSharedValue(1);
+
+	// disable pan vertically when image is smaller than screen
+	const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
+
+	// calculates bounds of the scaled image
+	// can we pan left/right/up/down
+	// can be used to limit gesture or implementing tension effect
+	const getBounds = useWorkletCallback(() => {
+		const target = {
+			x: 0,
+			y: 0,
+		};
+
+		const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
+		const scaledImage = targetHeight.value * fixedScale;
+
+		const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
+
+		let topBoundary = 0;
+
+		if (canvasY.value < scaledImage) {
+			topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
+		}
+
+		const maxVector = { x: rightBoundary, y: topBoundary };
+		const minVector = { x: -rightBoundary, y: -topBoundary };
+
+		target.x = clamp(offsetX.value, minVector.x, maxVector.x);
+		target.y = clamp(offsetY.value, minVector.y, maxVector.y);
+
+		const isInBoundaryX = target.x === offsetX.value;
+		const isInBoundaryY = target.y === offsetY.value;
+
+		return {
+			target,
+			isInBoundaryX,
+			isInBoundaryY,
+			minVector,
+			maxVector,
+			canPanLeft: target.x < maxVector.x,
+			canPanRight: target.x > minVector.x,
+		};
+	});
+
+	const afterGesture = useWorkletCallback(() => {
+		const { target, isInBoundaryX, isInBoundaryY, minVector, maxVector } = getBounds();
+
+		if (!canPanVertically.value) {
+			offsetY.value = withSpring(target.y, config);
+		}
+
+		if (
+			scale.value === 1 &&
+			offsetX.value === 0 &&
+			offsetY.value === 0 &&
+			translateX.value === 0 &&
+			translateY.value === 0 &&
+			scaleTranslateX.value === 0 &&
+			scaleTranslateY.value === 0
+		) {
+			// we don't need to run any animations
+			return;
+		}
+
+		if (scale.value <= 1) {
+			// just center it
+			// reset(true);
+			offsetX.value = withSpring(0, config);
+			offsetY.value = withSpring(0, config);
+			return;
+		}
+
+		const deceleration = 0.9915;
+
+		if (isInBoundaryX) {
+			if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
+				offsetX.value = withDecay({
+					velocity: panVelocityX.value,
+					clamp: [minVector.x, maxVector.x],
+					deceleration,
+					rubberBandEffect: false,
+				});
+			}
+		} else {
+			offsetX.value = withSpring(target.x, config);
+		}
+
+		if (isInBoundaryY) {
+			if (
+				Math.abs(panVelocityY.value) > 0 &&
+				scale.value <= MAX_SCALE &&
+				// limit vertical pan only when image is smaller than screen
+				offsetY.value !== minVector.y &&
+				offsetY.value !== maxVector.y
+			) {
+				offsetY.value = withDecay({
+					velocity: panVelocityY.value,
+					clamp: [minVector.y, maxVector.y],
+					deceleration,
+				});
+			}
+		} else {
+			offsetY.value = withSpring(target.y, config, () => {
+				isSwiping.value = false;
+			});
+		}
+	});
+
+	const stopAnimation = useWorkletCallback(() => {
+		cancelAnimation(offsetX);
+		cancelAnimation(offsetY);
+	});
+
+	const zoomToCoordinates = useWorkletCallback((x, y) => {
+		'worklet';
+
+		stopAnimation();
+
+		const usableImage = {
+			x: canvasWidth,
+			y: targetHeight.value,
+		};
+
+		const targetImageSize = {
+			x: usableImage.x * DOUBLE_TAP_SCALE,
+			y: usableImage.y * DOUBLE_TAP_SCALE,
+		};
+
+		const CENTER = {
+			x: canvasX.value / 2,
+			y: canvasY.value / 2,
+		};
+
+		const imageCenter = {
+			x: usableImage.x / 2,
+			y: usableImage.y / 2,
+		};
+
+		const focal = { x, y };
+
+		const currentOrigin = {
+			x: (targetImageSize.x / 2 - CENTER.x) * -1,
+			y: (targetImageSize.y / 2 - CENTER.y) * -1,
+		};
+
+		const koef = {
+			x: (1 / imageCenter.x) * focal.x - 1,
+			y: (1 / imageCenter.y) * focal.y - 1,
+		};
+
+		const target = {
+			x: currentOrigin.x * koef.x,
+			y: currentOrigin.y * koef.y,
+		};
+
+		if (targetImageSize.y < canvasY.value) {
+			target.y = 0;
+		}
+
+		offsetX.value = withSpring(target.x, config);
+		offsetY.value = withSpring(target.y, config);
+		scale.value = withSpring(DOUBLE_TAP_SCALE, config);
+		scaleOffset.value = DOUBLE_TAP_SCALE;
+	});
+
+	const reset = useWorkletCallback((animated) => {
+		scaleOffset.value = 1;
+
+		stopAnimation();
+
+		if (animated) {
+			offsetX.value = withSpring(0, config);
+			offsetY.value = withSpring(0, config);
+			scale.value = withSpring(1, config);
+		} else {
+			scale.value = 1;
+			translateX.value = 0;
+			translateY.value = 0;
+			offsetX.value = 0;
+			offsetY.value = 0;
+			scaleTranslateX.value = 0;
+			scaleTranslateY.value = 0;
+		}
+	});
+
+	const doubleTap = Gesture.Tap()
+		.numberOfTaps(2)
+		.maxDelay(150)
+		.maxDistance(20)
+		.onEnd((evt) => {
+			if (scale.value > 1) {
+				reset(true);
+			} else {
+				zoomToCoordinates(evt.x, evt.y);
+			}
+		});
+
+	const panGestureRef = useRef(Gesture.Pan());
+
+	const singleTap = Gesture.Tap()
+		.numberOfTaps(1)
+		.maxDuration(50)
+		.requireExternalGestureToFail(doubleTap, panGestureRef)
+		.onBegin(() => {
+			stopAnimation();
+		})
+		.onFinalize((evt, success) => {
+			if (success && onTap) {
+				runOnJS(onTap)();
+			}
+		});
+
+	const previousTouch = useSharedValue(null);
+
+	const panGesture = Gesture.Pan()
+		.manualActivation(true)
+		.averageTouches(true)
+		.onTouchesMove((evt, state) => {
+			if (scale.value > 1) {
+				state.activate();
+			}
+
+			// this is for swipe down to close
+			// needs fine tuning to work properly
+			// if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
+			//     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
+			//     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
+
+			//     // this needs tuning
+			//     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
+			//         state.activate();
+
+			//         isSwiping.value = true;
+			//         previousTouch.value = null;
+			//     }
+			// }
+
+			// if (previousTouch.value == null) {
+			//     previousTouch.value = {
+			//         x: evt.allTouches[0].x,
+			//         y: evt.allTouches[0].y,
+			//     };
+			// }
+		})
+		.simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
+		.onBegin(() => {
+			stopAnimation();
+		})
+		.onChange((evt) => {
+			// since we running both pinch and pan gesture handlers simultaneously
+			// we need to make sure that we don't pan when we pinch and move fingers
+			// since we track it as pinch focal gesture
+			if (evt.numberOfPointers > 1 || isScrolling.value) {
+				return;
+			}
+
+			panVelocityX.value = evt.velocityX;
+
+			panVelocityY.value = evt.velocityY;
+
+			if (!isSwiping.value) {
+				translateX.value += evt.changeX;
+			}
+
+			if (canPanVertically.value || isSwiping.value) {
+				translateY.value += evt.changeY;
+			}
+		})
+		.onEnd((evt) => {
+			previousTouch.value = null;
+
+			if (isScrolling.value) {
+				return;
+			}
+
+			offsetX.value += translateX.value;
+			offsetY.value += translateY.value;
+			translateX.value = 0;
+			translateY.value = 0;
+
+			if (isSwiping.value) {
+				const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
+				const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
+
+				if (enoughVelocity && rightDirection) {
+					const maybeInvert = (v) => {
+						const invert = evt.velocityY < 0;
+						return invert ? -v : v;
+					};
+
+					offsetY.value = withSpring(
+						maybeInvert(targetHeight.value * 2),
+						{
+							stiffness: 50,
+							damping: 30,
+							mass: 1,
+							overshootClamping: true,
+							restDisplacementThreshold: 300,
+							restSpeedThreshold: 300,
+							velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
+						},
+						() => {
+							console.log('success');
+							runOnJS(onSwipeSuccess)();
+						},
+					);
+					return;
+				}
+			}
+
+			afterGesture();
+
+			panVelocityX.value = 0;
+			panVelocityY.value = 0;
+		})
+		.withRef(panGestureRef);
+
+	const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
+		x: focalX - (canvasX.value / 2 + offsetX.value),
+		y: focalY - (canvasY.value / 2 + offsetY.value),
+	}));
+
+	// used to store event scale value when we limit scale
+	const gestureScale = useSharedValue(1);
+
+	const pinchGesture = Gesture.Pinch()
+		.onTouchesDown((evt, state) => {
+			// we don't want to activate pinch gesture when we are scrolling pager
+			if (isScrolling.value) {
+				state.fail();
+			}
+		})
+		.simultaneousWithExternalGesture(panGesture, doubleTap)
+		.onStart((evt) => {
+			stopAnimation();
+
+			const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+			origin.x.value = adjustFocal.x;
+			origin.y.value = adjustFocal.y;
+		})
+		.onChange((evt) => {
+			scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+
+			if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
+				gestureScale.value = evt.scale;
+			}
+
+			const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+			scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
+			scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
+		})
+		.onEnd(() => {
+			offsetX.value += scaleTranslateX.value;
+			offsetY.value += scaleTranslateY.value;
+			scaleTranslateX.value = 0;
+			scaleTranslateY.value = 0;
+			scaleOffset.value = scale.value;
+			gestureScale.value = 1;
+
+			if (scaleOffset.value < 1) {
+				// make sure we don't add stuff below the 1
+				scaleOffset.value = 1;
+
+				// this runs the timing animation
+				scale.value = withSpring(1, config);
+			} else if (scaleOffset.value > MAX_SCALE) {
+				scaleOffset.value = MAX_SCALE;
+				scale.value = withSpring(MAX_SCALE, config);
+			}
+
+			afterGesture();
+		});
+
+	const animatedStyles = useAnimatedStyle(() => {
+		const x = scaleTranslateX.value + translateX.value + offsetX.value;
+		const y = scaleTranslateY.value + translateY.value + offsetY.value;
+
+		if (isSwiping.value) {
+			onSwipe(y);
+		}
+
+		return {
+			transform: [
+				{
+					translateX: x,
+				},
+				{
+					translateY: y,
+				},
+				{ scale: scale.value },
+			],
+		};
+	}, []);
+
+	// reacts to scale change and enables/disables pager scroll
+	useAnimatedReaction(
+		() => scale.value,
+		() => {
+			shouldPagerScroll.value = scale.value === 1;
+		},
+	);
+
+	const mounted = useRef(false);
+	useEffect(() => {
+		if (!mounted.current) {
+			mounted.current = true;
+			return;
+		}
+
+		if (!isActive) {
+			runOnUI(reset)(false);
+		}
+	}, [isActive, mounted, reset]);
+
+	return (
+		<View
+			collapsable={false}
+			style={{
+				flex: 1,
+				width: canvasWidth,
+			}}
+		>
+			<GestureDetector gesture={pinchGesture}>
+				<Animated.View
+					collapsable
+					style={StyleSheet.absoluteFill}
+				>
+					<GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
+						<Animated.View
+							collapsable
+							style={[StyleSheet.absoluteFill]}
+						>
+							<ImageWrapper>
+								<GestureDetector gesture={doubleTap}>
+									<Animated.View
+										collapsable={false}
+										style={[animatedStyles]}
+									>
+										{showFallback && renderFallback()}
+
+										{renderImage({
+											onResolveImageDimensions: onLoad,
+											...imageDimensions,
+											style: showFallback ? { opacity: 0, position: 'absolute' } : {},
+										})}
+									</Animated.View>
+								</GestureDetector>
+							</ImageWrapper>
+						</Animated.View>
+					</GestureDetector>
+				</Animated.View>
+			</GestureDetector>
+		</View>
+	);
 }
 
 function ImageWrapper({ children }) {
-    return (
-        <Animated.View
-            collapsable={false}
-            style={[
-                {
-                    ...StyleSheet.absoluteFillObject,
-                    justifyContent: 'center',
-                    alignItems: 'center',
-                },
-            ]}
-        >
-            {children}
-        </Animated.View>
-    );
+	return (
+		<Animated.View
+			collapsable={false}
+			style={[
+				{
+					...StyleSheet.absoluteFillObject,
+					justifyContent: 'center',
+					alignItems: 'center',
+				},
+			]}
+		>
+			{children}
+		</Animated.View>
+	);
 }
 
 const cachedDimensions = new Map();
 
-function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight }) {
-    console.log
-
-    const dimensions = cachedDimensions.get(item.url);
-
-    if (!isActive) {
-        return (
-            <ImageWrapper>
-                <Image
-                    source={{ uri: item.url }}
-                    onLoad={(evt) => {
-
-
-                        cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent?.width,
-                            height: evt.nativeEvent?.height,
-                        });
-                    }}
-                    style={
-                        dimensions
-                            ? getScaledDimensions({
-                                imageHeight: dimensions?.height,
-                                imageWidth: dimensions?.width,
-                                canvasHeight,
-                                canvasWidth,
-                            })
-                            : {}
-                    }
-                />
-            </ImageWrapper>
-        );
-    }
-
-    return (
-        <ImageTransformer
-            onSwipe={onSwipe}
-            onSwipeSuccess={onSwipeSuccess}
-            isActive
-            imageHeight={dimensions?.height}
-            imageWidth={dimensions?.width}
-            canvasHeight={canvasHeight}
-            canvasWidth={canvasWidth}
-            renderFallback={() => <ActivityIndicator />}
-            renderImage={({ onResolveImageDimensions, width, height, style }) => (
-                <Image
-                    source={{ uri: item.url, }}
-                    style={[style, { width, height }]}
-                    onLoad={(evt) => {
-                        cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent.width,
-                            height: evt.nativeEvent.height,
-                        });
-
-
-                        onResolveImageDimensions({
-                            width: evt.nativeEvent.width,
-                            height: evt.nativeEvent.height,
-                        });
-                    }}
-                />
-            )}
-        />
-    );
+function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight, onTap }) {
+	const dimensions = cachedDimensions.get(item.url);
+
+	if (!isActive) {
+		return (
+			<ImageWrapper>
+				<Image
+					source={{ uri: item.url }}
+					onLoad={(evt) => {
+
+
+						cachedDimensions.set(item.url, {
+							width: evt.nativeEvent?.width,
+							height: evt.nativeEvent?.height,
+						});
+					}}
+					style={
+						dimensions
+							? getScaledDimensions({
+								imageHeight: dimensions?.height,
+								imageWidth: dimensions?.width,
+								canvasHeight,
+								canvasWidth,
+							})
+							: {}
+					}
+				/>
+			</ImageWrapper>
+		);
+	}
+
+	return (
+		<ImageTransformer
+			onSwipe={onSwipe}
+			onSwipeSuccess={onSwipeSuccess}
+			isActive
+			onTap={onTap}
+			imageHeight={dimensions?.height}
+			imageWidth={dimensions?.width}
+			canvasHeight={canvasHeight}
+			canvasWidth={canvasWidth}
+			renderFallback={() => <ActivityIndicator />}
+			renderImage={({ onResolveImageDimensions, width, height, style }) => (
+				<Image
+					source={{ uri: item.url, width: 100, height: 100 }}
+					style={[style, { width, height }]}
+					onLoad={(evt) => {
+						cachedDimensions.set(item.url, {
+							width: evt.nativeEvent.width,
+							height: evt.nativeEvent.height,
+						});
+
+
+						onResolveImageDimensions({
+							width: evt.nativeEvent.width / PixelRatio.get(),
+							height: evt.nativeEvent.height / PixelRatio.get(),
+						});
+					}}
+				/>
+			)}
+		/>
+	);
 }
 
 const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
 
 function usePageScrollHandler(handlers, dependencies) {
-    const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
-    const subscribeForEvents = ['onPageScroll'];
-
-    return useEvent(
-        (event) => {
-            'worklet';
-
-            const { onPageScroll } = handlers;
-            if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
-                onPageScroll(event, context);
-            }
-        },
-        subscribeForEvents,
-        doDependenciesDiffer,
-    );
+	const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
+	const subscribeForEvents = ['onPageScroll'];
+
+	return useEvent(
+		(event) => {
+			'worklet';
+
+			const { onPageScroll } = handlers;
+			if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
+				onPageScroll(event, context);
+			}
+		},
+		subscribeForEvents,
+		doDependenciesDiffer,
+	);
 }
 
 const noopWorklet = () => {
-    'worklet';
+	'worklet';
 
-    // noop
+	// noop
 };
 
-export default function Pager({ items, initialIndex = 0, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => { } }) {
-    const windowDimensions = useWindowDimensions();
-
-    const shouldPagerScroll = useSharedValue(true);
-    const pagerRef = useRef(null);
-
-    const isScrolling = useSharedValue(false);
-    const activeIndex = useSharedValue(initialIndex);
-
-    const pageScrollHandler = usePageScrollHandler(
-        {
-            onPageScroll: (e) => {
-                'worklet';
-
-                activeIndex.value = e.position;
-                isScrolling.value = e.offset !== 0;
-            },
-        },
-        [],
-    );
-
-    const [activePage, setActivePage] = useState(initialIndex);
-
-    // we use reanimated for this since onPageSelected is called
-    // in the middle of the pager animation
-    useAnimatedReaction(
-        () => isScrolling.value,
-        (stillScrolling) => {
-            if (stillScrolling) {
-                return;
-            }
-
-            runOnJS(setActivePage)(activeIndex.value);
-        },
-    );
-
-    const animatedProps = useAnimatedProps(() => ({
-        scrollEnabled: shouldPagerScroll.value,
-    }));
-
-    return (
-        <View style={{ flex: 1 }}>
-            <View style={{ flex: 1 }}>
-                <Context.Provider
-                    value={{
-                        isScrolling,
-                        pagerRef,
-                        shouldPagerScroll,
-                    }}
-                >
-                    <AnimatedPagerView
-                        pageMargin={40}
-                        onPageScroll={pageScrollHandler}
-                        animatedProps={animatedProps}
-                        ref={pagerRef}
-                        style={{ flex: 1 }}
-                        initialPage={initialIndex}
-                    >
-                        {items
-                            .map((item, index) => itemExtractor({ item, index }))
-                            .map((item, index) => (
-                                <View
-                                    key={item.key}
-                                    style={{ flex: 1 }}
-                                >
-                                    <Page
-                                        onSwipe={onSwipe}
-                                        onSwipeSuccess={onSwipeSuccess}
-                                        isActive={index === activePage}
-                                        item={item}
-                                        canvasHeight={windowDimensions.height}
-                                        canvasWidth={windowDimensions.width}
-                                    />
-                                </View>
-                            ))}
-                    </AnimatedPagerView>
-                </Context.Provider>
-            </View>
-        </View>
-    );
+export default function Pager({ items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => { } }) {
+	const windowDimensions = useWindowDimensions();
+
+	const shouldPagerScroll = useSharedValue(true);
+	const pagerRef = useRef(null);
+
+	const isScrolling = useSharedValue(false);
+	const activeIndex = useSharedValue(initialIndex);
+
+	const pageScrollHandler = usePageScrollHandler(
+		{
+			onPageScroll: (e) => {
+				'worklet';
+
+				activeIndex.value = e.position;
+				isScrolling.value = e.offset !== 0;
+			},
+		},
+		[],
+	);
+
+	const [activePage, setActivePage] = useState(initialIndex);
+
+	// we use reanimated for this since onPageSelected is called
+	// in the middle of the pager animation
+	useAnimatedReaction(
+		() => isScrolling.value,
+		(stillScrolling) => {
+			if (stillScrolling) {
+				return;
+			}
+
+			runOnJS(setActivePage)(activeIndex.value);
+		},
+	);
+
+	const animatedProps = useAnimatedProps(() => ({
+		scrollEnabled: shouldPagerScroll.value,
+	}));
+
+	const processedItems = items
+		.map((item, index) => itemExtractor({ item, index }))
+
+	return (
+		<View style={{ flex: 1 }}>
+			<View style={{ flex: 1 }}>
+				<Context.Provider
+					value={{
+						isScrolling,
+						pagerRef,
+						shouldPagerScroll,
+					}}
+				>
+					<AnimatedPagerView
+						pageMargin={40}
+						onPageScroll={pageScrollHandler}
+						animatedProps={animatedProps}
+						ref={pagerRef}
+						style={{ flex: 1 }}
+						initialPage={initialIndex}
+					>
+						{
+							processedItems.map((item, index) => (
+								<View
+									key={item.key}
+									style={{ flex: 1 }}
+								>
+									<Page
+										onTap={onTap}
+										onSwipe={onSwipe}
+										onSwipeSuccess={onSwipeSuccess}
+										isActive={index === activePage}
+										item={item}
+										canvasHeight={windowDimensions.height}
+										canvasWidth={windowDimensions.width}
+									/>
+								</View>
+							))}
+					</AnimatedPagerView>
+				</Context.Provider>
+			</View>
+		</View>
+	);
 }
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index d16458e88e8c..8cefaf45214e 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -23,375 +23,370 @@ import withWindowDimensions from '../withWindowDimensions';
 import Pager from './Lightbox';
 
 const propTypes = {
-    /** source is used to determine the starting index in the array of attachments */
-    source: PropTypes.string,
+	/** source is used to determine the starting index in the array of attachments */
+	source: PropTypes.string,
 
-    /** Callback to update the parent modal's state with a source and name from the attachments array */
-    onNavigate: PropTypes.func,
+	/** Callback to update the parent modal's state with a source and name from the attachments array */
+	onNavigate: PropTypes.func,
 
-    /** Object of report actions for this report */
-    reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+	/** Object of report actions for this report */
+	reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
 
-    ...withLocalizePropTypes,
+	...withLocalizePropTypes,
 };
 
 const defaultProps = {
-    source: '',
-    reportActions: {},
-    onNavigate: () => { },
+	source: '',
+	reportActions: {},
+	onNavigate: () => { },
 };
 
 class AttachmentCarousel extends React.Component {
-    constructor(props) {
-        super(props);
-
-        this.scrollRef = React.createRef();
-        this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
-        this.viewabilityConfig = {
-            // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-            // more than 90% visible. When that happens we update the page index in the state.
-            itemVisiblePercentThreshold: 95,
-        };
-
-        this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
-        this.autoHideArrow = this.autoHideArrow.bind(this);
-        this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
-        this.getItemLayout = this.getItemLayout.bind(this);
-        this.renderItem = this.renderItem.bind(this);
-        this.renderCell = this.renderCell.bind(this);
-        this.updatePage = this.updatePage.bind(this);
-        this.updateZoomState = this.updateZoomState.bind(this);
-        this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
-
-        this.state = this.makeInitialState();
-    }
-
-    componentDidMount() {
-        this.autoHideArrow();
-    }
-
-    /**
-     * Helps to navigate between next/previous attachments
-     * @param {Object} attachmentItem
-     * @returns {Object}
-     */
-    getAttachment(attachmentItem) {
-        const source = _.get(attachmentItem, 'source', '');
-        const file = _.get(attachmentItem, 'file', { name: '' });
-
-        return {
-            source,
-            file,
-        };
-    }
-
-    /**
-     * Calculate items layout information to optimize scrolling performance
-     * @param {*} data
-     * @param {Number} index
-     * @returns {{offset: Number, length: Number, index: Number}}
-     */
-    getItemLayout(data, index) {
-        return {
-            length: this.state.containerWidth,
-            offset: this.state.containerWidth * index,
-            index,
-        };
-    }
-
-    /**
-     * On a touch screen device, automatically hide the arrows
-     * if there is no interaction for 3 seconds.
-     */
-    autoHideArrow() {
-        if (!this.canUseTouchScreen) {
-            return;
-        }
-        this.cancelAutoHideArrow();
-        this.autoHideArrowTimeout = setTimeout(() => {
-            this.toggleArrowsVisibility(false);
-        }, CONST.ARROW_HIDE_DELAY);
-    }
-
-    /**
-     * Cancels the automatic hiding of the arrows.
-     */
-    cancelAutoHideArrow() {
-        clearTimeout(this.autoHideArrowTimeout);
-    }
-
-    /**
-     * Toggles the visibility of the arrows
-     * @param {Boolean} shouldShowArrow
-     */
-    toggleArrowsVisibility(shouldShowArrow) {
-        // Don't toggle arrows in a zoomed state
-        if (this.state.isZoomed) {
-            return;
-        }
-        this.setState(
-            (current) => {
-                const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-                return { shouldShowArrow: newShouldShowArrow };
-            },
-            () => {
-                if (this.state.shouldShowArrow) {
-                    this.autoHideArrow();
-                } else {
-                    this.cancelAutoHideArrow();
-                }
-            },
-        );
-    }
-
-    /**
-     * Updates zoomed state to enable/disable panning the PDF
-     * @param {Number} scale current PDF scale
-     */
-    updateZoomState(scale) {
-        const isZoomed = scale > 1;
-        if (isZoomed === this.state.isZoomed) {
-            return;
-        }
-        if (isZoomed) {
-            this.toggleArrowsVisibility(false);
-        }
-        this.setState({ isZoomed });
-    }
-
-    /**
-     * Map report actions to attachment items and sets the initial carousel state
-     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
-     */
-    makeInitialState() {
-        let page = 0;
-        const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
-
-        /**
-         * Looping to filter out attachments and retrieve the src URL and name of attachments.
-         */
-        const attachments = [];
-        _.forEach(actions, ({ originalMessage, message }) => {
-            // Check for attachment which hasn't been deleted
-            if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
-                return;
-            }
-            const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
-
-            // matchAll captured both source url and name of the attachment
-            if (matches.length === 2) {
-                const [originalSource, name] = _.map(matches, (m) => m[2]);
-
-                // Update the image URL so the images can be accessed depending on the config environment.
-                // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
-                const source = tryResolveUrlFromApiRoot(originalSource);
-                if (source === this.props.source) {
-                    page = attachments.length;
-                }
-
-                attachments.push({ source, file: { name } });
-            }
-        });
-
-        return {
-            page,
-            attachments,
-            shouldShowArrow: this.canUseTouchScreen,
-            containerWidth: 0,
-            isZoomed: false,
-        };
-    }
-
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    cycleThroughAttachments(deltaSlide) {
-        const nextIndex = this.state.page - deltaSlide;
-        const nextItem = this.state.attachments[nextIndex];
-
-        if (!nextItem) {
-            return;
-        }
-
-        // The sliding transition is a bit too much on web, because of the wider and bigger images,
-        // so we only enable it for mobile
-        this.scrollRef.current.scrollToIndex({ index: nextIndex, animated: this.canUseTouchScreen });
-    }
-
-    /**
-     * Updates the page state when the user navigates between attachments
-     * @param {Array<{item: *, index: Number}>} viewableItems
-     */
-    updatePage({ viewableItems }) {
-        // Since we can have only one item in view at a time, we can use the first item in the array
-        // to get the index of the current page
-        const entry = _.first(viewableItems);
-        if (!entry) {
-            return;
-        }
-
-        const page = entry.index;
-        const { source, file } = this.getAttachment(entry.item);
-        this.props.onNavigate({ source: addEncryptedAuthTokenToURL(source), file });
-        this.setState({ page, isZoomed: false });
-    }
-
-    /**
-     * Defines how a container for a single attachment should be rendered
-     * @param {Object} props
-     * @returns {JSX.Element}
-     */
-    renderCell(props) {
-        const style = [props.style, styles.h100, { width: this.state.containerWidth }];
-
-        return (
-            <View
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-                style={style}
-            />
-        );
-    }
-
-    /**
-     * Defines how a single attachment should be rendered
-     * @param {{ source: String, file: { name: String } }} item
-     * @returns {JSX.Element}
-     */
-    renderItem({ item }) {
-        const authSource = addEncryptedAuthTokenToURL(item.source);
-        if (!this.canUseTouchScreen) {
-            return (
-                <AttachmentView
-                    source={authSource}
-                    file={item.file}
-                />
-            );
-        }
-
-        return (
-            <AttachmentView
-                source={authSource}
-                file={item.file}
-                onScaleChanged={this.updateZoomState}
-                onPress={this.toggleArrowsVisibility}
-            />
-        );
-    }
-
-    render() {
-        const isForwardDisabled = this.state.page === 0;
-        const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
-
-        return (
-            <View
-                style={[styles.attachmentModalArrowsContainer, styles.flex1]}
-                onLayout={({ nativeEvent }) => this.setState({ containerWidth: nativeEvent.layout.width + 1 })}
-                onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
-                onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
-            >
-                {this.state.shouldShowArrow && (
-                    <>
-                        {!isBackDisabled && (
-                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
-                                <Tooltip text={this.props.translate('common.previous')}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.BackArrow}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(-1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </Tooltip>
-                            </View>
-                        )}
-                        {!isForwardDisabled && (
-                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
-                                <Tooltip text={this.props.translate('common.next')}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.ArrowRight}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </Tooltip>
-                            </View>
-                        )}
-                    </>
-                )}
-
-                <Pager
-                    items={this.state.attachments}
-                    initialIndex={this.state.page}
-                    itemExtractor={({ item }) => ({ key: item.source, url: addEncryptedAuthTokenToURL(item.source) })}
-                />
-
-                {/* {this.state.containerWidth > 0 && (
-                    <FlatList
-                        listKey="AttachmentCarousel"
-                        horizontal
-                        // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-                        // promotes the natural feeling of swiping left/right to go to the next/previous image
-                        // We don't want to invert the list for desktop/web because this interferes with mouse
-                        // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
-                        inverted={this.canUseTouchScreen}
-                        decelerationRate="fast"
-                        showsHorizontalScrollIndicator={false}
-                        bounces={false}
-                        // Scroll only one image at a time no matter how fast the user swipes
-                        disableIntervalMomentum
-                        pagingEnabled
-                        snapToAlignment="start"
-                        snapToInterval={this.state.containerWidth}
-                        // Enable scrolling by swiping on mobile (touch) devices only
-                        // disable scroll for desktop/browsers because they add their scrollbars
-                        // Enable scrolling FlatList only when PDF is not in a zoomed state
-                        scrollEnabled={this.canUseTouchScreen && !this.state.isZoomed}
-                        ref={this.scrollRef}
-                        initialScrollIndex={this.state.page}
-                        initialNumToRender={3}
-                        windowSize={5}
-                        maxToRenderPerBatch={3}
-                        data={this.state.attachments}
-                        CellRendererComponent={this.renderCell}
-                        renderItem={this.renderItem}
-                        getItemLayout={this.getItemLayout}
-                        keyExtractor={(item) => item.source}
-                        viewabilityConfig={this.viewabilityConfig}
-                        onViewableItemsChanged={this.updatePage}
-                    />
-                )} */}
-
-                <CarouselActions onCycleThroughAttachments={this.cycleThroughAttachments} />
-            </View>
-        );
-    }
+	constructor(props) {
+		super(props);
+
+		this.scrollRef = React.createRef();
+		this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
+		this.viewabilityConfig = {
+			// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+			// more than 90% visible. When that happens we update the page index in the state.
+			itemVisiblePercentThreshold: 95,
+		};
+
+		this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
+		this.autoHideArrow = this.autoHideArrow.bind(this);
+		this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
+		this.getItemLayout = this.getItemLayout.bind(this);
+		this.renderItem = this.renderItem.bind(this);
+		this.renderCell = this.renderCell.bind(this);
+		this.updatePage = this.updatePage.bind(this);
+		this.updateZoomState = this.updateZoomState.bind(this);
+		this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
+
+		this.state = this.makeInitialState();
+	}
+
+	componentDidMount() {
+		this.autoHideArrow();
+	}
+
+	/**
+	 * Helps to navigate between next/previous attachments
+	 * @param {Object} attachmentItem
+	 * @returns {Object}
+	 */
+	getAttachment(attachmentItem) {
+		const source = _.get(attachmentItem, 'source', '');
+		const file = _.get(attachmentItem, 'file', { name: '' });
+
+		return {
+			source,
+			file,
+		};
+	}
+
+	/**
+	 * Calculate items layout information to optimize scrolling performance
+	 * @param {*} data
+	 * @param {Number} index
+	 * @returns {{offset: Number, length: Number, index: Number}}
+	 */
+	getItemLayout(data, index) {
+		return {
+			length: this.state.containerWidth,
+			offset: this.state.containerWidth * index,
+			index,
+		};
+	}
+
+	/**
+	 * On a touch screen device, automatically hide the arrows
+	 * if there is no interaction for 3 seconds.
+	 */
+	autoHideArrow() {
+		if (!this.canUseTouchScreen) {
+			return;
+		}
+		this.cancelAutoHideArrow();
+		this.autoHideArrowTimeout = setTimeout(() => {
+			this.toggleArrowsVisibility(false);
+		}, CONST.ARROW_HIDE_DELAY);
+	}
+
+	/**
+	 * Cancels the automatic hiding of the arrows.
+	 */
+	cancelAutoHideArrow() {
+		clearTimeout(this.autoHideArrowTimeout);
+	}
+
+	/**
+	 * Toggles the visibility of the arrows
+	 * @param {Boolean} shouldShowArrow
+	 */
+	toggleArrowsVisibility(shouldShowArrow) {
+		// Don't toggle arrows in a zoomed state
+		if (this.state.isZoomed) {
+			return;
+		}
+		this.setState(
+			(current) => {
+				const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
+				return { shouldShowArrow: newShouldShowArrow };
+			},
+			() => {
+				if (this.state.shouldShowArrow) {
+					this.autoHideArrow();
+				} else {
+					this.cancelAutoHideArrow();
+				}
+			},
+		);
+	}
+
+	/**
+	 * Updates zoomed state to enable/disable panning the PDF
+	 * @param {Number} scale current PDF scale
+	 */
+	updateZoomState(scale) {
+		const isZoomed = scale > 1;
+		if (isZoomed === this.state.isZoomed) {
+			return;
+		}
+		if (isZoomed) {
+			this.toggleArrowsVisibility(false);
+		}
+		this.setState({ isZoomed });
+	}
+
+	/**
+	 * Map report actions to attachment items and sets the initial carousel state
+	 * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
+	 */
+	makeInitialState() {
+		let page = 0;
+		const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
+
+		/**
+		 * Looping to filter out attachments and retrieve the src URL and name of attachments.
+		 */
+		const attachments = [];
+		_.forEach(actions, ({ originalMessage, message }) => {
+			// Check for attachment which hasn't been deleted
+			if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
+				return;
+			}
+			const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
+
+			// matchAll captured both source url and name of the attachment
+			if (matches.length === 2) {
+				const [originalSource, name] = _.map(matches, (m) => m[2]);
+
+				// Update the image URL so the images can be accessed depending on the config environment.
+				// Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
+				const source = tryResolveUrlFromApiRoot(originalSource);
+				if (source === this.props.source) {
+					page = attachments.length;
+				}
+
+				attachments.push({ source, file: { name } });
+			}
+		});
+
+		return {
+			page,
+			attachments,
+			shouldShowArrow: this.canUseTouchScreen,
+			containerWidth: 0,
+			isZoomed: false,
+		};
+	}
+
+	/**
+	 * Increments or decrements the index to get another selected item
+	 * @param {Number} deltaSlide
+	 */
+	cycleThroughAttachments(deltaSlide) {
+		const nextIndex = this.state.page - deltaSlide;
+		const nextItem = this.state.attachments[nextIndex];
+
+		if (!nextItem) {
+			return;
+		}
+
+		// The sliding transition is a bit too much on web, because of the wider and bigger images,
+		// so we only enable it for mobile
+		this.scrollRef.current.scrollToIndex({ index: nextIndex, animated: this.canUseTouchScreen });
+	}
+
+	/**
+	 * Updates the page state when the user navigates between attachments
+	 * @param {Array<{item: *, index: Number}>} viewableItems
+	 */
+	updatePage({ viewableItems }) {
+		// Since we can have only one item in view at a time, we can use the first item in the array
+		// to get the index of the current page
+		const entry = _.first(viewableItems);
+		if (!entry) {
+			return;
+		}
+
+		const page = entry.index;
+		const { source, file } = this.getAttachment(entry.item);
+		this.props.onNavigate({ source: addEncryptedAuthTokenToURL(source), file });
+		this.setState({ page, isZoomed: false });
+	}
+
+	/**
+	 * Defines how a container for a single attachment should be rendered
+	 * @param {Object} props
+	 * @returns {JSX.Element}
+	 */
+	renderCell(props) {
+		const style = [props.style, styles.h100, { width: this.state.containerWidth }];
+
+		return (
+			<View
+				// eslint-disable-next-line react/jsx-props-no-spreading
+				{...props}
+				style={style}
+			/>
+		);
+	}
+
+	/**
+	 * Defines how a single attachment should be rendered
+	 * @param {{ source: String, file: { name: String } }} item
+	 * @returns {JSX.Element}
+	 */
+	renderItem({ item }) {
+		const authSource = addEncryptedAuthTokenToURL(item.source);
+		if (!this.canUseTouchScreen) {
+			return (
+				<AttachmentView
+					source={authSource}
+					file={item.file}
+				/>
+			);
+		}
+
+		return (
+			<AttachmentView
+				source={authSource}
+				file={item.file}
+				onScaleChanged={this.updateZoomState}
+				onPress={this.toggleArrowsVisibility}
+			/>
+		);
+	}
+
+	render() {
+		const isForwardDisabled = this.state.page === 0;
+		const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
+
+		return (
+			<View
+				style={[styles.attachmentModalArrowsContainer, styles.flex1]}
+				onLayout={({ nativeEvent }) => this.setState({ containerWidth: nativeEvent.layout.width + 1 })}
+				onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
+				onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
+			>
+				{this.state.shouldShowArrow && (
+					<>
+						{!isBackDisabled && (
+							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
+								<Tooltip text={this.props.translate('common.previous')}>
+									<Button
+										small
+										innerStyles={[styles.arrowIcon]}
+										icon={Expensicons.BackArrow}
+										iconFill={themeColors.text}
+										iconStyles={[styles.mr0]}
+										onPress={() => {
+											this.cycleThroughAttachments(-1);
+											this.autoHideArrow();
+										}}
+										onPressIn={this.cancelAutoHideArrow}
+										onPressOut={this.autoHideArrow}
+									/>
+								</Tooltip>
+							</View>
+						)}
+						{!isForwardDisabled && (
+							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
+								<Tooltip text={this.props.translate('common.next')}>
+									<Button
+										small
+										innerStyles={[styles.arrowIcon]}
+										icon={Expensicons.ArrowRight}
+										iconFill={themeColors.text}
+										iconStyles={[styles.mr0]}
+										onPress={() => {
+											this.cycleThroughAttachments(1);
+											this.autoHideArrow();
+										}}
+										onPressIn={this.cancelAutoHideArrow}
+										onPressOut={this.autoHideArrow}
+									/>
+								</Tooltip>
+							</View>
+						)}
+					</>
+				)}
+
+
+				{this.state.containerWidth > 0 && (
+					<FlatList
+						listKey="AttachmentCarousel"
+						horizontal
+						// Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
+						// promotes the natural feeling of swiping left/right to go to the next/previous image
+						// We don't want to invert the list for desktop/web because this interferes with mouse
+						// wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
+						inverted={this.canUseTouchScreen}
+						decelerationRate="fast"
+						showsHorizontalScrollIndicator={false}
+						bounces={false}
+						// Scroll only one image at a time no matter how fast the user swipes
+						disableIntervalMomentum
+						pagingEnabled
+						snapToAlignment="start"
+						snapToInterval={this.state.containerWidth}
+						// Enable scrolling by swiping on mobile (touch) devices only
+						// disable scroll for desktop/browsers because they add their scrollbars
+						// Enable scrolling FlatList only when PDF is not in a zoomed state
+						scrollEnabled={this.canUseTouchScreen && !this.state.isZoomed}
+						ref={this.scrollRef}
+						initialScrollIndex={this.state.page}
+						initialNumToRender={3}
+						windowSize={5}
+						maxToRenderPerBatch={3}
+						data={this.state.attachments}
+						CellRendererComponent={this.renderCell}
+						renderItem={this.renderItem}
+						getItemLayout={this.getItemLayout}
+						keyExtractor={(item) => item.source}
+						viewabilityConfig={this.viewabilityConfig}
+						onViewableItemsChanged={this.updatePage}
+					/>
+				)}
+
+				<CarouselActions onCycleThroughAttachments={this.cycleThroughAttachments} />
+			</View>
+		);
+	}
 }
 
 AttachmentCarousel.propTypes = propTypes;
 AttachmentCarousel.defaultProps = defaultProps;
 
 export default compose(
-    withOnyx({
-        reportActions: {
-            key: ({ reportID }) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
-            canEvict: false,
-        },
-    }),
-    withLocalize,
-    withWindowDimensions,
+	withOnyx({
+		reportActions: {
+			key: ({ reportID }) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+			canEvict: false,
+		},
+	}),
+	withLocalize,
+	withWindowDimensions,
 )(AttachmentCarousel);
diff --git a/src/components/AttachmentCarousel/index.native.js b/src/components/AttachmentCarousel/index.native.js
new file mode 100644
index 000000000000..2cf732659403
--- /dev/null
+++ b/src/components/AttachmentCarousel/index.native.js
@@ -0,0 +1,357 @@
+import React from 'react';
+import { View, FlatList } from 'react-native';
+import PropTypes from 'prop-types';
+import { withOnyx } from 'react-native-onyx';
+import _ from 'underscore';
+import * as Expensicons from '../Icon/Expensicons';
+import styles from '../../styles/styles';
+import themeColors from '../../styles/themes/default';
+import CarouselActions from './CarouselActions';
+import Button from '../Button';
+import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
+import AttachmentView from '../AttachmentView';
+import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
+import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
+import CONST from '../../CONST';
+import ONYXKEYS from '../../ONYXKEYS';
+import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
+import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
+import Tooltip from '../Tooltip';
+import withLocalize, { withLocalizePropTypes } from '../withLocalize';
+import compose from '../../libs/compose';
+import withWindowDimensions from '../withWindowDimensions';
+import Pager from './Lightbox';
+
+const propTypes = {
+	/** source is used to determine the starting index in the array of attachments */
+	source: PropTypes.string,
+
+	/** Callback to update the parent modal's state with a source and name from the attachments array */
+	onNavigate: PropTypes.func,
+
+	/** Object of report actions for this report */
+	reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+
+	...withLocalizePropTypes,
+};
+
+const defaultProps = {
+	source: '',
+	reportActions: {},
+	onNavigate: () => { },
+};
+
+class AttachmentCarousel extends React.Component {
+	constructor(props) {
+		super(props);
+
+		this.scrollRef = React.createRef();
+		this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
+		this.viewabilityConfig = {
+			// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+			// more than 90% visible. When that happens we update the page index in the state.
+			itemVisiblePercentThreshold: 95,
+		};
+
+		this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
+		this.autoHideArrow = this.autoHideArrow.bind(this);
+		this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
+		this.getItemLayout = this.getItemLayout.bind(this);
+		this.renderItem = this.renderItem.bind(this);
+		this.renderCell = this.renderCell.bind(this);
+		this.updatePage = this.updatePage.bind(this);
+		this.updateZoomState = this.updateZoomState.bind(this);
+		this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
+
+		this.state = this.makeInitialState();
+	}
+
+	componentDidMount() {
+		this.autoHideArrow();
+	}
+
+	/**
+	 * Helps to navigate between next/previous attachments
+	 * @param {Object} attachmentItem
+	 * @returns {Object}
+	 */
+	getAttachment(attachmentItem) {
+		const source = _.get(attachmentItem, 'source', '');
+		const file = _.get(attachmentItem, 'file', { name: '' });
+
+		return {
+			source,
+			file,
+		};
+	}
+
+	/**
+	 * Calculate items layout information to optimize scrolling performance
+	 * @param {*} data
+	 * @param {Number} index
+	 * @returns {{offset: Number, length: Number, index: Number}}
+	 */
+	getItemLayout(data, index) {
+		return {
+			length: this.state.containerWidth,
+			offset: this.state.containerWidth * index,
+			index,
+		};
+	}
+
+	/**
+	 * On a touch screen device, automatically hide the arrows
+	 * if there is no interaction for 3 seconds.
+	 */
+	autoHideArrow() {
+		if (!this.canUseTouchScreen) {
+			return;
+		}
+		this.cancelAutoHideArrow();
+		this.autoHideArrowTimeout = setTimeout(() => {
+			this.toggleArrowsVisibility(false);
+		}, CONST.ARROW_HIDE_DELAY);
+	}
+
+	/**
+	 * Cancels the automatic hiding of the arrows.
+	 */
+	cancelAutoHideArrow() {
+		clearTimeout(this.autoHideArrowTimeout);
+	}
+
+	/**
+	 * Toggles the visibility of the arrows
+	 * @param {Boolean} shouldShowArrow
+	 */
+	toggleArrowsVisibility(shouldShowArrow) {
+		// Don't toggle arrows in a zoomed state
+		if (this.state.isZoomed) {
+			return;
+		}
+		this.setState(
+			(current) => {
+				const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
+				return { shouldShowArrow: newShouldShowArrow };
+			},
+			() => {
+				if (this.state.shouldShowArrow) {
+					this.autoHideArrow();
+				} else {
+					this.cancelAutoHideArrow();
+				}
+			},
+		);
+	}
+
+	/**
+	 * Updates zoomed state to enable/disable panning the PDF
+	 * @param {Number} scale current PDF scale
+	 */
+	updateZoomState(scale) {
+		const isZoomed = scale > 1;
+		if (isZoomed === this.state.isZoomed) {
+			return;
+		}
+		if (isZoomed) {
+			this.toggleArrowsVisibility(false);
+		}
+		this.setState({ isZoomed });
+	}
+
+	/**
+	 * Map report actions to attachment items and sets the initial carousel state
+	 * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
+	 */
+	makeInitialState() {
+		let page = 0;
+		const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
+
+		/**
+		 * Looping to filter out attachments and retrieve the src URL and name of attachments.
+		 */
+		const attachments = [];
+		_.forEach(actions, ({ originalMessage, message }) => {
+			// Check for attachment which hasn't been deleted
+			if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
+				return;
+			}
+			const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
+
+			// matchAll captured both source url and name of the attachment
+			if (matches.length === 2) {
+				const [originalSource, name] = _.map(matches, (m) => m[2]);
+
+				// Update the image URL so the images can be accessed depending on the config environment.
+				// Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
+				const source = tryResolveUrlFromApiRoot(originalSource);
+				if (source === this.props.source) {
+					page = attachments.length;
+				}
+
+				attachments.push({ source, file: { name } });
+			}
+		});
+
+		return {
+			page,
+			attachments,
+			shouldShowArrow: this.canUseTouchScreen,
+			containerWidth: 0,
+			isZoomed: false,
+		};
+	}
+
+	/**
+	 * Increments or decrements the index to get another selected item
+	 * @param {Number} deltaSlide
+	 */
+	cycleThroughAttachments(deltaSlide) {
+		const nextIndex = this.state.page - deltaSlide;
+		const nextItem = this.state.attachments[nextIndex];
+
+		if (!nextItem) {
+			return;
+		}
+
+		// The sliding transition is a bit too much on web, because of the wider and bigger images,
+		// so we only enable it for mobile
+		this.scrollRef.current.scrollToIndex({ index: nextIndex, animated: this.canUseTouchScreen });
+	}
+
+	/**
+	 * Updates the page state when the user navigates between attachments
+	 * @param {Array<{item: *, index: Number}>} viewableItems
+	 */
+	updatePage({ viewableItems }) {
+		// Since we can have only one item in view at a time, we can use the first item in the array
+		// to get the index of the current page
+		const entry = _.first(viewableItems);
+		if (!entry) {
+			return;
+		}
+
+		const page = entry.index;
+		const { source, file } = this.getAttachment(entry.item);
+		this.props.onNavigate({ source: addEncryptedAuthTokenToURL(source), file });
+		this.setState({ page, isZoomed: false });
+	}
+
+	/**
+	 * Defines how a container for a single attachment should be rendered
+	 * @param {Object} props
+	 * @returns {JSX.Element}
+	 */
+	renderCell(props) {
+		const style = [props.style, styles.h100, { width: this.state.containerWidth }];
+
+		return (
+			<View
+				// eslint-disable-next-line react/jsx-props-no-spreading
+				{...props}
+				style={style}
+			/>
+		);
+	}
+
+	/**
+	 * Defines how a single attachment should be rendered
+	 * @param {{ source: String, file: { name: String } }} item
+	 * @returns {JSX.Element}
+	 */
+	renderItem({ item }) {
+		const authSource = addEncryptedAuthTokenToURL(item.source);
+		if (!this.canUseTouchScreen) {
+			return (
+				<AttachmentView
+					source={authSource}
+					file={item.file}
+				/>
+			);
+		}
+
+		return (
+			<AttachmentView
+				source={authSource}
+				file={item.file}
+				onScaleChanged={this.updateZoomState}
+				onPress={this.toggleArrowsVisibility}
+			/>
+		);
+	}
+
+	render() {
+		const isForwardDisabled = this.state.page === 0;
+		const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
+
+		return (
+			<View
+				style={[styles.attachmentModalArrowsContainer, styles.flex1]}
+			>
+				{this.state.shouldShowArrow && (
+					<>
+						{!isBackDisabled && (
+							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
+								<Tooltip text={this.props.translate('common.previous')}>
+									<Button
+										small
+										innerStyles={[styles.arrowIcon]}
+										icon={Expensicons.BackArrow}
+										iconFill={themeColors.text}
+										iconStyles={[styles.mr0]}
+										onPress={() => {
+											this.cycleThroughAttachments(-1);
+											this.autoHideArrow();
+										}}
+										onPressIn={this.cancelAutoHideArrow}
+										onPressOut={this.autoHideArrow}
+									/>
+								</Tooltip>
+							</View>
+						)}
+						{!isForwardDisabled && (
+							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
+								<Tooltip text={this.props.translate('common.next')}>
+									<Button
+										small
+										innerStyles={[styles.arrowIcon]}
+										icon={Expensicons.ArrowRight}
+										iconFill={themeColors.text}
+										iconStyles={[styles.mr0]}
+										onPress={() => {
+											this.cycleThroughAttachments(1);
+											this.autoHideArrow();
+										}}
+										onPressIn={this.cancelAutoHideArrow}
+										onPressOut={this.autoHideArrow}
+									/>
+								</Tooltip>
+							</View>
+						)}
+					</>
+				)}
+
+				<Pager
+					items={this.state.attachments}
+					initialIndex={this.state.page}
+					onTap={this.toggleArrowsVisibility}
+					itemExtractor={({ item }) => ({ key: item.source, url: addEncryptedAuthTokenToURL(item.source) })}
+				/>
+			</View>
+		);
+	}
+}
+
+AttachmentCarousel.propTypes = propTypes;
+AttachmentCarousel.defaultProps = defaultProps;
+
+export default compose(
+	withOnyx({
+		reportActions: {
+			key: ({ reportID }) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+			canEvict: false,
+		},
+	}),
+	withLocalize,
+	withWindowDimensions,
+)(AttachmentCarousel);
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 0cc26dbc14f2..ce24c7518c3d 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -9,7 +9,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'
 import CONST from '../CONST';
 import Modal from './Modal';
 import AttachmentView from './AttachmentView';
-import AttachmentCarousel from './AttachmentCarousel';
+import AttachmentCarousel from './AttachmentCarousel/index.native';
 import styles from '../styles/styles';
 import * as StyleUtils from '../styles/StyleUtils';
 import * as FileUtils from '../libs/fileDownload/FileUtils';
@@ -30,312 +30,312 @@ import SafeAreaConsumer from './SafeAreaConsumer';
  */
 
 const propTypes = {
-    /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */
-    source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+	/** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */
+	source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
 
-    /** Optional callback to fire when we want to preview an image and approve it for use. */
-    onConfirm: PropTypes.func,
+	/** Optional callback to fire when we want to preview an image and approve it for use. */
+	onConfirm: PropTypes.func,
 
-    /** Optional callback to fire when we want to do something after modal hide. */
-    onModalHide: PropTypes.func,
+	/** Optional callback to fire when we want to do something after modal hide. */
+	onModalHide: PropTypes.func,
 
-    /** Optional original filename when uploading */
-    originalFileName: PropTypes.string,
+	/** Optional original filename when uploading */
+	originalFileName: PropTypes.string,
 
-    /** A function as a child to pass modal launching methods to */
-    children: PropTypes.func.isRequired,
+	/** A function as a child to pass modal launching methods to */
+	children: PropTypes.func.isRequired,
 
-    /** Whether source url requires authentication */
-    isAuthTokenRequired: PropTypes.bool,
+	/** Whether source url requires authentication */
+	isAuthTokenRequired: PropTypes.bool,
 
-    /** Determines if download Button should be shown or not */
-    allowDownload: PropTypes.bool,
+	/** Determines if download Button should be shown or not */
+	allowDownload: PropTypes.bool,
 
-    /** Title shown in the header of the modal */
-    headerTitle: PropTypes.string,
+	/** Title shown in the header of the modal */
+	headerTitle: PropTypes.string,
 
-    /** The ID of the report that has this attachment */
-    reportID: PropTypes.string,
+	/** The ID of the report that has this attachment */
+	reportID: PropTypes.string,
 
-    ...withLocalizePropTypes,
+	...withLocalizePropTypes,
 
-    ...windowDimensionsPropTypes,
+	...windowDimensionsPropTypes,
 };
 
 const defaultProps = {
-    source: '',
-    onConfirm: null,
-    originalFileName: '',
-    isAuthTokenRequired: false,
-    allowDownload: false,
-    headerTitle: null,
-    reportID: '',
-    onModalHide: () => { },
+	source: '',
+	onConfirm: null,
+	originalFileName: '',
+	isAuthTokenRequired: false,
+	allowDownload: false,
+	headerTitle: null,
+	reportID: '',
+	onModalHide: () => { },
 };
 
 class AttachmentModal extends PureComponent {
-    constructor(props) {
-        super(props);
-
-        this.state = {
-            isModalOpen: false,
-            shouldLoadAttachment: false,
-            isAttachmentInvalid: false,
-            attachmentInvalidReasonTitle: null,
-            attachmentInvalidReason: null,
-            source: props.source,
-            modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
-            isConfirmButtonDisabled: false,
-            confirmButtonFadeAnimation: new Animated.Value(1),
-            file: props.originalFileName
-                ? {
-                    name: props.originalFileName,
-                }
-                : undefined,
-        };
-
-        this.submitAndClose = this.submitAndClose.bind(this);
-        this.closeConfirmModal = this.closeConfirmModal.bind(this);
-        this.onNavigate = this.onNavigate.bind(this);
-        this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
-        this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
-    }
-
-    /**
-     * Helps to navigate between next/previous attachments
-     * by setting sourceURL and file in state
-     * @param {Object} attachmentData
-     */
-    onNavigate(attachmentData) {
-        this.setState(attachmentData);
-    }
-
-    /**
-     * If our attachment is a PDF, return the unswipeable Modal type.
-     * @param {String} sourceURL
-     * @param {Object} file
-     * @returns {String}
-     */
-    getModalType(sourceURL, file) {
-        return sourceURL && (Str.isPDF(sourceURL) || (file && Str.isPDF(file.name || this.props.translate('attachmentView.unknownFilename'))))
-            ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
-            : CONST.MODAL.MODAL_TYPE.CENTERED;
-    }
-
-    /**
-     * @param {String} sourceURL
-     */
-    downloadAttachment(sourceURL) {
-        const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
-        fileDownload(sourceURL, originalFileName);
-
-        // At ios, if the keyboard is open while opening the attachment, then after downloading
-        // the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
-        Keyboard.dismiss();
-    }
-
-    /**
-     * Execute the onConfirm callback and close the modal.
-     */
-    submitAndClose() {
-        // If the modal has already been closed or the confirm button is disabled
-        // do not submit.
-        if (!this.state.isModalOpen || this.state.isConfirmButtonDisabled) {
-            return;
-        }
-
-        if (this.props.onConfirm) {
-            this.props.onConfirm(lodashExtend(this.state.file, { source: this.state.source }));
-        }
-
-        this.setState({ isModalOpen: false });
-    }
-
-    /**
-     * Close the confirm modal.
-     */
-    closeConfirmModal() {
-        this.setState({ isAttachmentInvalid: false });
-    }
-
-    /**
-     * @param {Object} file
-     * @returns {Boolean}
-     */
-    isValidFile(file) {
-        const { fileExtension } = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
-        if (!_.contains(CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
-            const invalidReason = `${this.props.translate('attachmentPicker.notAllowedExtension')} ${CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS.join(', ')}`;
-            this.setState({
-                isAttachmentInvalid: true,
-                attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.wrongFileType'),
-                attachmentInvalidReason: invalidReason,
-            });
-            return false;
-        }
-
-        if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
-            this.setState({
-                isAttachmentInvalid: true,
-                attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooLarge'),
-                attachmentInvalidReason: this.props.translate('attachmentPicker.sizeExceeded'),
-            });
-            return false;
-        }
-
-        if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
-            this.setState({
-                isAttachmentInvalid: true,
-                attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooSmall'),
-                attachmentInvalidReason: this.props.translate('attachmentPicker.sizeNotMet'),
-            });
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * @param {Object} file
-     */
-    validateAndDisplayFileToUpload(file) {
-        if (!file) {
-            return;
-        }
-
-        if (!this.isValidFile(file)) {
-            return;
-        }
-
-        if (file instanceof File) {
-            const source = URL.createObjectURL(file);
-            const modalType = this.getModalType(source, file);
-            this.setState({
-                isModalOpen: true,
-                source,
-                file,
-                modalType,
-            });
-        } else {
-            const modalType = this.getModalType(file.uri, file);
-            this.setState({
-                isModalOpen: true,
-                source: file.uri,
-                file,
-                modalType,
-            });
-        }
-    }
-
-    /**
-     * In order to gracefully hide/show the confirm button when the keyboard
-     * opens/closes, apply an animation to fade the confirm button out/in. And since
-     * we're only updating the opacity of the confirm button, we must also conditionally
-     * disable it.
-     *
-     * @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in.
-     */
-    updateConfirmButtonVisibility(shouldFadeOut) {
-        this.setState({ isConfirmButtonDisabled: shouldFadeOut });
-        const toValue = shouldFadeOut ? 0 : 1;
-
-        Animated.timing(this.state.confirmButtonFadeAnimation, {
-            toValue,
-            duration: 100,
-            useNativeDriver: true,
-        }).start();
-    }
-
-    render() {
-        const source = this.state.source;
-        console.log('render attachment modal')
-
-        return (
-            <>
-                <Modal
-                    type={this.state.modalType}
-                    onSubmit={this.submitAndClose}
-                    onClose={() => this.setState({ isModalOpen: false })}
-                    isVisible={this.state.isModalOpen}
-                    backgroundColor={themeColors.componentBG}
-                    onModalShow={() => this.setState({ shouldLoadAttachment: true })}
-                    onModalHide={(e) => {
-                        this.props.onModalHide(e);
-                        this.setState({ shouldLoadAttachment: false });
-                    }}
-                    propagateSwipe
-                >
-                    <GestureHandlerRootView style={{ flex: 1 }}>
-                        {this.props.isSmallScreenWidth && <HeaderGap />}
-                        <HeaderWithCloseButton
-                            title={this.props.headerTitle || this.props.translate('common.attachment')}
-                            shouldShowBorderBottom
-                            shouldShowDownloadButton={this.props.allowDownload}
-                            onDownloadButtonPress={() => this.downloadAttachment(source)}
-                            onCloseButtonPress={() => this.setState({ isModalOpen: false })}
-                        />
-                        <View style={styles.imageModalImageCenterContainer}>
-                            {this.props.reportID ? (
-
-                                <AttachmentCarousel
-                                    reportID={this.props.reportID}
-                                    onNavigate={this.onNavigate}
-                                    source={this.props.source}
-                                    onToggleKeyboard={this.updateConfirmButtonVisibility}
-                                />
-
-                            ) : (
-                                Boolean(this.state.source) &&
-                                this.state.shouldLoadAttachment && (
-                                    <AttachmentView
-                                        source={source}
-                                        isAuthTokenRequired={this.props.isAuthTokenRequired}
-                                        file={this.state.file}
-                                        onToggleKeyboard={this.updateConfirmButtonVisibility}
-                                    />
-                                )
-                            )}
-                        </View>
-                        {/* If we have an onConfirm method show a confirmation button */}
-                        {Boolean(this.props.onConfirm) && (
-                            <SafeAreaConsumer>
-                                {({ safeAreaPaddingBottomStyle }) => (
-                                    <Animated.View style={[StyleUtils.fade(this.state.confirmButtonFadeAnimation), safeAreaPaddingBottomStyle]}>
-                                        <Button
-                                            success
-                                            style={[styles.buttonConfirm, this.props.isSmallScreenWidth ? {} : styles.attachmentButtonBigScreen]}
-                                            textStyles={[styles.buttonConfirmText]}
-                                            text={this.props.translate('common.send')}
-                                            onPress={this.submitAndClose}
-                                            disabled={this.state.isConfirmButtonDisabled}
-                                            pressOnEnter
-                                        />
-                                    </Animated.View>
-                                )}
-                            </SafeAreaConsumer>
-                        )}
-                    </GestureHandlerRootView>
-                </Modal>
-
-                <ConfirmModal
-                    title={this.state.attachmentInvalidReasonTitle}
-                    onConfirm={this.closeConfirmModal}
-                    onCancel={this.closeConfirmModal}
-                    isVisible={this.state.isAttachmentInvalid}
-                    prompt={this.state.attachmentInvalidReason}
-                    confirmText={this.props.translate('common.close')}
-                    shouldShowCancelButton={false}
-                />
-
-                {this.props.children({
-                    displayFileInModal: this.validateAndDisplayFileToUpload,
-                    show: () => {
-                        this.setState({ isModalOpen: true });
-                    },
-                })}
-            </>
-        );
-    }
+	constructor(props) {
+		super(props);
+
+		this.state = {
+			isModalOpen: false,
+			shouldLoadAttachment: false,
+			isAttachmentInvalid: false,
+			attachmentInvalidReasonTitle: null,
+			attachmentInvalidReason: null,
+			source: props.source,
+			modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
+			isConfirmButtonDisabled: false,
+			confirmButtonFadeAnimation: new Animated.Value(1),
+			file: props.originalFileName
+				? {
+					name: props.originalFileName,
+				}
+				: undefined,
+		};
+
+		this.submitAndClose = this.submitAndClose.bind(this);
+		this.closeConfirmModal = this.closeConfirmModal.bind(this);
+		this.onNavigate = this.onNavigate.bind(this);
+		this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
+		this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
+	}
+
+	/**
+	 * Helps to navigate between next/previous attachments
+	 * by setting sourceURL and file in state
+	 * @param {Object} attachmentData
+	 */
+	onNavigate(attachmentData) {
+		this.setState(attachmentData);
+	}
+
+	/**
+	 * If our attachment is a PDF, return the unswipeable Modal type.
+	 * @param {String} sourceURL
+	 * @param {Object} file
+	 * @returns {String}
+	 */
+	getModalType(sourceURL, file) {
+		return sourceURL && (Str.isPDF(sourceURL) || (file && Str.isPDF(file.name || this.props.translate('attachmentView.unknownFilename'))))
+			? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
+			: CONST.MODAL.MODAL_TYPE.CENTERED;
+	}
+
+	/**
+	 * @param {String} sourceURL
+	 */
+	downloadAttachment(sourceURL) {
+		const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
+		fileDownload(sourceURL, originalFileName);
+
+		// At ios, if the keyboard is open while opening the attachment, then after downloading
+		// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
+		Keyboard.dismiss();
+	}
+
+	/**
+	 * Execute the onConfirm callback and close the modal.
+	 */
+	submitAndClose() {
+		// If the modal has already been closed or the confirm button is disabled
+		// do not submit.
+		if (!this.state.isModalOpen || this.state.isConfirmButtonDisabled) {
+			return;
+		}
+
+		if (this.props.onConfirm) {
+			this.props.onConfirm(lodashExtend(this.state.file, { source: this.state.source }));
+		}
+
+		this.setState({ isModalOpen: false });
+	}
+
+	/**
+	 * Close the confirm modal.
+	 */
+	closeConfirmModal() {
+		this.setState({ isAttachmentInvalid: false });
+	}
+
+	/**
+	 * @param {Object} file
+	 * @returns {Boolean}
+	 */
+	isValidFile(file) {
+		const { fileExtension } = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
+		if (!_.contains(CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
+			const invalidReason = `${this.props.translate('attachmentPicker.notAllowedExtension')} ${CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_EXTENSIONS.join(', ')}`;
+			this.setState({
+				isAttachmentInvalid: true,
+				attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.wrongFileType'),
+				attachmentInvalidReason: invalidReason,
+			});
+			return false;
+		}
+
+		if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+			this.setState({
+				isAttachmentInvalid: true,
+				attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooLarge'),
+				attachmentInvalidReason: this.props.translate('attachmentPicker.sizeExceeded'),
+			});
+			return false;
+		}
+
+		if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+			this.setState({
+				isAttachmentInvalid: true,
+				attachmentInvalidReasonTitle: this.props.translate('attachmentPicker.attachmentTooSmall'),
+				attachmentInvalidReason: this.props.translate('attachmentPicker.sizeNotMet'),
+			});
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * @param {Object} file
+	 */
+	validateAndDisplayFileToUpload(file) {
+		if (!file) {
+			return;
+		}
+
+		if (!this.isValidFile(file)) {
+			return;
+		}
+
+		if (file instanceof File) {
+			const source = URL.createObjectURL(file);
+			const modalType = this.getModalType(source, file);
+			this.setState({
+				isModalOpen: true,
+				source,
+				file,
+				modalType,
+			});
+		} else {
+			const modalType = this.getModalType(file.uri, file);
+			this.setState({
+				isModalOpen: true,
+				source: file.uri,
+				file,
+				modalType,
+			});
+		}
+	}
+
+	/**
+	 * In order to gracefully hide/show the confirm button when the keyboard
+	 * opens/closes, apply an animation to fade the confirm button out/in. And since
+	 * we're only updating the opacity of the confirm button, we must also conditionally
+	 * disable it.
+	 *
+	 * @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in.
+	 */
+	updateConfirmButtonVisibility(shouldFadeOut) {
+		this.setState({ isConfirmButtonDisabled: shouldFadeOut });
+		const toValue = shouldFadeOut ? 0 : 1;
+
+		Animated.timing(this.state.confirmButtonFadeAnimation, {
+			toValue,
+			duration: 100,
+			useNativeDriver: true,
+		}).start();
+	}
+
+	render() {
+		const source = this.state.source;
+		console.log('render attachment modal')
+
+		return (
+			<>
+				<Modal
+					type={this.state.modalType}
+					onSubmit={this.submitAndClose}
+					onClose={() => this.setState({ isModalOpen: false })}
+					isVisible={this.state.isModalOpen}
+					backgroundColor={themeColors.componentBG}
+					onModalShow={() => this.setState({ shouldLoadAttachment: true })}
+					onModalHide={(e) => {
+						this.props.onModalHide(e);
+						this.setState({ shouldLoadAttachment: false });
+					}}
+					propagateSwipe
+				>
+					<GestureHandlerRootView style={{ flex: 1 }}>
+						{this.props.isSmallScreenWidth && <HeaderGap />}
+						<HeaderWithCloseButton
+							title={this.props.headerTitle || this.props.translate('common.attachment')}
+							shouldShowBorderBottom
+							shouldShowDownloadButton={this.props.allowDownload}
+							onDownloadButtonPress={() => this.downloadAttachment(source)}
+							onCloseButtonPress={() => this.setState({ isModalOpen: false })}
+						/>
+						<View style={styles.imageModalImageCenterContainer}>
+							{this.props.reportID ? (
+
+								<AttachmentCarousel
+									reportID={this.props.reportID}
+									onNavigate={this.onNavigate}
+									source={this.props.source}
+									onToggleKeyboard={this.updateConfirmButtonVisibility}
+								/>
+
+							) : (
+								Boolean(this.state.source) &&
+								this.state.shouldLoadAttachment && (
+									<AttachmentView
+										source={source}
+										isAuthTokenRequired={this.props.isAuthTokenRequired}
+										file={this.state.file}
+										onToggleKeyboard={this.updateConfirmButtonVisibility}
+									/>
+								)
+							)}
+						</View>
+						{/* If we have an onConfirm method show a confirmation button */}
+						{Boolean(this.props.onConfirm) && (
+							<SafeAreaConsumer>
+								{({ safeAreaPaddingBottomStyle }) => (
+									<Animated.View style={[StyleUtils.fade(this.state.confirmButtonFadeAnimation), safeAreaPaddingBottomStyle]}>
+										<Button
+											success
+											style={[styles.buttonConfirm, this.props.isSmallScreenWidth ? {} : styles.attachmentButtonBigScreen]}
+											textStyles={[styles.buttonConfirmText]}
+											text={this.props.translate('common.send')}
+											onPress={this.submitAndClose}
+											disabled={this.state.isConfirmButtonDisabled}
+											pressOnEnter
+										/>
+									</Animated.View>
+								)}
+							</SafeAreaConsumer>
+						)}
+					</GestureHandlerRootView>
+				</Modal>
+
+				<ConfirmModal
+					title={this.state.attachmentInvalidReasonTitle}
+					onConfirm={this.closeConfirmModal}
+					onCancel={this.closeConfirmModal}
+					isVisible={this.state.isAttachmentInvalid}
+					prompt={this.state.attachmentInvalidReason}
+					confirmText={this.props.translate('common.close')}
+					shouldShowCancelButton={false}
+				/>
+
+				{this.props.children({
+					displayFileInModal: this.validateAndDisplayFileToUpload,
+					show: () => {
+						this.setState({ isModalOpen: true });
+					},
+				})}
+			</>
+		);
+	}
 }
 
 AttachmentModal.propTypes = propTypes;
diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js
index c8bd9b075e2d..021d9d0fd3f4 100644
--- a/src/components/ImageView/index.native.js
+++ b/src/components/ImageView/index.native.js
@@ -1,11 +1,11 @@
-import React, {PureComponent} from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
-import {View, InteractionManager, PanResponder} from 'react-native';
+import { View, InteractionManager, PanResponder } from 'react-native';
 import ImageZoom from 'react-native-image-pan-zoom';
 import _ from 'underscore';
 import styles from '../../styles/styles';
 import variables from '../../styles/variables';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
+import withWindowDimensions, { windowDimensionsPropTypes } from '../withWindowDimensions';
 import FullscreenLoadingIndicator from '../FullscreenLoadingIndicator';
 import Image from '../Image';
 
@@ -27,7 +27,7 @@ const propTypes = {
 
 const defaultProps = {
     isAuthTokenRequired: false,
-    onPress: () => {},
+    onPress: () => { },
 };
 
 class ImageView extends PureComponent {
@@ -99,7 +99,7 @@ class ImageView extends PureComponent {
      *
      * @param {Object} nativeEvent
      */
-    configureImageZoom({nativeEvent}) {
+    configureImageZoom({ nativeEvent }) {
         // Wait till animations are over to prevent stutter in navigation animation
         this.state.interactionPromise = InteractionManager.runAfterInteractions(() => {
             let imageWidth = nativeEvent.width;
@@ -116,7 +116,7 @@ class ImageView extends PureComponent {
             const maxDimensionsScale = 11;
             imageWidth = Math.min(imageWidth, containerWidth * maxDimensionsScale);
             imageHeight = Math.min(imageHeight, containerHeight * maxDimensionsScale);
-            this.setState({imageHeight, imageWidth, isLoading: false});
+            this.setState({ imageHeight, imageWidth, isLoading: false });
         });
     }
 
@@ -144,7 +144,7 @@ class ImageView extends PureComponent {
             return;
         }
         this.resetImageZoom();
-        this.setState({imageHeight: 0, imageWidth: 0, isLoading: true});
+        this.setState({ imageHeight: 0, imageWidth: 0, isLoading: true });
     }
 
     render() {
@@ -195,7 +195,7 @@ class ImageView extends PureComponent {
                             // We must be either swiping down or double tapping since we are at zoom scale 1
                             return false;
                         }}
-                        onMove={({scale}) => {
+                        onMove={({ scale }) => {
                             this.imageZoomScale = scale;
                         }}
                     >
@@ -211,7 +211,7 @@ class ImageView extends PureComponent {
                                 shouldShowLoadingIndicator ? styles.opacity0 : styles.opacity1,
                             ]}
                             disableTransformation
-                            source={{uri: this.props.url}}
+                            source={{ uri: this.props.url }}
                             isAuthTokenRequired={this.props.isAuthTokenRequired}
                             resizeMode={Image.resizeMode.contain}
                             onLoadStart={this.imageLoadingStart}

From 39ea2a86d366aab7c3e705fa5d8dc909a4e3205c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 15 Jun 2023 12:59:13 +0200
Subject: [PATCH 004/133] fix: prettier

---
 src/components/AttachmentCarousel/Lightbox.js | 1497 ++++++++---------
 .../AttachmentCarousel/index.native.js        |  636 ++++---
 2 files changed, 1063 insertions(+), 1070 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 2fbe91c7c682..92a01306583e 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,20 +1,20 @@
-import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
-import { ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions } from 'react-native';
-import { Gesture, GestureDetector, createNativeWrapper } from 'react-native-gesture-handler';
+import React, {createContext, useContext, useEffect, useRef, useState} from 'react';
+import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
+import {Gesture, GestureDetector, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
-	cancelAnimation,
-	runOnJS,
-	runOnUI,
-	useAnimatedProps,
-	useAnimatedReaction,
-	useAnimatedStyle,
-	useDerivedValue,
-	useEvent,
-	useHandler,
-	useSharedValue,
-	useWorkletCallback,
-	withDecay,
-	withSpring,
+    cancelAnimation,
+    runOnJS,
+    runOnUI,
+    useAnimatedProps,
+    useAnimatedReaction,
+    useAnimatedStyle,
+    useDerivedValue,
+    useEvent,
+    useHandler,
+    useSharedValue,
+    useWorkletCallback,
+    withDecay,
+    withSpring,
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import Image from '../Image';
@@ -22,9 +22,9 @@ import Image from '../Image';
 const Context = createContext(null);
 
 function clamp(value, lowerBound, upperBound) {
-	'worklet';
+    'worklet';
 
-	return Math.min(Math.max(lowerBound, value), upperBound);
+    return Math.min(Math.max(lowerBound, value), upperBound);
 }
 
 const DOUBLE_TAP_SCALE = 3;
@@ -32,763 +32,758 @@ const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
 
 const config = {
-	mass: 3,
-	stiffness: 1000,
-	damping: 500,
+    mass: 3,
+    stiffness: 1000,
+    damping: 500,
 };
 
-function getScaledDimensions({ canvasWidth, canvasHeight, imageWidth, imageHeight }) {
-	console.log({
-		canvasWidth,
-		canvasHeight,
-		imageWidth,
-		imageHeight,
-	})
-
-	const scaleFactor = imageWidth / canvasWidth;
-
-	const width = canvasWidth;
-	const height = imageHeight / scaleFactor;
-
-	console.log({
-		canvasWidth,
-		canvasHeight,
-		imageWidth,
-		imageHeight,
-		width,
-		height,
-		scaleFactor,
-	})
-
-	return { width, height };
+function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
+    console.log({
+        canvasWidth,
+        canvasHeight,
+        imageWidth,
+        imageHeight,
+    });
+
+    const scaleFactor = imageWidth / canvasWidth;
+
+    const width = canvasWidth;
+    const height = imageHeight / scaleFactor;
+
+    console.log({
+        canvasWidth,
+        canvasHeight,
+        imageWidth,
+        imageHeight,
+        width,
+        height,
+        scaleFactor,
+    });
+
+    return {width, height};
 }
 
 const defaultDimensions = {
-	width: 1,
-	height: 1,
+    width: 1,
+    height: 1,
 };
 
-function ImageTransformer({ canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap }) {
-	const { pagerRef, shouldPagerScroll, isScrolling } = useContext(Context);
+function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
+    const {pagerRef, shouldPagerScroll, isScrolling} = useContext(Context);
 
-	const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
+    const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
 
-	const [imageDimensions, setImageDimensions] = useState(
-		showFallback
-			? defaultDimensions
-			: getScaledDimensions({
-				canvasWidth,
-				canvasHeight,
-				imageWidth,
-				imageHeight,
-			}),
-	);
-
-	const canvasX = useSharedValue(canvasWidth);
-	const canvasY = useSharedValue(canvasHeight);
-
-	useEffect(() => {
-		runOnUI(() => {
-			'worklet';
-
-			canvasX.value = canvasWidth;
-			canvasY.value = canvasHeight;
-		})();
-	}, [canvasX, canvasY, canvasWidth, canvasHeight]);
-
-	const targetHeight = useSharedValue(0);
-
-	const onLoad = (resolvedDimensions) => {
-		const { width, height } = getScaledDimensions({
-			canvasWidth,
-			canvasHeight,
-			imageWidth: resolvedDimensions.width,
-			imageHeight: resolvedDimensions.height,
-		});
-
-		targetHeight.value = height;
-
-		setImageDimensions({
-			width,
-			height,
-		});
-
-		setShowFallback(false);
-	};
-
-	// used for pan gesture
-
-	const translateY = useSharedValue(0);
-	const translateX = useSharedValue(0);
-	const offsetX = useSharedValue(0);
-	const offsetY = useSharedValue(0);
-	const isSwiping = useSharedValue(false);
-
-	// used for moving fingers when pinching
-	const scaleTranslateX = useSharedValue(0);
-	const scaleTranslateY = useSharedValue(0);
-
-	// storage for the the origin of the gesture
-	const origin = {
-		x: useSharedValue(0),
-		y: useSharedValue(0),
-	};
-
-	// storage for the pan velocity to calculate the decay
-	const panVelocityX = useSharedValue(0);
-	const panVelocityY = useSharedValue(0);
-
-	const scale = useSharedValue(1);
-	// store scale in between gestures
-	const scaleOffset = useSharedValue(1);
-
-	// disable pan vertically when image is smaller than screen
-	const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
-
-	// calculates bounds of the scaled image
-	// can we pan left/right/up/down
-	// can be used to limit gesture or implementing tension effect
-	const getBounds = useWorkletCallback(() => {
-		const target = {
-			x: 0,
-			y: 0,
-		};
-
-		const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
-		const scaledImage = targetHeight.value * fixedScale;
-
-		const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
-
-		let topBoundary = 0;
-
-		if (canvasY.value < scaledImage) {
-			topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
-		}
-
-		const maxVector = { x: rightBoundary, y: topBoundary };
-		const minVector = { x: -rightBoundary, y: -topBoundary };
-
-		target.x = clamp(offsetX.value, minVector.x, maxVector.x);
-		target.y = clamp(offsetY.value, minVector.y, maxVector.y);
-
-		const isInBoundaryX = target.x === offsetX.value;
-		const isInBoundaryY = target.y === offsetY.value;
-
-		return {
-			target,
-			isInBoundaryX,
-			isInBoundaryY,
-			minVector,
-			maxVector,
-			canPanLeft: target.x < maxVector.x,
-			canPanRight: target.x > minVector.x,
-		};
-	});
-
-	const afterGesture = useWorkletCallback(() => {
-		const { target, isInBoundaryX, isInBoundaryY, minVector, maxVector } = getBounds();
-
-		if (!canPanVertically.value) {
-			offsetY.value = withSpring(target.y, config);
-		}
-
-		if (
-			scale.value === 1 &&
-			offsetX.value === 0 &&
-			offsetY.value === 0 &&
-			translateX.value === 0 &&
-			translateY.value === 0 &&
-			scaleTranslateX.value === 0 &&
-			scaleTranslateY.value === 0
-		) {
-			// we don't need to run any animations
-			return;
-		}
-
-		if (scale.value <= 1) {
-			// just center it
-			// reset(true);
-			offsetX.value = withSpring(0, config);
-			offsetY.value = withSpring(0, config);
-			return;
-		}
-
-		const deceleration = 0.9915;
-
-		if (isInBoundaryX) {
-			if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
-				offsetX.value = withDecay({
-					velocity: panVelocityX.value,
-					clamp: [minVector.x, maxVector.x],
-					deceleration,
-					rubberBandEffect: false,
-				});
-			}
-		} else {
-			offsetX.value = withSpring(target.x, config);
-		}
-
-		if (isInBoundaryY) {
-			if (
-				Math.abs(panVelocityY.value) > 0 &&
-				scale.value <= MAX_SCALE &&
-				// limit vertical pan only when image is smaller than screen
-				offsetY.value !== minVector.y &&
-				offsetY.value !== maxVector.y
-			) {
-				offsetY.value = withDecay({
-					velocity: panVelocityY.value,
-					clamp: [minVector.y, maxVector.y],
-					deceleration,
-				});
-			}
-		} else {
-			offsetY.value = withSpring(target.y, config, () => {
-				isSwiping.value = false;
-			});
-		}
-	});
-
-	const stopAnimation = useWorkletCallback(() => {
-		cancelAnimation(offsetX);
-		cancelAnimation(offsetY);
-	});
-
-	const zoomToCoordinates = useWorkletCallback((x, y) => {
-		'worklet';
-
-		stopAnimation();
-
-		const usableImage = {
-			x: canvasWidth,
-			y: targetHeight.value,
-		};
-
-		const targetImageSize = {
-			x: usableImage.x * DOUBLE_TAP_SCALE,
-			y: usableImage.y * DOUBLE_TAP_SCALE,
-		};
-
-		const CENTER = {
-			x: canvasX.value / 2,
-			y: canvasY.value / 2,
-		};
-
-		const imageCenter = {
-			x: usableImage.x / 2,
-			y: usableImage.y / 2,
-		};
-
-		const focal = { x, y };
-
-		const currentOrigin = {
-			x: (targetImageSize.x / 2 - CENTER.x) * -1,
-			y: (targetImageSize.y / 2 - CENTER.y) * -1,
-		};
-
-		const koef = {
-			x: (1 / imageCenter.x) * focal.x - 1,
-			y: (1 / imageCenter.y) * focal.y - 1,
-		};
-
-		const target = {
-			x: currentOrigin.x * koef.x,
-			y: currentOrigin.y * koef.y,
-		};
-
-		if (targetImageSize.y < canvasY.value) {
-			target.y = 0;
-		}
-
-		offsetX.value = withSpring(target.x, config);
-		offsetY.value = withSpring(target.y, config);
-		scale.value = withSpring(DOUBLE_TAP_SCALE, config);
-		scaleOffset.value = DOUBLE_TAP_SCALE;
-	});
-
-	const reset = useWorkletCallback((animated) => {
-		scaleOffset.value = 1;
-
-		stopAnimation();
-
-		if (animated) {
-			offsetX.value = withSpring(0, config);
-			offsetY.value = withSpring(0, config);
-			scale.value = withSpring(1, config);
-		} else {
-			scale.value = 1;
-			translateX.value = 0;
-			translateY.value = 0;
-			offsetX.value = 0;
-			offsetY.value = 0;
-			scaleTranslateX.value = 0;
-			scaleTranslateY.value = 0;
-		}
-	});
-
-	const doubleTap = Gesture.Tap()
-		.numberOfTaps(2)
-		.maxDelay(150)
-		.maxDistance(20)
-		.onEnd((evt) => {
-			if (scale.value > 1) {
-				reset(true);
-			} else {
-				zoomToCoordinates(evt.x, evt.y);
-			}
-		});
-
-	const panGestureRef = useRef(Gesture.Pan());
-
-	const singleTap = Gesture.Tap()
-		.numberOfTaps(1)
-		.maxDuration(50)
-		.requireExternalGestureToFail(doubleTap, panGestureRef)
-		.onBegin(() => {
-			stopAnimation();
-		})
-		.onFinalize((evt, success) => {
-			if (success && onTap) {
-				runOnJS(onTap)();
-			}
-		});
-
-	const previousTouch = useSharedValue(null);
-
-	const panGesture = Gesture.Pan()
-		.manualActivation(true)
-		.averageTouches(true)
-		.onTouchesMove((evt, state) => {
-			if (scale.value > 1) {
-				state.activate();
-			}
-
-			// this is for swipe down to close
-			// needs fine tuning to work properly
-			// if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
-			//     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
-			//     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
-
-			//     // this needs tuning
-			//     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
-			//         state.activate();
-
-			//         isSwiping.value = true;
-			//         previousTouch.value = null;
-			//     }
-			// }
-
-			// if (previousTouch.value == null) {
-			//     previousTouch.value = {
-			//         x: evt.allTouches[0].x,
-			//         y: evt.allTouches[0].y,
-			//     };
-			// }
-		})
-		.simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
-		.onBegin(() => {
-			stopAnimation();
-		})
-		.onChange((evt) => {
-			// since we running both pinch and pan gesture handlers simultaneously
-			// we need to make sure that we don't pan when we pinch and move fingers
-			// since we track it as pinch focal gesture
-			if (evt.numberOfPointers > 1 || isScrolling.value) {
-				return;
-			}
-
-			panVelocityX.value = evt.velocityX;
-
-			panVelocityY.value = evt.velocityY;
-
-			if (!isSwiping.value) {
-				translateX.value += evt.changeX;
-			}
-
-			if (canPanVertically.value || isSwiping.value) {
-				translateY.value += evt.changeY;
-			}
-		})
-		.onEnd((evt) => {
-			previousTouch.value = null;
-
-			if (isScrolling.value) {
-				return;
-			}
-
-			offsetX.value += translateX.value;
-			offsetY.value += translateY.value;
-			translateX.value = 0;
-			translateY.value = 0;
-
-			if (isSwiping.value) {
-				const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
-				const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
-
-				if (enoughVelocity && rightDirection) {
-					const maybeInvert = (v) => {
-						const invert = evt.velocityY < 0;
-						return invert ? -v : v;
-					};
-
-					offsetY.value = withSpring(
-						maybeInvert(targetHeight.value * 2),
-						{
-							stiffness: 50,
-							damping: 30,
-							mass: 1,
-							overshootClamping: true,
-							restDisplacementThreshold: 300,
-							restSpeedThreshold: 300,
-							velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
-						},
-						() => {
-							console.log('success');
-							runOnJS(onSwipeSuccess)();
-						},
-					);
-					return;
-				}
-			}
-
-			afterGesture();
-
-			panVelocityX.value = 0;
-			panVelocityY.value = 0;
-		})
-		.withRef(panGestureRef);
-
-	const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
-		x: focalX - (canvasX.value / 2 + offsetX.value),
-		y: focalY - (canvasY.value / 2 + offsetY.value),
-	}));
-
-	// used to store event scale value when we limit scale
-	const gestureScale = useSharedValue(1);
-
-	const pinchGesture = Gesture.Pinch()
-		.onTouchesDown((evt, state) => {
-			// we don't want to activate pinch gesture when we are scrolling pager
-			if (isScrolling.value) {
-				state.fail();
-			}
-		})
-		.simultaneousWithExternalGesture(panGesture, doubleTap)
-		.onStart((evt) => {
-			stopAnimation();
-
-			const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-			origin.x.value = adjustFocal.x;
-			origin.y.value = adjustFocal.y;
-		})
-		.onChange((evt) => {
-			scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
-
-			if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
-				gestureScale.value = evt.scale;
-			}
-
-			const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-			scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
-			scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
-		})
-		.onEnd(() => {
-			offsetX.value += scaleTranslateX.value;
-			offsetY.value += scaleTranslateY.value;
-			scaleTranslateX.value = 0;
-			scaleTranslateY.value = 0;
-			scaleOffset.value = scale.value;
-			gestureScale.value = 1;
-
-			if (scaleOffset.value < 1) {
-				// make sure we don't add stuff below the 1
-				scaleOffset.value = 1;
-
-				// this runs the timing animation
-				scale.value = withSpring(1, config);
-			} else if (scaleOffset.value > MAX_SCALE) {
-				scaleOffset.value = MAX_SCALE;
-				scale.value = withSpring(MAX_SCALE, config);
-			}
-
-			afterGesture();
-		});
-
-	const animatedStyles = useAnimatedStyle(() => {
-		const x = scaleTranslateX.value + translateX.value + offsetX.value;
-		const y = scaleTranslateY.value + translateY.value + offsetY.value;
-
-		if (isSwiping.value) {
-			onSwipe(y);
-		}
-
-		return {
-			transform: [
-				{
-					translateX: x,
-				},
-				{
-					translateY: y,
-				},
-				{ scale: scale.value },
-			],
-		};
-	}, []);
-
-	// reacts to scale change and enables/disables pager scroll
-	useAnimatedReaction(
-		() => scale.value,
-		() => {
-			shouldPagerScroll.value = scale.value === 1;
-		},
-	);
-
-	const mounted = useRef(false);
-	useEffect(() => {
-		if (!mounted.current) {
-			mounted.current = true;
-			return;
-		}
-
-		if (!isActive) {
-			runOnUI(reset)(false);
-		}
-	}, [isActive, mounted, reset]);
-
-	return (
-		<View
-			collapsable={false}
-			style={{
-				flex: 1,
-				width: canvasWidth,
-			}}
-		>
-			<GestureDetector gesture={pinchGesture}>
-				<Animated.View
-					collapsable
-					style={StyleSheet.absoluteFill}
-				>
-					<GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
-						<Animated.View
-							collapsable
-							style={[StyleSheet.absoluteFill]}
-						>
-							<ImageWrapper>
-								<GestureDetector gesture={doubleTap}>
-									<Animated.View
-										collapsable={false}
-										style={[animatedStyles]}
-									>
-										{showFallback && renderFallback()}
-
-										{renderImage({
-											onResolveImageDimensions: onLoad,
-											...imageDimensions,
-											style: showFallback ? { opacity: 0, position: 'absolute' } : {},
-										})}
-									</Animated.View>
-								</GestureDetector>
-							</ImageWrapper>
-						</Animated.View>
-					</GestureDetector>
-				</Animated.View>
-			</GestureDetector>
-		</View>
-	);
+    const [imageDimensions, setImageDimensions] = useState(
+        showFallback
+            ? defaultDimensions
+            : getScaledDimensions({
+                  canvasWidth,
+                  canvasHeight,
+                  imageWidth,
+                  imageHeight,
+              }),
+    );
+
+    const canvasX = useSharedValue(canvasWidth);
+    const canvasY = useSharedValue(canvasHeight);
+
+    useEffect(() => {
+        runOnUI(() => {
+            'worklet';
+
+            canvasX.value = canvasWidth;
+            canvasY.value = canvasHeight;
+        })();
+    }, [canvasX, canvasY, canvasWidth, canvasHeight]);
+
+    const targetHeight = useSharedValue(0);
+
+    const onLoad = (resolvedDimensions) => {
+        const {width, height} = getScaledDimensions({
+            canvasWidth,
+            canvasHeight,
+            imageWidth: resolvedDimensions.width,
+            imageHeight: resolvedDimensions.height,
+        });
+
+        targetHeight.value = height;
+
+        setImageDimensions({
+            width,
+            height,
+        });
+
+        setShowFallback(false);
+    };
+
+    // used for pan gesture
+
+    const translateY = useSharedValue(0);
+    const translateX = useSharedValue(0);
+    const offsetX = useSharedValue(0);
+    const offsetY = useSharedValue(0);
+    const isSwiping = useSharedValue(false);
+
+    // used for moving fingers when pinching
+    const scaleTranslateX = useSharedValue(0);
+    const scaleTranslateY = useSharedValue(0);
+
+    // storage for the the origin of the gesture
+    const origin = {
+        x: useSharedValue(0),
+        y: useSharedValue(0),
+    };
+
+    // storage for the pan velocity to calculate the decay
+    const panVelocityX = useSharedValue(0);
+    const panVelocityY = useSharedValue(0);
+
+    const scale = useSharedValue(1);
+    // store scale in between gestures
+    const scaleOffset = useSharedValue(1);
+
+    // disable pan vertically when image is smaller than screen
+    const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
+
+    // calculates bounds of the scaled image
+    // can we pan left/right/up/down
+    // can be used to limit gesture or implementing tension effect
+    const getBounds = useWorkletCallback(() => {
+        const target = {
+            x: 0,
+            y: 0,
+        };
+
+        const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
+        const scaledImage = targetHeight.value * fixedScale;
+
+        const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
+
+        let topBoundary = 0;
+
+        if (canvasY.value < scaledImage) {
+            topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
+        }
+
+        const maxVector = {x: rightBoundary, y: topBoundary};
+        const minVector = {x: -rightBoundary, y: -topBoundary};
+
+        target.x = clamp(offsetX.value, minVector.x, maxVector.x);
+        target.y = clamp(offsetY.value, minVector.y, maxVector.y);
+
+        const isInBoundaryX = target.x === offsetX.value;
+        const isInBoundaryY = target.y === offsetY.value;
+
+        return {
+            target,
+            isInBoundaryX,
+            isInBoundaryY,
+            minVector,
+            maxVector,
+            canPanLeft: target.x < maxVector.x,
+            canPanRight: target.x > minVector.x,
+        };
+    });
+
+    const afterGesture = useWorkletCallback(() => {
+        const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
+
+        if (!canPanVertically.value) {
+            offsetY.value = withSpring(target.y, config);
+        }
+
+        if (
+            scale.value === 1 &&
+            offsetX.value === 0 &&
+            offsetY.value === 0 &&
+            translateX.value === 0 &&
+            translateY.value === 0 &&
+            scaleTranslateX.value === 0 &&
+            scaleTranslateY.value === 0
+        ) {
+            // we don't need to run any animations
+            return;
+        }
+
+        if (scale.value <= 1) {
+            // just center it
+            // reset(true);
+            offsetX.value = withSpring(0, config);
+            offsetY.value = withSpring(0, config);
+            return;
+        }
+
+        const deceleration = 0.9915;
+
+        if (isInBoundaryX) {
+            if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
+                offsetX.value = withDecay({
+                    velocity: panVelocityX.value,
+                    clamp: [minVector.x, maxVector.x],
+                    deceleration,
+                    rubberBandEffect: false,
+                });
+            }
+        } else {
+            offsetX.value = withSpring(target.x, config);
+        }
+
+        if (isInBoundaryY) {
+            if (
+                Math.abs(panVelocityY.value) > 0 &&
+                scale.value <= MAX_SCALE &&
+                // limit vertical pan only when image is smaller than screen
+                offsetY.value !== minVector.y &&
+                offsetY.value !== maxVector.y
+            ) {
+                offsetY.value = withDecay({
+                    velocity: panVelocityY.value,
+                    clamp: [minVector.y, maxVector.y],
+                    deceleration,
+                });
+            }
+        } else {
+            offsetY.value = withSpring(target.y, config, () => {
+                isSwiping.value = false;
+            });
+        }
+    });
+
+    const stopAnimation = useWorkletCallback(() => {
+        cancelAnimation(offsetX);
+        cancelAnimation(offsetY);
+    });
+
+    const zoomToCoordinates = useWorkletCallback((x, y) => {
+        'worklet';
+
+        stopAnimation();
+
+        const usableImage = {
+            x: canvasWidth,
+            y: targetHeight.value,
+        };
+
+        const targetImageSize = {
+            x: usableImage.x * DOUBLE_TAP_SCALE,
+            y: usableImage.y * DOUBLE_TAP_SCALE,
+        };
+
+        const CENTER = {
+            x: canvasX.value / 2,
+            y: canvasY.value / 2,
+        };
+
+        const imageCenter = {
+            x: usableImage.x / 2,
+            y: usableImage.y / 2,
+        };
+
+        const focal = {x, y};
+
+        const currentOrigin = {
+            x: (targetImageSize.x / 2 - CENTER.x) * -1,
+            y: (targetImageSize.y / 2 - CENTER.y) * -1,
+        };
+
+        const koef = {
+            x: (1 / imageCenter.x) * focal.x - 1,
+            y: (1 / imageCenter.y) * focal.y - 1,
+        };
+
+        const target = {
+            x: currentOrigin.x * koef.x,
+            y: currentOrigin.y * koef.y,
+        };
+
+        if (targetImageSize.y < canvasY.value) {
+            target.y = 0;
+        }
+
+        offsetX.value = withSpring(target.x, config);
+        offsetY.value = withSpring(target.y, config);
+        scale.value = withSpring(DOUBLE_TAP_SCALE, config);
+        scaleOffset.value = DOUBLE_TAP_SCALE;
+    });
+
+    const reset = useWorkletCallback((animated) => {
+        scaleOffset.value = 1;
+
+        stopAnimation();
+
+        if (animated) {
+            offsetX.value = withSpring(0, config);
+            offsetY.value = withSpring(0, config);
+            scale.value = withSpring(1, config);
+        } else {
+            scale.value = 1;
+            translateX.value = 0;
+            translateY.value = 0;
+            offsetX.value = 0;
+            offsetY.value = 0;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+        }
+    });
+
+    const doubleTap = Gesture.Tap()
+        .numberOfTaps(2)
+        .maxDelay(150)
+        .maxDistance(20)
+        .onEnd((evt) => {
+            if (scale.value > 1) {
+                reset(true);
+            } else {
+                zoomToCoordinates(evt.x, evt.y);
+            }
+        });
+
+    const panGestureRef = useRef(Gesture.Pan());
+
+    const singleTap = Gesture.Tap()
+        .numberOfTaps(1)
+        .maxDuration(50)
+        .requireExternalGestureToFail(doubleTap, panGestureRef)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onFinalize((evt, success) => {
+            if (success && onTap) {
+                runOnJS(onTap)();
+            }
+        });
+
+    const previousTouch = useSharedValue(null);
+
+    const panGesture = Gesture.Pan()
+        .manualActivation(true)
+        .averageTouches(true)
+        .onTouchesMove((evt, state) => {
+            if (scale.value > 1) {
+                state.activate();
+            }
+
+            // this is for swipe down to close
+            // needs fine tuning to work properly
+            // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
+            //     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
+            //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
+
+            //     // this needs tuning
+            //     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
+            //         state.activate();
+
+            //         isSwiping.value = true;
+            //         previousTouch.value = null;
+            //     }
+            // }
+
+            // if (previousTouch.value == null) {
+            //     previousTouch.value = {
+            //         x: evt.allTouches[0].x,
+            //         y: evt.allTouches[0].y,
+            //     };
+            // }
+        })
+        .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onChange((evt) => {
+            // since we running both pinch and pan gesture handlers simultaneously
+            // we need to make sure that we don't pan when we pinch and move fingers
+            // since we track it as pinch focal gesture
+            if (evt.numberOfPointers > 1 || isScrolling.value) {
+                return;
+            }
+
+            panVelocityX.value = evt.velocityX;
+
+            panVelocityY.value = evt.velocityY;
+
+            if (!isSwiping.value) {
+                translateX.value += evt.changeX;
+            }
+
+            if (canPanVertically.value || isSwiping.value) {
+                translateY.value += evt.changeY;
+            }
+        })
+        .onEnd((evt) => {
+            previousTouch.value = null;
+
+            if (isScrolling.value) {
+                return;
+            }
+
+            offsetX.value += translateX.value;
+            offsetY.value += translateY.value;
+            translateX.value = 0;
+            translateY.value = 0;
+
+            if (isSwiping.value) {
+                const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
+                const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
+
+                if (enoughVelocity && rightDirection) {
+                    const maybeInvert = (v) => {
+                        const invert = evt.velocityY < 0;
+                        return invert ? -v : v;
+                    };
+
+                    offsetY.value = withSpring(
+                        maybeInvert(targetHeight.value * 2),
+                        {
+                            stiffness: 50,
+                            damping: 30,
+                            mass: 1,
+                            overshootClamping: true,
+                            restDisplacementThreshold: 300,
+                            restSpeedThreshold: 300,
+                            velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
+                        },
+                        () => {
+                            console.log('success');
+                            runOnJS(onSwipeSuccess)();
+                        },
+                    );
+                    return;
+                }
+            }
+
+            afterGesture();
+
+            panVelocityX.value = 0;
+            panVelocityY.value = 0;
+        })
+        .withRef(panGestureRef);
+
+    const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
+        x: focalX - (canvasX.value / 2 + offsetX.value),
+        y: focalY - (canvasY.value / 2 + offsetY.value),
+    }));
+
+    // used to store event scale value when we limit scale
+    const gestureScale = useSharedValue(1);
+
+    const pinchGesture = Gesture.Pinch()
+        .onTouchesDown((evt, state) => {
+            // we don't want to activate pinch gesture when we are scrolling pager
+            if (isScrolling.value) {
+                state.fail();
+            }
+        })
+        .simultaneousWithExternalGesture(panGesture, doubleTap)
+        .onStart((evt) => {
+            stopAnimation();
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            origin.x.value = adjustFocal.x;
+            origin.y.value = adjustFocal.y;
+        })
+        .onChange((evt) => {
+            scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+
+            if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
+                gestureScale.value = evt.scale;
+            }
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
+            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
+        })
+        .onEnd(() => {
+            offsetX.value += scaleTranslateX.value;
+            offsetY.value += scaleTranslateY.value;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+            scaleOffset.value = scale.value;
+            gestureScale.value = 1;
+
+            if (scaleOffset.value < 1) {
+                // make sure we don't add stuff below the 1
+                scaleOffset.value = 1;
+
+                // this runs the timing animation
+                scale.value = withSpring(1, config);
+            } else if (scaleOffset.value > MAX_SCALE) {
+                scaleOffset.value = MAX_SCALE;
+                scale.value = withSpring(MAX_SCALE, config);
+            }
+
+            afterGesture();
+        });
+
+    const animatedStyles = useAnimatedStyle(() => {
+        const x = scaleTranslateX.value + translateX.value + offsetX.value;
+        const y = scaleTranslateY.value + translateY.value + offsetY.value;
+
+        if (isSwiping.value) {
+            onSwipe(y);
+        }
+
+        return {
+            transform: [
+                {
+                    translateX: x,
+                },
+                {
+                    translateY: y,
+                },
+                {scale: scale.value},
+            ],
+        };
+    }, []);
+
+    // reacts to scale change and enables/disables pager scroll
+    useAnimatedReaction(
+        () => scale.value,
+        () => {
+            shouldPagerScroll.value = scale.value === 1;
+        },
+    );
+
+    const mounted = useRef(false);
+    useEffect(() => {
+        if (!mounted.current) {
+            mounted.current = true;
+            return;
+        }
+
+        if (!isActive) {
+            runOnUI(reset)(false);
+        }
+    }, [isActive, mounted, reset]);
+
+    return (
+        <View
+            collapsable={false}
+            style={{
+                flex: 1,
+                width: canvasWidth,
+            }}
+        >
+            <GestureDetector gesture={pinchGesture}>
+                <Animated.View
+                    collapsable
+                    style={StyleSheet.absoluteFill}
+                >
+                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
+                        <Animated.View
+                            collapsable
+                            style={[StyleSheet.absoluteFill]}
+                        >
+                            <ImageWrapper>
+                                <GestureDetector gesture={doubleTap}>
+                                    <Animated.View
+                                        collapsable={false}
+                                        style={[animatedStyles]}
+                                    >
+                                        {showFallback && renderFallback()}
+
+                                        {renderImage({
+                                            onResolveImageDimensions: onLoad,
+                                            ...imageDimensions,
+                                            style: showFallback ? {opacity: 0, position: 'absolute'} : {},
+                                        })}
+                                    </Animated.View>
+                                </GestureDetector>
+                            </ImageWrapper>
+                        </Animated.View>
+                    </GestureDetector>
+                </Animated.View>
+            </GestureDetector>
+        </View>
+    );
 }
 
-function ImageWrapper({ children }) {
-	return (
-		<Animated.View
-			collapsable={false}
-			style={[
-				{
-					...StyleSheet.absoluteFillObject,
-					justifyContent: 'center',
-					alignItems: 'center',
-				},
-			]}
-		>
-			{children}
-		</Animated.View>
-	);
+function ImageWrapper({children}) {
+    return (
+        <Animated.View
+            collapsable={false}
+            style={[
+                {
+                    ...StyleSheet.absoluteFillObject,
+                    justifyContent: 'center',
+                    alignItems: 'center',
+                },
+            ]}
+        >
+            {children}
+        </Animated.View>
+    );
 }
 
 const cachedDimensions = new Map();
 
-function Page({ isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight, onTap }) {
-	const dimensions = cachedDimensions.get(item.url);
-
-	if (!isActive) {
-		return (
-			<ImageWrapper>
-				<Image
-					source={{ uri: item.url }}
-					onLoad={(evt) => {
-
-
-						cachedDimensions.set(item.url, {
-							width: evt.nativeEvent?.width,
-							height: evt.nativeEvent?.height,
-						});
-					}}
-					style={
-						dimensions
-							? getScaledDimensions({
-								imageHeight: dimensions?.height,
-								imageWidth: dimensions?.width,
-								canvasHeight,
-								canvasWidth,
-							})
-							: {}
-					}
-				/>
-			</ImageWrapper>
-		);
-	}
-
-	return (
-		<ImageTransformer
-			onSwipe={onSwipe}
-			onSwipeSuccess={onSwipeSuccess}
-			isActive
-			onTap={onTap}
-			imageHeight={dimensions?.height}
-			imageWidth={dimensions?.width}
-			canvasHeight={canvasHeight}
-			canvasWidth={canvasWidth}
-			renderFallback={() => <ActivityIndicator />}
-			renderImage={({ onResolveImageDimensions, width, height, style }) => (
-				<Image
-					source={{ uri: item.url, width: 100, height: 100 }}
-					style={[style, { width, height }]}
-					onLoad={(evt) => {
-						cachedDimensions.set(item.url, {
-							width: evt.nativeEvent.width,
-							height: evt.nativeEvent.height,
-						});
-
-
-						onResolveImageDimensions({
-							width: evt.nativeEvent.width / PixelRatio.get(),
-							height: evt.nativeEvent.height / PixelRatio.get(),
-						});
-					}}
-				/>
-			)}
-		/>
-	);
+function Page({isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight, onTap}) {
+    const dimensions = cachedDimensions.get(item.url);
+
+    if (!isActive) {
+        return (
+            <ImageWrapper>
+                <Image
+                    source={{uri: item.url}}
+                    onLoad={(evt) => {
+                        cachedDimensions.set(item.url, {
+                            width: evt.nativeEvent?.width,
+                            height: evt.nativeEvent?.height,
+                        });
+                    }}
+                    style={
+                        dimensions
+                            ? getScaledDimensions({
+                                  imageHeight: dimensions?.height,
+                                  imageWidth: dimensions?.width,
+                                  canvasHeight,
+                                  canvasWidth,
+                              })
+                            : {}
+                    }
+                />
+            </ImageWrapper>
+        );
+    }
+
+    return (
+        <ImageTransformer
+            onSwipe={onSwipe}
+            onSwipeSuccess={onSwipeSuccess}
+            isActive
+            onTap={onTap}
+            imageHeight={dimensions?.height}
+            imageWidth={dimensions?.width}
+            canvasHeight={canvasHeight}
+            canvasWidth={canvasWidth}
+            renderFallback={() => <ActivityIndicator />}
+            renderImage={({onResolveImageDimensions, width, height, style}) => (
+                <Image
+                    source={{uri: item.url, width: 100, height: 100}}
+                    style={[style, {width, height}]}
+                    onLoad={(evt) => {
+                        cachedDimensions.set(item.url, {
+                            width: evt.nativeEvent.width,
+                            height: evt.nativeEvent.height,
+                        });
+
+                        onResolveImageDimensions({
+                            width: evt.nativeEvent.width / PixelRatio.get(),
+                            height: evt.nativeEvent.height / PixelRatio.get(),
+                        });
+                    }}
+                />
+            )}
+        />
+    );
 }
 
 const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
 
 function usePageScrollHandler(handlers, dependencies) {
-	const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
-	const subscribeForEvents = ['onPageScroll'];
-
-	return useEvent(
-		(event) => {
-			'worklet';
-
-			const { onPageScroll } = handlers;
-			if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
-				onPageScroll(event, context);
-			}
-		},
-		subscribeForEvents,
-		doDependenciesDiffer,
-	);
+    const {context, doDependenciesDiffer} = useHandler(handlers, dependencies);
+    const subscribeForEvents = ['onPageScroll'];
+
+    return useEvent(
+        (event) => {
+            'worklet';
+
+            const {onPageScroll} = handlers;
+            if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
+                onPageScroll(event, context);
+            }
+        },
+        subscribeForEvents,
+        doDependenciesDiffer,
+    );
 }
 
 const noopWorklet = () => {
-	'worklet';
+    'worklet';
 
-	// noop
+    // noop
 };
 
-export default function Pager({ items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => { } }) {
-	const windowDimensions = useWindowDimensions();
-
-	const shouldPagerScroll = useSharedValue(true);
-	const pagerRef = useRef(null);
-
-	const isScrolling = useSharedValue(false);
-	const activeIndex = useSharedValue(initialIndex);
-
-	const pageScrollHandler = usePageScrollHandler(
-		{
-			onPageScroll: (e) => {
-				'worklet';
-
-				activeIndex.value = e.position;
-				isScrolling.value = e.offset !== 0;
-			},
-		},
-		[],
-	);
-
-	const [activePage, setActivePage] = useState(initialIndex);
-
-	// we use reanimated for this since onPageSelected is called
-	// in the middle of the pager animation
-	useAnimatedReaction(
-		() => isScrolling.value,
-		(stillScrolling) => {
-			if (stillScrolling) {
-				return;
-			}
-
-			runOnJS(setActivePage)(activeIndex.value);
-		},
-	);
-
-	const animatedProps = useAnimatedProps(() => ({
-		scrollEnabled: shouldPagerScroll.value,
-	}));
-
-	const processedItems = items
-		.map((item, index) => itemExtractor({ item, index }))
-
-	return (
-		<View style={{ flex: 1 }}>
-			<View style={{ flex: 1 }}>
-				<Context.Provider
-					value={{
-						isScrolling,
-						pagerRef,
-						shouldPagerScroll,
-					}}
-				>
-					<AnimatedPagerView
-						pageMargin={40}
-						onPageScroll={pageScrollHandler}
-						animatedProps={animatedProps}
-						ref={pagerRef}
-						style={{ flex: 1 }}
-						initialPage={initialIndex}
-					>
-						{
-							processedItems.map((item, index) => (
-								<View
-									key={item.key}
-									style={{ flex: 1 }}
-								>
-									<Page
-										onTap={onTap}
-										onSwipe={onSwipe}
-										onSwipeSuccess={onSwipeSuccess}
-										isActive={index === activePage}
-										item={item}
-										canvasHeight={windowDimensions.height}
-										canvasWidth={windowDimensions.width}
-									/>
-								</View>
-							))}
-					</AnimatedPagerView>
-				</Context.Provider>
-			</View>
-		</View>
-	);
+export default function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}}) {
+    const windowDimensions = useWindowDimensions();
+
+    const shouldPagerScroll = useSharedValue(true);
+    const pagerRef = useRef(null);
+
+    const isScrolling = useSharedValue(false);
+    const activeIndex = useSharedValue(initialIndex);
+
+    const pageScrollHandler = usePageScrollHandler(
+        {
+            onPageScroll: (e) => {
+                'worklet';
+
+                activeIndex.value = e.position;
+                isScrolling.value = e.offset !== 0;
+            },
+        },
+        [],
+    );
+
+    const [activePage, setActivePage] = useState(initialIndex);
+
+    // we use reanimated for this since onPageSelected is called
+    // in the middle of the pager animation
+    useAnimatedReaction(
+        () => isScrolling.value,
+        (stillScrolling) => {
+            if (stillScrolling) {
+                return;
+            }
+
+            runOnJS(setActivePage)(activeIndex.value);
+        },
+    );
+
+    const animatedProps = useAnimatedProps(() => ({
+        scrollEnabled: shouldPagerScroll.value,
+    }));
+
+    const processedItems = items.map((item, index) => itemExtractor({item, index}));
+
+    return (
+        <View style={{flex: 1}}>
+            <View style={{flex: 1}}>
+                <Context.Provider
+                    value={{
+                        isScrolling,
+                        pagerRef,
+                        shouldPagerScroll,
+                    }}
+                >
+                    <AnimatedPagerView
+                        pageMargin={40}
+                        onPageScroll={pageScrollHandler}
+                        animatedProps={animatedProps}
+                        ref={pagerRef}
+                        style={{flex: 1}}
+                        initialPage={initialIndex}
+                    >
+                        {processedItems.map((item, index) => (
+                            <View
+                                key={item.key}
+                                style={{flex: 1}}
+                            >
+                                <Page
+                                    onTap={onTap}
+                                    onSwipe={onSwipe}
+                                    onSwipeSuccess={onSwipeSuccess}
+                                    isActive={index === activePage}
+                                    item={item}
+                                    canvasHeight={windowDimensions.height}
+                                    canvasWidth={windowDimensions.width}
+                                />
+                            </View>
+                        ))}
+                    </AnimatedPagerView>
+                </Context.Provider>
+            </View>
+        </View>
+    );
 }
diff --git a/src/components/AttachmentCarousel/index.native.js b/src/components/AttachmentCarousel/index.native.js
index 2cf732659403..b57d9e1c971d 100644
--- a/src/components/AttachmentCarousel/index.native.js
+++ b/src/components/AttachmentCarousel/index.native.js
@@ -1,7 +1,7 @@
 import React from 'react';
-import { View, FlatList } from 'react-native';
+import {View, FlatList} from 'react-native';
 import PropTypes from 'prop-types';
-import { withOnyx } from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
 import _ from 'underscore';
 import * as Expensicons from '../Icon/Expensicons';
 import styles from '../../styles/styles';
@@ -17,341 +17,339 @@ import ONYXKEYS from '../../ONYXKEYS';
 import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
 import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
 import Tooltip from '../Tooltip';
-import withLocalize, { withLocalizePropTypes } from '../withLocalize';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
 import compose from '../../libs/compose';
 import withWindowDimensions from '../withWindowDimensions';
 import Pager from './Lightbox';
 
 const propTypes = {
-	/** source is used to determine the starting index in the array of attachments */
-	source: PropTypes.string,
+    /** source is used to determine the starting index in the array of attachments */
+    source: PropTypes.string,
 
-	/** Callback to update the parent modal's state with a source and name from the attachments array */
-	onNavigate: PropTypes.func,
+    /** Callback to update the parent modal's state with a source and name from the attachments array */
+    onNavigate: PropTypes.func,
 
-	/** Object of report actions for this report */
-	reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+    /** Object of report actions for this report */
+    reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
 
-	...withLocalizePropTypes,
+    ...withLocalizePropTypes,
 };
 
 const defaultProps = {
-	source: '',
-	reportActions: {},
-	onNavigate: () => { },
+    source: '',
+    reportActions: {},
+    onNavigate: () => {},
 };
 
 class AttachmentCarousel extends React.Component {
-	constructor(props) {
-		super(props);
-
-		this.scrollRef = React.createRef();
-		this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
-		this.viewabilityConfig = {
-			// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-			// more than 90% visible. When that happens we update the page index in the state.
-			itemVisiblePercentThreshold: 95,
-		};
-
-		this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
-		this.autoHideArrow = this.autoHideArrow.bind(this);
-		this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
-		this.getItemLayout = this.getItemLayout.bind(this);
-		this.renderItem = this.renderItem.bind(this);
-		this.renderCell = this.renderCell.bind(this);
-		this.updatePage = this.updatePage.bind(this);
-		this.updateZoomState = this.updateZoomState.bind(this);
-		this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
-
-		this.state = this.makeInitialState();
-	}
-
-	componentDidMount() {
-		this.autoHideArrow();
-	}
-
-	/**
-	 * Helps to navigate between next/previous attachments
-	 * @param {Object} attachmentItem
-	 * @returns {Object}
-	 */
-	getAttachment(attachmentItem) {
-		const source = _.get(attachmentItem, 'source', '');
-		const file = _.get(attachmentItem, 'file', { name: '' });
-
-		return {
-			source,
-			file,
-		};
-	}
-
-	/**
-	 * Calculate items layout information to optimize scrolling performance
-	 * @param {*} data
-	 * @param {Number} index
-	 * @returns {{offset: Number, length: Number, index: Number}}
-	 */
-	getItemLayout(data, index) {
-		return {
-			length: this.state.containerWidth,
-			offset: this.state.containerWidth * index,
-			index,
-		};
-	}
-
-	/**
-	 * On a touch screen device, automatically hide the arrows
-	 * if there is no interaction for 3 seconds.
-	 */
-	autoHideArrow() {
-		if (!this.canUseTouchScreen) {
-			return;
-		}
-		this.cancelAutoHideArrow();
-		this.autoHideArrowTimeout = setTimeout(() => {
-			this.toggleArrowsVisibility(false);
-		}, CONST.ARROW_HIDE_DELAY);
-	}
-
-	/**
-	 * Cancels the automatic hiding of the arrows.
-	 */
-	cancelAutoHideArrow() {
-		clearTimeout(this.autoHideArrowTimeout);
-	}
-
-	/**
-	 * Toggles the visibility of the arrows
-	 * @param {Boolean} shouldShowArrow
-	 */
-	toggleArrowsVisibility(shouldShowArrow) {
-		// Don't toggle arrows in a zoomed state
-		if (this.state.isZoomed) {
-			return;
-		}
-		this.setState(
-			(current) => {
-				const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-				return { shouldShowArrow: newShouldShowArrow };
-			},
-			() => {
-				if (this.state.shouldShowArrow) {
-					this.autoHideArrow();
-				} else {
-					this.cancelAutoHideArrow();
-				}
-			},
-		);
-	}
-
-	/**
-	 * Updates zoomed state to enable/disable panning the PDF
-	 * @param {Number} scale current PDF scale
-	 */
-	updateZoomState(scale) {
-		const isZoomed = scale > 1;
-		if (isZoomed === this.state.isZoomed) {
-			return;
-		}
-		if (isZoomed) {
-			this.toggleArrowsVisibility(false);
-		}
-		this.setState({ isZoomed });
-	}
-
-	/**
-	 * Map report actions to attachment items and sets the initial carousel state
-	 * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
-	 */
-	makeInitialState() {
-		let page = 0;
-		const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
-
-		/**
-		 * Looping to filter out attachments and retrieve the src URL and name of attachments.
-		 */
-		const attachments = [];
-		_.forEach(actions, ({ originalMessage, message }) => {
-			// Check for attachment which hasn't been deleted
-			if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
-				return;
-			}
-			const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
-
-			// matchAll captured both source url and name of the attachment
-			if (matches.length === 2) {
-				const [originalSource, name] = _.map(matches, (m) => m[2]);
-
-				// Update the image URL so the images can be accessed depending on the config environment.
-				// Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
-				const source = tryResolveUrlFromApiRoot(originalSource);
-				if (source === this.props.source) {
-					page = attachments.length;
-				}
-
-				attachments.push({ source, file: { name } });
-			}
-		});
-
-		return {
-			page,
-			attachments,
-			shouldShowArrow: this.canUseTouchScreen,
-			containerWidth: 0,
-			isZoomed: false,
-		};
-	}
-
-	/**
-	 * Increments or decrements the index to get another selected item
-	 * @param {Number} deltaSlide
-	 */
-	cycleThroughAttachments(deltaSlide) {
-		const nextIndex = this.state.page - deltaSlide;
-		const nextItem = this.state.attachments[nextIndex];
-
-		if (!nextItem) {
-			return;
-		}
-
-		// The sliding transition is a bit too much on web, because of the wider and bigger images,
-		// so we only enable it for mobile
-		this.scrollRef.current.scrollToIndex({ index: nextIndex, animated: this.canUseTouchScreen });
-	}
-
-	/**
-	 * Updates the page state when the user navigates between attachments
-	 * @param {Array<{item: *, index: Number}>} viewableItems
-	 */
-	updatePage({ viewableItems }) {
-		// Since we can have only one item in view at a time, we can use the first item in the array
-		// to get the index of the current page
-		const entry = _.first(viewableItems);
-		if (!entry) {
-			return;
-		}
-
-		const page = entry.index;
-		const { source, file } = this.getAttachment(entry.item);
-		this.props.onNavigate({ source: addEncryptedAuthTokenToURL(source), file });
-		this.setState({ page, isZoomed: false });
-	}
-
-	/**
-	 * Defines how a container for a single attachment should be rendered
-	 * @param {Object} props
-	 * @returns {JSX.Element}
-	 */
-	renderCell(props) {
-		const style = [props.style, styles.h100, { width: this.state.containerWidth }];
-
-		return (
-			<View
-				// eslint-disable-next-line react/jsx-props-no-spreading
-				{...props}
-				style={style}
-			/>
-		);
-	}
-
-	/**
-	 * Defines how a single attachment should be rendered
-	 * @param {{ source: String, file: { name: String } }} item
-	 * @returns {JSX.Element}
-	 */
-	renderItem({ item }) {
-		const authSource = addEncryptedAuthTokenToURL(item.source);
-		if (!this.canUseTouchScreen) {
-			return (
-				<AttachmentView
-					source={authSource}
-					file={item.file}
-				/>
-			);
-		}
-
-		return (
-			<AttachmentView
-				source={authSource}
-				file={item.file}
-				onScaleChanged={this.updateZoomState}
-				onPress={this.toggleArrowsVisibility}
-			/>
-		);
-	}
-
-	render() {
-		const isForwardDisabled = this.state.page === 0;
-		const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
-
-		return (
-			<View
-				style={[styles.attachmentModalArrowsContainer, styles.flex1]}
-			>
-				{this.state.shouldShowArrow && (
-					<>
-						{!isBackDisabled && (
-							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
-								<Tooltip text={this.props.translate('common.previous')}>
-									<Button
-										small
-										innerStyles={[styles.arrowIcon]}
-										icon={Expensicons.BackArrow}
-										iconFill={themeColors.text}
-										iconStyles={[styles.mr0]}
-										onPress={() => {
-											this.cycleThroughAttachments(-1);
-											this.autoHideArrow();
-										}}
-										onPressIn={this.cancelAutoHideArrow}
-										onPressOut={this.autoHideArrow}
-									/>
-								</Tooltip>
-							</View>
-						)}
-						{!isForwardDisabled && (
-							<View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
-								<Tooltip text={this.props.translate('common.next')}>
-									<Button
-										small
-										innerStyles={[styles.arrowIcon]}
-										icon={Expensicons.ArrowRight}
-										iconFill={themeColors.text}
-										iconStyles={[styles.mr0]}
-										onPress={() => {
-											this.cycleThroughAttachments(1);
-											this.autoHideArrow();
-										}}
-										onPressIn={this.cancelAutoHideArrow}
-										onPressOut={this.autoHideArrow}
-									/>
-								</Tooltip>
-							</View>
-						)}
-					</>
-				)}
-
-				<Pager
-					items={this.state.attachments}
-					initialIndex={this.state.page}
-					onTap={this.toggleArrowsVisibility}
-					itemExtractor={({ item }) => ({ key: item.source, url: addEncryptedAuthTokenToURL(item.source) })}
-				/>
-			</View>
-		);
-	}
+    constructor(props) {
+        super(props);
+
+        this.scrollRef = React.createRef();
+        this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
+        this.viewabilityConfig = {
+            // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+            // more than 90% visible. When that happens we update the page index in the state.
+            itemVisiblePercentThreshold: 95,
+        };
+
+        this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
+        this.autoHideArrow = this.autoHideArrow.bind(this);
+        this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
+        this.getItemLayout = this.getItemLayout.bind(this);
+        this.renderItem = this.renderItem.bind(this);
+        this.renderCell = this.renderCell.bind(this);
+        this.updatePage = this.updatePage.bind(this);
+        this.updateZoomState = this.updateZoomState.bind(this);
+        this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
+
+        this.state = this.makeInitialState();
+    }
+
+    componentDidMount() {
+        this.autoHideArrow();
+    }
+
+    /**
+     * Helps to navigate between next/previous attachments
+     * @param {Object} attachmentItem
+     * @returns {Object}
+     */
+    getAttachment(attachmentItem) {
+        const source = _.get(attachmentItem, 'source', '');
+        const file = _.get(attachmentItem, 'file', {name: ''});
+
+        return {
+            source,
+            file,
+        };
+    }
+
+    /**
+     * Calculate items layout information to optimize scrolling performance
+     * @param {*} data
+     * @param {Number} index
+     * @returns {{offset: Number, length: Number, index: Number}}
+     */
+    getItemLayout(data, index) {
+        return {
+            length: this.state.containerWidth,
+            offset: this.state.containerWidth * index,
+            index,
+        };
+    }
+
+    /**
+     * On a touch screen device, automatically hide the arrows
+     * if there is no interaction for 3 seconds.
+     */
+    autoHideArrow() {
+        if (!this.canUseTouchScreen) {
+            return;
+        }
+        this.cancelAutoHideArrow();
+        this.autoHideArrowTimeout = setTimeout(() => {
+            this.toggleArrowsVisibility(false);
+        }, CONST.ARROW_HIDE_DELAY);
+    }
+
+    /**
+     * Cancels the automatic hiding of the arrows.
+     */
+    cancelAutoHideArrow() {
+        clearTimeout(this.autoHideArrowTimeout);
+    }
+
+    /**
+     * Toggles the visibility of the arrows
+     * @param {Boolean} shouldShowArrow
+     */
+    toggleArrowsVisibility(shouldShowArrow) {
+        // Don't toggle arrows in a zoomed state
+        if (this.state.isZoomed) {
+            return;
+        }
+        this.setState(
+            (current) => {
+                const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
+                return {shouldShowArrow: newShouldShowArrow};
+            },
+            () => {
+                if (this.state.shouldShowArrow) {
+                    this.autoHideArrow();
+                } else {
+                    this.cancelAutoHideArrow();
+                }
+            },
+        );
+    }
+
+    /**
+     * Updates zoomed state to enable/disable panning the PDF
+     * @param {Number} scale current PDF scale
+     */
+    updateZoomState(scale) {
+        const isZoomed = scale > 1;
+        if (isZoomed === this.state.isZoomed) {
+            return;
+        }
+        if (isZoomed) {
+            this.toggleArrowsVisibility(false);
+        }
+        this.setState({isZoomed});
+    }
+
+    /**
+     * Map report actions to attachment items and sets the initial carousel state
+     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
+     */
+    makeInitialState() {
+        let page = 0;
+        const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
+
+        /**
+         * Looping to filter out attachments and retrieve the src URL and name of attachments.
+         */
+        const attachments = [];
+        _.forEach(actions, ({originalMessage, message}) => {
+            // Check for attachment which hasn't been deleted
+            if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
+                return;
+            }
+            const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
+
+            // matchAll captured both source url and name of the attachment
+            if (matches.length === 2) {
+                const [originalSource, name] = _.map(matches, (m) => m[2]);
+
+                // Update the image URL so the images can be accessed depending on the config environment.
+                // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
+                const source = tryResolveUrlFromApiRoot(originalSource);
+                if (source === this.props.source) {
+                    page = attachments.length;
+                }
+
+                attachments.push({source, file: {name}});
+            }
+        });
+
+        return {
+            page,
+            attachments,
+            shouldShowArrow: this.canUseTouchScreen,
+            containerWidth: 0,
+            isZoomed: false,
+        };
+    }
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    cycleThroughAttachments(deltaSlide) {
+        const nextIndex = this.state.page - deltaSlide;
+        const nextItem = this.state.attachments[nextIndex];
+
+        if (!nextItem) {
+            return;
+        }
+
+        // The sliding transition is a bit too much on web, because of the wider and bigger images,
+        // so we only enable it for mobile
+        this.scrollRef.current.scrollToIndex({index: nextIndex, animated: this.canUseTouchScreen});
+    }
+
+    /**
+     * Updates the page state when the user navigates between attachments
+     * @param {Array<{item: *, index: Number}>} viewableItems
+     */
+    updatePage({viewableItems}) {
+        // Since we can have only one item in view at a time, we can use the first item in the array
+        // to get the index of the current page
+        const entry = _.first(viewableItems);
+        if (!entry) {
+            return;
+        }
+
+        const page = entry.index;
+        const {source, file} = this.getAttachment(entry.item);
+        this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
+        this.setState({page, isZoomed: false});
+    }
+
+    /**
+     * Defines how a container for a single attachment should be rendered
+     * @param {Object} props
+     * @returns {JSX.Element}
+     */
+    renderCell(props) {
+        const style = [props.style, styles.h100, {width: this.state.containerWidth}];
+
+        return (
+            <View
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...props}
+                style={style}
+            />
+        );
+    }
+
+    /**
+     * Defines how a single attachment should be rendered
+     * @param {{ source: String, file: { name: String } }} item
+     * @returns {JSX.Element}
+     */
+    renderItem({item}) {
+        const authSource = addEncryptedAuthTokenToURL(item.source);
+        if (!this.canUseTouchScreen) {
+            return (
+                <AttachmentView
+                    source={authSource}
+                    file={item.file}
+                />
+            );
+        }
+
+        return (
+            <AttachmentView
+                source={authSource}
+                file={item.file}
+                onScaleChanged={this.updateZoomState}
+                onPress={this.toggleArrowsVisibility}
+            />
+        );
+    }
+
+    render() {
+        const isForwardDisabled = this.state.page === 0;
+        const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
+
+        return (
+            <View style={[styles.attachmentModalArrowsContainer, styles.flex1]}>
+                {this.state.shouldShowArrow && (
+                    <>
+                        {!isBackDisabled && (
+                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
+                                <Tooltip text={this.props.translate('common.previous')}>
+                                    <Button
+                                        small
+                                        innerStyles={[styles.arrowIcon]}
+                                        icon={Expensicons.BackArrow}
+                                        iconFill={themeColors.text}
+                                        iconStyles={[styles.mr0]}
+                                        onPress={() => {
+                                            this.cycleThroughAttachments(-1);
+                                            this.autoHideArrow();
+                                        }}
+                                        onPressIn={this.cancelAutoHideArrow}
+                                        onPressOut={this.autoHideArrow}
+                                    />
+                                </Tooltip>
+                            </View>
+                        )}
+                        {!isForwardDisabled && (
+                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
+                                <Tooltip text={this.props.translate('common.next')}>
+                                    <Button
+                                        small
+                                        innerStyles={[styles.arrowIcon]}
+                                        icon={Expensicons.ArrowRight}
+                                        iconFill={themeColors.text}
+                                        iconStyles={[styles.mr0]}
+                                        onPress={() => {
+                                            this.cycleThroughAttachments(1);
+                                            this.autoHideArrow();
+                                        }}
+                                        onPressIn={this.cancelAutoHideArrow}
+                                        onPressOut={this.autoHideArrow}
+                                    />
+                                </Tooltip>
+                            </View>
+                        )}
+                    </>
+                )}
+
+                <Pager
+                    items={this.state.attachments}
+                    initialIndex={this.state.page}
+                    onTap={this.toggleArrowsVisibility}
+                    itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
+                />
+            </View>
+        );
+    }
 }
 
 AttachmentCarousel.propTypes = propTypes;
 AttachmentCarousel.defaultProps = defaultProps;
 
 export default compose(
-	withOnyx({
-		reportActions: {
-			key: ({ reportID }) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
-			canEvict: false,
-		},
-	}),
-	withLocalize,
-	withWindowDimensions,
+    withOnyx({
+        reportActions: {
+            key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+            canEvict: false,
+        },
+    }),
+    withLocalize,
+    withWindowDimensions,
 )(AttachmentCarousel);

From 24f13ac27a6284647656f570ac169731b2bcb560 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 15 Jun 2023 12:59:21 +0200
Subject: [PATCH 005/133] use correct import

---
 src/components/AttachmentModal.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 8179f4c8c1bd..c6f4ae393c9b 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -9,7 +9,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler';
 import CONST from '../CONST';
 import Modal from './Modal';
 import AttachmentView from './AttachmentView';
-import AttachmentCarousel from './AttachmentCarousel/index.native';
+import AttachmentCarousel from './AttachmentCarousel';
 import styles from '../styles/styles';
 import * as StyleUtils from '../styles/StyleUtils';
 import * as FileUtils from '../libs/fileDownload/FileUtils';

From 5866b790af1337b5027c1d0d94d50579658cc267 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 15 Jun 2023 13:21:54 +0200
Subject: [PATCH 006/133] add dimensions with scale

---
 src/components/AttachmentCarousel/Lightbox.js | 80 ++++++++++++++-----
 1 file changed, 59 insertions(+), 21 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 92a01306583e..4e98e5b14ff3 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,4 +1,4 @@
-import React, {createContext, useContext, useEffect, useRef, useState} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState, useMemo} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
 import {Gesture, GestureDetector, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
@@ -38,29 +38,47 @@ const config = {
 };
 
 function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
-    console.log({
-        canvasWidth,
-        canvasHeight,
-        imageWidth,
-        imageHeight,
-    });
+    const scaleFactorX = imageWidth / canvasWidth;
+    const scaleFactorY = imageHeight / canvasHeight;
 
-    const scaleFactor = imageWidth / canvasWidth;
+    const scaledWidth = imageWidth / scaleFactorY;
+    const scaledHeight = imageHeight / scaleFactorX;
 
-    const width = canvasWidth;
-    const height = imageHeight / scaleFactor;
+    if (scaledWidth > canvasWidth) {
+        return {
+            width: canvasWidth,
+            height: scaledHeight,
+        };
+    }
 
-    console.log({
-        canvasWidth,
-        canvasHeight,
-        imageWidth,
-        imageHeight,
-        width,
-        height,
-        scaleFactor,
-    });
+    return {
+        width: scaledWidth,
+        height: canvasHeight,
+    };
 
-    return {width, height};
+    // console.log({
+    //     canvasWidth,
+    //     canvasHeight,
+    //     imageWidth,
+    //     imageHeight,
+    // });
+
+    // const scaleFactor = imageWidth / canvasWidth;
+
+    // const width = canvasWidth;
+    // const height = imageHeight / scaleFactor;
+
+    // console.log({
+    //     canvasWidth,
+    //     canvasHeight,
+    //     imageWidth,
+    //     imageHeight,
+    //     width,
+    //     height,
+    //     scaleFactor,
+    // });
+
+    // return {width, height};
 }
 
 const defaultDimensions = {
@@ -70,6 +88,7 @@ const defaultDimensions = {
 
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling} = useContext(Context);
+    const windowDimensions = useWindowDimensions();
 
     const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
 
@@ -84,6 +103,25 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
               }),
     );
 
+    const dimensionsWithScale = useMemo(() => {
+        const scaleFactorX = windowDimensions.width / imageDimensions.width;
+        const scaleFactorY = windowDimensions.height / imageDimensions.height;
+
+        const scaledWidth = imageDimensions.width * scaleFactorY;
+
+        if (scaledWidth > windowDimensions.width) {
+            return {
+                ...imageDimensions,
+                scale: scaleFactorX,
+            };
+        }
+
+        return {
+            ...imageDimensions,
+            scale: scaleFactorY,
+        };
+    }, [imageDimensions, windowDimensions]);
+
     const canvasX = useSharedValue(canvasWidth);
     const canvasY = useSharedValue(canvasHeight);
 
@@ -585,7 +623,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
 
                                         {renderImage({
                                             onResolveImageDimensions: onLoad,
-                                            ...imageDimensions,
+                                            ...dimensionsWithScale,
                                             style: showFallback ? {opacity: 0, position: 'absolute'} : {},
                                         })}
                                     </Animated.View>

From a80bf0ecf7cb43419cdbc5fd779c3124073f7163 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 10:59:29 +0200
Subject: [PATCH 007/133] prefer early return

---
 src/components/AttachmentCarousel/Lightbox.js | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 4e98e5b14ff3..0343b69b611e 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -17,6 +17,7 @@ import Animated, {
     withSpring,
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
+import _ from 'underscore';
 import Image from '../Image';
 
 const Context = createContext(null);
@@ -386,9 +387,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             stopAnimation();
         })
         .onFinalize((evt, success) => {
-            if (success && onTap) {
-                runOnJS(onTap)();
-            }
+            if (!success || !onTap) return;
+
+            runOnJS(onTap)();
         });
 
     const previousTouch = useSharedValue(null);
@@ -507,9 +508,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     const pinchGesture = Gesture.Pinch()
         .onTouchesDown((evt, state) => {
             // we don't want to activate pinch gesture when we are scrolling pager
-            if (isScrolling.value) {
-                state.fail();
-            }
+            if (!isScrolling.value) return;
+
+            state.fail();
         })
         .simultaneousWithExternalGesture(panGesture, doubleTap)
         .onStart((evt) => {
@@ -783,7 +784,7 @@ export default function Pager({items, initialIndex = 0, onTap, itemExtractor, on
         scrollEnabled: shouldPagerScroll.value,
     }));
 
-    const processedItems = items.map((item, index) => itemExtractor({item, index}));
+    const processedItems = _.map(items, (item, index) => itemExtractor({item, index}));
 
     return (
         <View style={{flex: 1}}>
@@ -803,7 +804,7 @@ export default function Pager({items, initialIndex = 0, onTap, itemExtractor, on
                         style={{flex: 1}}
                         initialPage={initialIndex}
                     >
-                        {processedItems.map((item, index) => (
+                        {_.map(processedItems, (item, index) => (
                             <View
                                 key={item.key}
                                 style={{flex: 1}}

From e9b7c09841c6b80966975ede5433d2d202f19234 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 12:12:20 +0200
Subject: [PATCH 008/133] simplify AttachmentCarousel structure

---
 .../AttachmentCarouselView/index.js           | 156 ++++++++
 .../AttachmentCarouselView/index.native.js    |  66 ++++
 .../AttachmentCarousel/CarouselButtons.js     |  71 ++++
 src/components/AttachmentCarousel/index.js    | 186 +--------
 .../AttachmentCarousel/index.native.js        | 355 ------------------
 src/components/AttachmentModal.js             |   1 -
 6 files changed, 302 insertions(+), 533 deletions(-)
 create mode 100644 src/components/AttachmentCarousel/AttachmentCarouselView/index.js
 create mode 100644 src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
 create mode 100644 src/components/AttachmentCarousel/CarouselButtons.js
 delete mode 100644 src/components/AttachmentCarousel/index.native.js

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
new file mode 100644
index 000000000000..1114f26f02b5
--- /dev/null
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -0,0 +1,156 @@
+import React, {useRef, useCallback} from 'react';
+import {View, FlatList, PixelRatio} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+import CarouselActions from '../CarouselActions';
+import AttachmentView from '../../AttachmentView';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
+import CarouselButtons from '../CarouselButtons';
+
+const propTypes = {
+    // eslint-disable-next-line react/forbid-prop-types
+    carouselState: PropTypes.object.isRequired,
+
+    updatePage: PropTypes.func.isRequired,
+    toggleArrowsVisibility: PropTypes.func.isRequired,
+    autoHideArrow: PropTypes.func.isRequired,
+    cancelAutoHideArrow: PropTypes.func.isRequired,
+
+    ...windowDimensionsPropTypes,
+};
+
+function AttachmentCarouselView(props) {
+    const scrollRef = useRef(null);
+    const viewabilityConfig = {
+        // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+        // more than 90% visible. When that happens we update the page index in the state.
+        itemVisiblePercentThreshold: 95,
+    };
+
+    /**
+     * Calculate items layout information to optimize scrolling performance
+     * @param {*} data
+     * @param {Number} index
+     * @returns {{offset: Number, length: Number, index: Number}}
+     */
+    const getItemLayout = useCallback(
+        (data, index) => ({
+            length: props.carouselState.containerWidth,
+            offset: props.carouselState.containerWidth * index,
+            index,
+        }),
+        [props.carouselState.containerWidth],
+    );
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide) => {
+            const nextIndex = props.carouselState.page - deltaSlide;
+            const nextItem = props.carouselState.attachments[nextIndex];
+
+            if (!nextItem || !scrollRef.current) {
+                return;
+            }
+
+            // The sliding transition is a bit too much on web, because of the wider and bigger images,
+            // so we only enable it for mobile
+            scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
+        },
+        [props.carouselState.attachments, props.carouselState.page],
+    );
+
+    /**
+     * Defines how a container for a single attachment should be rendered
+     * @param {Object} props
+     * @returns {JSX.Element}
+     */
+    const renderCell = useCallback((cellProps) => {
+        // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
+        // considering horizontal margin and border width in centered modal
+        const modalStyles = styles.centeredModalStyles(cellProps.isSmallScreenWidth, true);
+        const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(cellProps.windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
+
+        return (
+            <View
+                // eslint-disable-next-line react/jsx-props-no-spreading
+                {...cellProps}
+                style={style}
+            />
+        );
+    }, []);
+
+    /**
+     * Defines how a single attachment should be rendered
+     * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
+     * @returns {JSX.Element}
+     */
+    const renderItem = useCallback(
+        ({item}) => (
+            <AttachmentView
+                source={item.source}
+                file={item.file}
+                isAuthTokenRequired={item.isAuthTokenRequired}
+            />
+        ),
+        [],
+    );
+
+    return (
+        <>
+            <CarouselButtons
+                carouselState={props.carouselState}
+                onBack={() => {
+                    cycleThroughAttachments(-1);
+                    props.autoHideArrow();
+                }}
+                onForward={() => {
+                    cycleThroughAttachments(1);
+                    props.autoHideArrow();
+                }}
+                autoHideArrow={props.autoHideArrow}
+                cancelAutoHideArrow={props.cancelAutoHideArrow}
+            />
+
+            {props.carouselState.containerWidth > 0 && (
+                <FlatList
+                    listKey="AttachmentCarousel"
+                    horizontal
+                    // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
+                    // promotes the natural feeling of swiping left/right to go to the next/previous image
+                    // We don't want to invert the list for desktop/web because this interferes with mouse
+                    // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
+                    decelerationRate="fast"
+                    showsHorizontalScrollIndicator={false}
+                    bounces={false}
+                    // Scroll only one image at a time no matter how fast the user swipes
+                    disableIntervalMomentum
+                    pagingEnabled
+                    snapToAlignment="start"
+                    snapToInterval={props.carouselState.containerWidth}
+                    scrollEnabled={false}
+                    ref={scrollRef}
+                    initialScrollIndex={props.carouselState.page}
+                    initialNumToRender={3}
+                    windowSize={5}
+                    maxToRenderPerBatch={3}
+                    data={props.carouselState.attachments}
+                    CellRendererComponent={renderCell}
+                    renderItem={renderItem}
+                    getItemLayout={getItemLayout}
+                    keyExtractor={(item) => item.source}
+                    viewabilityConfig={viewabilityConfig}
+                    onViewableItemsChanged={props.updatePage}
+                />
+            )}
+
+            <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
+        </>
+    );
+}
+
+AttachmentCarouselView.propTypes = propTypes;
+
+export default withWindowDimensions(AttachmentCarouselView);
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
new file mode 100644
index 000000000000..612a8d7a1bae
--- /dev/null
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -0,0 +1,66 @@
+import React, {useCallback, useRef} from 'react';
+import PropTypes from 'prop-types';
+import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
+import Pager from '../Lightbox';
+import CarouselButtons from '../CarouselButtons';
+
+const propTypes = {
+    // eslint-disable-next-line react/forbid-prop-types
+    carouselState: PropTypes.object.isRequired,
+
+    toggleArrowsVisibility: PropTypes.func.isRequired,
+    autoHideArrow: PropTypes.func.isRequired,
+    cancelAutoHideArrow: PropTypes.func.isRequired,
+};
+
+function AttachmentCarouselView(props) {
+    const pagerRef = useRef(null);
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide) => {
+            const nextIndex = props.carouselState.page - deltaSlide;
+            const nextItem = props.carouselState.attachments[nextIndex];
+
+            if (!nextItem) {
+                return;
+            }
+
+            pagerRef.current.setPage(nextIndex);
+        },
+        [props.carouselState.attachments, props.carouselState.page],
+    );
+
+    return (
+        <>
+            <CarouselButtons
+                carouselState={props.carouselState}
+                onBack={() => {
+                    cycleThroughAttachments(-1);
+                    props.autoHideArrow();
+                }}
+                onForward={() => {
+                    cycleThroughAttachments(1);
+                    props.autoHideArrow();
+                }}
+                autoHideArrow={props.autoHideArrow}
+                cancelAutoHideArrow={props.cancelAutoHideArrow}
+            />
+
+            <Pager
+                items={props.carouselState.attachments}
+                initialIndex={props.carouselState.page}
+                onTap={props.toggleArrowsVisibility}
+                itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
+                ref={pagerRef}
+            />
+        </>
+    );
+}
+
+AttachmentCarouselView.propTypes = propTypes;
+
+export default AttachmentCarouselView;
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js
new file mode 100644
index 000000000000..9f3eb053590b
--- /dev/null
+++ b/src/components/AttachmentCarousel/CarouselButtons.js
@@ -0,0 +1,71 @@
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import {View} from 'react-native';
+import withLocalize, {withLocalizePropTypes} from '../withLocalize';
+import * as Expensicons from '../Icon/Expensicons';
+import Tooltip from '../Tooltip';
+import Button from '../Button';
+import styles from '../../styles/styles';
+import themeColors from '../../styles/themes/default';
+
+const propTypes = {
+    /** Callback to go one page back */
+    // eslint-disable-next-line react/forbid-prop-types
+    carouselState: PropTypes.object.isRequired,
+
+    /** Callback to go one page back */
+    onBack: PropTypes.func.isRequired,
+    /** Callback to go one page forward */
+    onForward: PropTypes.func.isRequired,
+
+    autoHideArrow: PropTypes.func.isRequired,
+    cancelAutoHideArrow: PropTypes.func.isRequired,
+
+    ...withLocalizePropTypes,
+};
+
+function CarouselButtons(props) {
+    const isForwardDisabled = props.carouselState.page === 0;
+    const isBackDisabled = props.carouselState.page === _.size(props.carouselState.attachments) - 1;
+
+    return props.carouselState.shouldShowArrow ? (
+        <>
+            {!isBackDisabled && (
+                <Tooltip text={props.translate('common.previous')}>
+                    <View style={[styles.attachmentArrow, props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
+                        <Button
+                            small
+                            innerStyles={[styles.arrowIcon]}
+                            icon={Expensicons.BackArrow}
+                            iconFill={themeColors.text}
+                            iconStyles={[styles.mr0]}
+                            onPress={props.onBack}
+                            onPressIn={props.cancelAutoHideArrow}
+                            onPressOut={props.autoHideArrow}
+                        />
+                    </View>
+                </Tooltip>
+            )}
+            {!isForwardDisabled && (
+                <Tooltip text={props.translate('common.next')}>
+                    <View style={[styles.attachmentArrow, props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
+                        <Button
+                            small
+                            innerStyles={[styles.arrowIcon]}
+                            icon={Expensicons.ArrowRight}
+                            iconFill={themeColors.text}
+                            iconStyles={[styles.mr0]}
+                            onPress={props.onForward}
+                            onPressIn={props.cancelAutoHideArrow}
+                            onPressOut={props.autoHideArrow}
+                        />
+                    </View>
+                </Tooltip>
+            )}
+        </>
+    ) : null;
+}
+
+CarouselButtons.propTypes = propTypes;
+
+export default withLocalize(CarouselButtons);
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 77ab66400ae2..0a02ee573f59 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -1,26 +1,21 @@
 import React from 'react';
-import {View, FlatList, PixelRatio} from 'react-native';
+import {View, PixelRatio} from 'react-native';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
 import _ from 'underscore';
 import {Parser as HtmlParser} from 'htmlparser2';
-import * as Expensicons from '../Icon/Expensicons';
 import styles from '../../styles/styles';
-import themeColors from '../../styles/themes/default';
-import CarouselActions from './CarouselActions';
-import Button from '../Button';
 import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
-import AttachmentView from '../AttachmentView';
 import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
 import CONST from '../../CONST';
 import ONYXKEYS from '../../ONYXKEYS';
 import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
 import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
-import Tooltip from '../Tooltip';
 import withLocalize, {withLocalizePropTypes} from '../withLocalize';
 import compose from '../../libs/compose';
 import withWindowDimensions from '../withWindowDimensions';
 import reportPropTypes from '../../pages/reportPropTypes';
+import AttachmentCarouselView from './AttachmentCarouselView';
 
 const propTypes = {
     /** source is used to determine the starting index in the array of attachments */
@@ -48,7 +43,6 @@ class AttachmentCarousel extends React.Component {
     constructor(props) {
         super(props);
 
-        this.scrollRef = React.createRef();
         this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
         this.viewabilityConfig = {
             // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -60,10 +54,7 @@ class AttachmentCarousel extends React.Component {
         this.autoHideArrow = this.autoHideArrow.bind(this);
         this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
         this.getItemLayout = this.getItemLayout.bind(this);
-        this.renderItem = this.renderItem.bind(this);
-        this.renderCell = this.renderCell.bind(this);
         this.updatePage = this.updatePage.bind(this);
-        this.updateZoomState = this.updateZoomState.bind(this);
         this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
 
         this.state = this.createInitialState();
@@ -73,20 +64,6 @@ class AttachmentCarousel extends React.Component {
         this.autoHideArrow();
     }
 
-    /**
-     * Calculate items layout information to optimize scrolling performance
-     * @param {*} data
-     * @param {Number} index
-     * @returns {{offset: Number, length: Number, index: Number}}
-     */
-    getItemLayout(data, index) {
-        return {
-            length: this.state.containerWidth,
-            offset: this.state.containerWidth * index,
-            index,
-        };
-    }
-
     /**
      * On a touch screen device, automatically hide the arrows
      * if there is no interaction for 3 seconds.
@@ -132,21 +109,6 @@ class AttachmentCarousel extends React.Component {
         );
     }
 
-    /**
-     * Updates zoomed state to enable/disable panning the PDF
-     * @param {Number} scale current PDF scale
-     */
-    updateZoomState(scale) {
-        const isZoomed = scale > 1;
-        if (isZoomed === this.state.isZoomed) {
-            return;
-        }
-        if (isZoomed) {
-            this.toggleArrowsVisibility(false);
-        }
-        this.setState({isZoomed});
-    }
-
     /**
      * Constructs the initial component state from report actions
      * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
@@ -190,23 +152,6 @@ class AttachmentCarousel extends React.Component {
         };
     }
 
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    cycleThroughAttachments(deltaSlide) {
-        const nextIndex = this.state.page - deltaSlide;
-        const nextItem = this.state.attachments[nextIndex];
-
-        if (!nextItem || !this.scrollRef.current) {
-            return;
-        }
-
-        // The sliding transition is a bit too much on web, because of the wider and bigger images,
-        // so we only enable it for mobile
-        this.scrollRef.current.scrollToIndex({index: nextIndex, animated: this.canUseTouchScreen});
-    }
-
     /**
      * Updates the page state when the user navigates between attachments
      * @param {Array<{item: {source, file}, index: Number}>} viewableItems
@@ -224,47 +169,7 @@ class AttachmentCarousel extends React.Component {
         this.setState({page, isZoomed: false});
     }
 
-    /**
-     * Defines how a container for a single attachment should be rendered
-     * @param {Object} props
-     * @returns {JSX.Element}
-     */
-    renderCell(props) {
-        // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
-        // considering horizontal margin and border width in centered modal
-        const modalStyles = styles.centeredModalStyles(this.props.isSmallScreenWidth, true);
-        const style = [props.style, styles.h100, {width: PixelRatio.roundToNearestPixel(this.props.windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
-
-        return (
-            <View
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-                style={style}
-            />
-        );
-    }
-
-    /**
-     * Defines how a single attachment should be rendered
-     * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
-     * @returns {JSX.Element}
-     */
-    renderItem({item}) {
-        return (
-            <AttachmentView
-                source={item.source}
-                file={item.file}
-                isAuthTokenRequired={item.isAuthTokenRequired}
-                onScaleChanged={this.canUseTouchScreen ? this.updateZoomState : undefined}
-                onPress={this.canUseTouchScreen ? this.toggleArrowsVisibility : undefined}
-            />
-        );
-    }
-
     render() {
-        const isForwardDisabled = this.state.page === 0;
-        const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
-
         return (
             <View
                 style={[styles.attachmentModalArrowsContainer, styles.flex1]}
@@ -272,86 +177,13 @@ class AttachmentCarousel extends React.Component {
                 onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
                 onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
             >
-                {this.state.shouldShowArrow && (
-                    <>
-                        {!isBackDisabled && (
-                            <Tooltip text={this.props.translate('common.previous')}>
-                                <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.BackArrow}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(-1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </View>
-                            </Tooltip>
-                        )}
-                        {!isForwardDisabled && (
-                            <Tooltip text={this.props.translate('common.next')}>
-                                <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.ArrowRight}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </View>
-                            </Tooltip>
-                        )}
-                    </>
-                )}
-
-                {this.state.containerWidth > 0 && (
-                    <FlatList
-                        listKey="AttachmentCarousel"
-                        horizontal
-                        // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-                        // promotes the natural feeling of swiping left/right to go to the next/previous image
-                        // We don't want to invert the list for desktop/web because this interferes with mouse
-                        // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
-                        inverted={this.canUseTouchScreen}
-                        decelerationRate="fast"
-                        showsHorizontalScrollIndicator={false}
-                        bounces={false}
-                        // Scroll only one image at a time no matter how fast the user swipes
-                        disableIntervalMomentum
-                        pagingEnabled
-                        snapToAlignment="start"
-                        snapToInterval={this.state.containerWidth}
-                        // Enable scrolling by swiping on mobile (touch) devices only
-                        // disable scroll for desktop/browsers because they add their scrollbars
-                        // Enable scrolling FlatList only when PDF is not in a zoomed state
-                        scrollEnabled={this.canUseTouchScreen && !this.state.isZoomed}
-                        ref={this.scrollRef}
-                        initialScrollIndex={this.state.page}
-                        initialNumToRender={3}
-                        windowSize={5}
-                        maxToRenderPerBatch={3}
-                        data={this.state.attachments}
-                        CellRendererComponent={this.renderCell}
-                        renderItem={this.renderItem}
-                        getItemLayout={this.getItemLayout}
-                        keyExtractor={(item) => item.source}
-                        viewabilityConfig={this.viewabilityConfig}
-                        onViewableItemsChanged={this.updatePage}
-                    />
-                )}
-
-                <CarouselActions onCycleThroughAttachments={this.cycleThroughAttachments} />
+                <AttachmentCarouselView
+                    carouselState={this.state}
+                    updatePage={this.updatePage}
+                    toggleArrowsVisibility={this.toggleArrowsVisibility}
+                    autoHideArrow={this.autoHideArrow}
+                    cancelAutoHideArrow={this.cancelAutoHideArrow}
+                />
             </View>
         );
     }
diff --git a/src/components/AttachmentCarousel/index.native.js b/src/components/AttachmentCarousel/index.native.js
deleted file mode 100644
index b57d9e1c971d..000000000000
--- a/src/components/AttachmentCarousel/index.native.js
+++ /dev/null
@@ -1,355 +0,0 @@
-import React from 'react';
-import {View, FlatList} from 'react-native';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import * as Expensicons from '../Icon/Expensicons';
-import styles from '../../styles/styles';
-import themeColors from '../../styles/themes/default';
-import CarouselActions from './CarouselActions';
-import Button from '../Button';
-import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
-import AttachmentView from '../AttachmentView';
-import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
-import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
-import CONST from '../../CONST';
-import ONYXKEYS from '../../ONYXKEYS';
-import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
-import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
-import Tooltip from '../Tooltip';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-import compose from '../../libs/compose';
-import withWindowDimensions from '../withWindowDimensions';
-import Pager from './Lightbox';
-
-const propTypes = {
-    /** source is used to determine the starting index in the array of attachments */
-    source: PropTypes.string,
-
-    /** Callback to update the parent modal's state with a source and name from the attachments array */
-    onNavigate: PropTypes.func,
-
-    /** Object of report actions for this report */
-    reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
-    ...withLocalizePropTypes,
-};
-
-const defaultProps = {
-    source: '',
-    reportActions: {},
-    onNavigate: () => {},
-};
-
-class AttachmentCarousel extends React.Component {
-    constructor(props) {
-        super(props);
-
-        this.scrollRef = React.createRef();
-        this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
-        this.viewabilityConfig = {
-            // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-            // more than 90% visible. When that happens we update the page index in the state.
-            itemVisiblePercentThreshold: 95,
-        };
-
-        this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
-        this.autoHideArrow = this.autoHideArrow.bind(this);
-        this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
-        this.getItemLayout = this.getItemLayout.bind(this);
-        this.renderItem = this.renderItem.bind(this);
-        this.renderCell = this.renderCell.bind(this);
-        this.updatePage = this.updatePage.bind(this);
-        this.updateZoomState = this.updateZoomState.bind(this);
-        this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
-
-        this.state = this.makeInitialState();
-    }
-
-    componentDidMount() {
-        this.autoHideArrow();
-    }
-
-    /**
-     * Helps to navigate between next/previous attachments
-     * @param {Object} attachmentItem
-     * @returns {Object}
-     */
-    getAttachment(attachmentItem) {
-        const source = _.get(attachmentItem, 'source', '');
-        const file = _.get(attachmentItem, 'file', {name: ''});
-
-        return {
-            source,
-            file,
-        };
-    }
-
-    /**
-     * Calculate items layout information to optimize scrolling performance
-     * @param {*} data
-     * @param {Number} index
-     * @returns {{offset: Number, length: Number, index: Number}}
-     */
-    getItemLayout(data, index) {
-        return {
-            length: this.state.containerWidth,
-            offset: this.state.containerWidth * index,
-            index,
-        };
-    }
-
-    /**
-     * On a touch screen device, automatically hide the arrows
-     * if there is no interaction for 3 seconds.
-     */
-    autoHideArrow() {
-        if (!this.canUseTouchScreen) {
-            return;
-        }
-        this.cancelAutoHideArrow();
-        this.autoHideArrowTimeout = setTimeout(() => {
-            this.toggleArrowsVisibility(false);
-        }, CONST.ARROW_HIDE_DELAY);
-    }
-
-    /**
-     * Cancels the automatic hiding of the arrows.
-     */
-    cancelAutoHideArrow() {
-        clearTimeout(this.autoHideArrowTimeout);
-    }
-
-    /**
-     * Toggles the visibility of the arrows
-     * @param {Boolean} shouldShowArrow
-     */
-    toggleArrowsVisibility(shouldShowArrow) {
-        // Don't toggle arrows in a zoomed state
-        if (this.state.isZoomed) {
-            return;
-        }
-        this.setState(
-            (current) => {
-                const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-                return {shouldShowArrow: newShouldShowArrow};
-            },
-            () => {
-                if (this.state.shouldShowArrow) {
-                    this.autoHideArrow();
-                } else {
-                    this.cancelAutoHideArrow();
-                }
-            },
-        );
-    }
-
-    /**
-     * Updates zoomed state to enable/disable panning the PDF
-     * @param {Number} scale current PDF scale
-     */
-    updateZoomState(scale) {
-        const isZoomed = scale > 1;
-        if (isZoomed === this.state.isZoomed) {
-            return;
-        }
-        if (isZoomed) {
-            this.toggleArrowsVisibility(false);
-        }
-        this.setState({isZoomed});
-    }
-
-    /**
-     * Map report actions to attachment items and sets the initial carousel state
-     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
-     */
-    makeInitialState() {
-        let page = 0;
-        const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
-
-        /**
-         * Looping to filter out attachments and retrieve the src URL and name of attachments.
-         */
-        const attachments = [];
-        _.forEach(actions, ({originalMessage, message}) => {
-            // Check for attachment which hasn't been deleted
-            if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
-                return;
-            }
-            const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
-
-            // matchAll captured both source url and name of the attachment
-            if (matches.length === 2) {
-                const [originalSource, name] = _.map(matches, (m) => m[2]);
-
-                // Update the image URL so the images can be accessed depending on the config environment.
-                // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
-                const source = tryResolveUrlFromApiRoot(originalSource);
-                if (source === this.props.source) {
-                    page = attachments.length;
-                }
-
-                attachments.push({source, file: {name}});
-            }
-        });
-
-        return {
-            page,
-            attachments,
-            shouldShowArrow: this.canUseTouchScreen,
-            containerWidth: 0,
-            isZoomed: false,
-        };
-    }
-
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    cycleThroughAttachments(deltaSlide) {
-        const nextIndex = this.state.page - deltaSlide;
-        const nextItem = this.state.attachments[nextIndex];
-
-        if (!nextItem) {
-            return;
-        }
-
-        // The sliding transition is a bit too much on web, because of the wider and bigger images,
-        // so we only enable it for mobile
-        this.scrollRef.current.scrollToIndex({index: nextIndex, animated: this.canUseTouchScreen});
-    }
-
-    /**
-     * Updates the page state when the user navigates between attachments
-     * @param {Array<{item: *, index: Number}>} viewableItems
-     */
-    updatePage({viewableItems}) {
-        // Since we can have only one item in view at a time, we can use the first item in the array
-        // to get the index of the current page
-        const entry = _.first(viewableItems);
-        if (!entry) {
-            return;
-        }
-
-        const page = entry.index;
-        const {source, file} = this.getAttachment(entry.item);
-        this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
-        this.setState({page, isZoomed: false});
-    }
-
-    /**
-     * Defines how a container for a single attachment should be rendered
-     * @param {Object} props
-     * @returns {JSX.Element}
-     */
-    renderCell(props) {
-        const style = [props.style, styles.h100, {width: this.state.containerWidth}];
-
-        return (
-            <View
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...props}
-                style={style}
-            />
-        );
-    }
-
-    /**
-     * Defines how a single attachment should be rendered
-     * @param {{ source: String, file: { name: String } }} item
-     * @returns {JSX.Element}
-     */
-    renderItem({item}) {
-        const authSource = addEncryptedAuthTokenToURL(item.source);
-        if (!this.canUseTouchScreen) {
-            return (
-                <AttachmentView
-                    source={authSource}
-                    file={item.file}
-                />
-            );
-        }
-
-        return (
-            <AttachmentView
-                source={authSource}
-                file={item.file}
-                onScaleChanged={this.updateZoomState}
-                onPress={this.toggleArrowsVisibility}
-            />
-        );
-    }
-
-    render() {
-        const isForwardDisabled = this.state.page === 0;
-        const isBackDisabled = this.state.page === _.size(this.state.attachments) - 1;
-
-        return (
-            <View style={[styles.attachmentModalArrowsContainer, styles.flex1]}>
-                {this.state.shouldShowArrow && (
-                    <>
-                        {!isBackDisabled && (
-                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.l2 : styles.l8]}>
-                                <Tooltip text={this.props.translate('common.previous')}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.BackArrow}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(-1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </Tooltip>
-                            </View>
-                        )}
-                        {!isForwardDisabled && (
-                            <View style={[styles.attachmentArrow, this.props.isSmallScreenWidth ? styles.r2 : styles.r8]}>
-                                <Tooltip text={this.props.translate('common.next')}>
-                                    <Button
-                                        small
-                                        innerStyles={[styles.arrowIcon]}
-                                        icon={Expensicons.ArrowRight}
-                                        iconFill={themeColors.text}
-                                        iconStyles={[styles.mr0]}
-                                        onPress={() => {
-                                            this.cycleThroughAttachments(1);
-                                            this.autoHideArrow();
-                                        }}
-                                        onPressIn={this.cancelAutoHideArrow}
-                                        onPressOut={this.autoHideArrow}
-                                    />
-                                </Tooltip>
-                            </View>
-                        )}
-                    </>
-                )}
-
-                <Pager
-                    items={this.state.attachments}
-                    initialIndex={this.state.page}
-                    onTap={this.toggleArrowsVisibility}
-                    itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
-                />
-            </View>
-        );
-    }
-}
-
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
-
-export default compose(
-    withOnyx({
-        reportActions: {
-            key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
-            canEvict: false,
-        },
-    }),
-    withLocalize,
-    withWindowDimensions,
-)(AttachmentCarousel);
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index c6f4ae393c9b..393a0085176c 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -5,7 +5,6 @@ import Str from 'expensify-common/lib/str';
 import lodashGet from 'lodash/get';
 import lodashExtend from 'lodash/extend';
 import _ from 'underscore';
-import {GestureHandlerRootView} from 'react-native-gesture-handler';
 import CONST from '../CONST';
 import Modal from './Modal';
 import AttachmentView from './AttachmentView';

From f1c2accbdf30afed5816d488692cea07b8c04abe Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 12:14:04 +0200
Subject: [PATCH 009/133] fix: wrong bindings

---
 src/components/AttachmentCarousel/index.js | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 0a02ee573f59..e792c5d67294 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -44,18 +44,12 @@ class AttachmentCarousel extends React.Component {
         super(props);
 
         this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
-        this.viewabilityConfig = {
-            // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-            // more than 90% visible. When that happens we update the page index in the state.
-            itemVisiblePercentThreshold: 95,
-        };
 
-        this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
         this.autoHideArrow = this.autoHideArrow.bind(this);
         this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
-        this.getItemLayout = this.getItemLayout.bind(this);
         this.updatePage = this.updatePage.bind(this);
         this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
+        this.createInitialState = this.createInitialState.bind(this);
 
         this.state = this.createInitialState();
     }

From a45c8cf387f81006f66f9e210d881b369fe39771 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 12:18:50 +0200
Subject: [PATCH 010/133] fix: eslint warning

---
 src/components/AttachmentCarousel/index.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index e792c5d67294..7bf9905692ec 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -167,6 +167,7 @@ class AttachmentCarousel extends React.Component {
         return (
             <View
                 style={[styles.attachmentModalArrowsContainer, styles.flex1]}
+                // eslint-disable-next-line react/no-unused-state
                 onLayout={({nativeEvent}) => this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width)})}
                 onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
                 onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}

From f51cd9f88a3337c57e2d0649355d5c8aa7a0c518 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 13:45:17 +0200
Subject: [PATCH 011/133] fix: paging

---
 .../AttachmentCarouselView/index.native.js    | 21 ++++++++++++------
 src/components/AttachmentCarousel/index.js    | 22 ++++++++++++++-----
 2 files changed, 31 insertions(+), 12 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 612a8d7a1bae..98646ecb2b28 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,4 +1,4 @@
-import React, {useCallback, useRef} from 'react';
+import React, {useCallback, useMemo, useRef} from 'react';
 import PropTypes from 'prop-types';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import Pager from '../Lightbox';
@@ -8,6 +8,7 @@ const propTypes = {
     // eslint-disable-next-line react/forbid-prop-types
     carouselState: PropTypes.object.isRequired,
 
+    updatePageByIndex: PropTypes.func.isRequired,
     toggleArrowsVisibility: PropTypes.func.isRequired,
     autoHideArrow: PropTypes.func.isRequired,
     cancelAutoHideArrow: PropTypes.func.isRequired,
@@ -16,22 +17,28 @@ const propTypes = {
 function AttachmentCarouselView(props) {
     const pagerRef = useRef(null);
 
+    const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
+    const reversedPage = useMemo(() => props.carouselState.attachments.length - props.carouselState.page - 1, [props.carouselState.attachments, props.carouselState.page]);
+
     /**
      * Increments or decrements the index to get another selected item
      * @param {Number} deltaSlide
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextIndex = props.carouselState.page - deltaSlide;
-            const nextItem = props.carouselState.attachments[nextIndex];
+            const nextCarouselPage = props.carouselState.page - deltaSlide;
+            const nextItem = reversedAttachments[nextCarouselPage];
+
+            const nextPagerIndex = reversedPage + deltaSlide;
 
             if (!nextItem) {
                 return;
             }
 
-            pagerRef.current.setPage(nextIndex);
+            pagerRef.current.setPage(nextPagerIndex);
+            props.updatePageByIndex(nextCarouselPage);
         },
-        [props.carouselState.attachments, props.carouselState.page],
+        [props, reversedAttachments, reversedPage],
     );
 
     return (
@@ -51,8 +58,8 @@ function AttachmentCarouselView(props) {
             />
 
             <Pager
-                items={props.carouselState.attachments}
-                initialIndex={props.carouselState.page}
+                items={reversedAttachments}
+                initialIndex={reversedPage}
                 onTap={props.toggleArrowsVisibility}
                 itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
                 ref={pagerRef}
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 7bf9905692ec..3b04ce8e4813 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -48,6 +48,7 @@ class AttachmentCarousel extends React.Component {
         this.autoHideArrow = this.autoHideArrow.bind(this);
         this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
         this.updatePage = this.updatePage.bind(this);
+        this.updatePageByIndex = this.updatePageByIndex.bind(this);
         this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
         this.createInitialState = this.createInitialState.bind(this);
 
@@ -82,10 +83,11 @@ class AttachmentCarousel extends React.Component {
     /**
      * Toggles the visibility of the arrows
      * @param {Boolean} shouldShowArrow
+     * @param {Boolean} isGestureInUse
      */
-    toggleArrowsVisibility(shouldShowArrow) {
+    toggleArrowsVisibility(shouldShowArrow, isGestureInUse = false) {
         // Don't toggle arrows in a zoomed state
-        if (this.state.isZoomed) {
+        if (isGestureInUse) {
             return;
         }
         this.setState(
@@ -105,7 +107,7 @@ class AttachmentCarousel extends React.Component {
 
     /**
      * Constructs the initial component state from report actions
-     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
+     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}}
      */
     createInitialState() {
         const actions = [ReportActionsUtils.getParentReportAction(this.props.report), ...ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions))];
@@ -142,7 +144,6 @@ class AttachmentCarousel extends React.Component {
             attachments,
             shouldShowArrow: this.canUseTouchScreen,
             containerWidth: 0,
-            isZoomed: false,
         };
     }
 
@@ -160,7 +161,17 @@ class AttachmentCarousel extends React.Component {
 
         const page = entry.index;
         this.props.onNavigate(entry.item);
-        this.setState({page, isZoomed: false});
+        // eslint-disable-next-line react/no-unused-state
+        this.setState({page});
+    }
+
+    /**
+     * Updates the page state by index when the user navigates between attachments
+     * @param {number} index
+     */
+    updatePageByIndex(index) {
+        // eslint-disable-next-line react/no-unused-state
+        this.setState({page: index});
     }
 
     render() {
@@ -175,6 +186,7 @@ class AttachmentCarousel extends React.Component {
                 <AttachmentCarouselView
                     carouselState={this.state}
                     updatePage={this.updatePage}
+                    updatePageByIndex={this.updatePageByIndex}
                     toggleArrowsVisibility={this.toggleArrowsVisibility}
                     autoHideArrow={this.autoHideArrow}
                     cancelAutoHideArrow={this.cancelAutoHideArrow}

From 0daf91fa0cb968b47276cc40526c95dbac97f4d7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 16 Jun 2023 13:45:57 +0200
Subject: [PATCH 012/133] forward PagerRef

---
 src/components/AttachmentCarousel/Lightbox.js | 21 +++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 0343b69b611e..7528415fb7de 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,4 +1,4 @@
-import React, {createContext, useContext, useEffect, useRef, useState, useMemo} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
 import {Gesture, GestureDetector, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
@@ -744,7 +744,8 @@ const noopWorklet = () => {
     // noop
 };
 
-export default function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}}) {
+// eslint-disable-next-line react/prop-types
+function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}, forwardedRef}) {
     const windowDimensions = useWindowDimensions();
 
     const shouldPagerScroll = useSharedValue(true);
@@ -780,6 +781,14 @@ export default function Pager({items, initialIndex = 0, onTap, itemExtractor, on
         },
     );
 
+    useImperativeHandle(
+        forwardedRef,
+        () => ({
+            setPage: (...props) => pagerRef.current.setPage(...props),
+        }),
+        [],
+    );
+
     const animatedProps = useAnimatedProps(() => ({
         scrollEnabled: shouldPagerScroll.value,
     }));
@@ -826,3 +835,11 @@ export default function Pager({items, initialIndex = 0, onTap, itemExtractor, on
         </View>
     );
 }
+
+export default React.forwardRef((props, ref) => (
+    <Pager
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        {...props}
+        forwardedRef={ref}
+    />
+));

From 34b0913cb0dca64dc112f9a9a3ea56c81dedbae6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 15:54:04 +0200
Subject: [PATCH 013/133] fix: merge changes

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js        | 4 ----
 src/components/AttachmentCarousel/CarouselButtons.js          | 1 +
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 1114f26f02b5..f41408f3d4d1 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -118,10 +118,6 @@ function AttachmentCarouselView(props) {
                 <FlatList
                     listKey="AttachmentCarousel"
                     horizontal
-                    // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-                    // promotes the natural feeling of swiping left/right to go to the next/previous image
-                    // We don't want to invert the list for desktop/web because this interferes with mouse
-                    // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
                     decelerationRate="fast"
                     showsHorizontalScrollIndicator={false}
                     bounces={false}
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js
index 9f3eb053590b..10eabb502582 100644
--- a/src/components/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/AttachmentCarousel/CarouselButtons.js
@@ -1,4 +1,5 @@
 import PropTypes from 'prop-types';
+import React from 'react';
 import _ from 'underscore';
 import {View} from 'react-native';
 import withLocalize, {withLocalizePropTypes} from '../withLocalize';

From b2f4f064531822344fbbf0fdf157fcdb28eacaed Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 15:58:50 +0200
Subject: [PATCH 014/133] stop reversing the attachments on native in
 "AttachmentCarousel"

---
 src/components/AttachmentCarousel/index.js | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index e402be1da35a..3b04ce8e4813 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -134,14 +134,6 @@ class AttachmentCarousel extends React.Component {
         _.forEach(actions, (action) => htmlParser.write(_.get(action, ['message', 0, 'html'])));
         htmlParser.end();
 
-        // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-        // promotes the natural feeling of swiping left/right to go to the next/previous image
-        // We don't want to invert the list for desktop/web because this interferes with mouse
-        // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
-        if (this.canUseTouchScreen) {
-            attachments.reverse();
-        }
-
         const page = _.findIndex(attachments, (a) => a.source === this.props.source);
         if (page === -1) {
             throw new Error('Attachment not found');

From fd9837653e55c4311fca0594a6649ddc77268dde Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 15:59:02 +0200
Subject: [PATCH 015/133] only show pager if container is big enough

---
 .../AttachmentCarouselView/index.native.js       | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 98646ecb2b28..fa240a53f5e2 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -57,13 +57,15 @@ function AttachmentCarouselView(props) {
                 cancelAutoHideArrow={props.cancelAutoHideArrow}
             />
 
-            <Pager
-                items={reversedAttachments}
-                initialIndex={reversedPage}
-                onTap={props.toggleArrowsVisibility}
-                itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
-                ref={pagerRef}
-            />
+            {props.carouselState.containerWidth > 0 && (
+                <Pager
+                    items={reversedAttachments}
+                    initialIndex={reversedPage}
+                    onTap={props.toggleArrowsVisibility}
+                    itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
+                    ref={pagerRef}
+                />
+            )}
         </>
     );
 }

From 83084832b819faf351568d411ae2eb3fc2dab990 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 16:52:21 +0200
Subject: [PATCH 016/133] hide buttons on gesture

---
 .../AttachmentCarouselView/index.native.js    |  3 ++-
 src/components/AttachmentCarousel/Lightbox.js | 26 +++++++++++++++++--
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index fa240a53f5e2..b3d3f59c26d5 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -61,7 +61,8 @@ function AttachmentCarouselView(props) {
                 <Pager
                     items={reversedAttachments}
                     initialIndex={reversedPage}
-                    onTap={props.toggleArrowsVisibility}
+                    onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
+                    onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
                     itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
                     ref={pagerRef}
                 />
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 7528415fb7de..dff551e8bb82 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -88,7 +88,7 @@ const defaultDimensions = {
 };
 
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
-    const {pagerRef, shouldPagerScroll, isScrolling} = useContext(Context);
+    const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
     const windowDimensions = useWindowDimensions();
 
     const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
@@ -505,6 +505,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     // used to store event scale value when we limit scale
     const gestureScale = useSharedValue(1);
 
+    const pinchGestureRunning = useSharedValue(false);
     const pinchGesture = Gesture.Pinch()
         .onTouchesDown((evt, state) => {
             // we don't want to activate pinch gesture when we are scrolling pager
@@ -514,6 +515,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         })
         .simultaneousWithExternalGesture(panGesture, doubleTap)
         .onStart((evt) => {
+            pinchGestureRunning.value = true;
+
             stopAnimation();
 
             const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
@@ -553,8 +556,26 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             }
 
             afterGesture();
+
+            pinchGestureRunning.value = false;
         });
 
+    const isPinchGestureInUse = useSharedValue(false);
+    useAnimatedReaction(
+        () => [scale.value, pinchGestureRunning.value],
+        ([s, running]) => {
+            const newIsPinchGestureInUse = s !== 1 || running;
+            if (isPinchGestureInUse.value !== newIsPinchGestureInUse) {
+                isPinchGestureInUse.value = newIsPinchGestureInUse;
+            }
+        },
+    );
+
+    useAnimatedReaction(
+        () => isPinchGestureInUse.value,
+        (zoomed) => runOnJS(onPinchGestureChange)(zoomed),
+    );
+
     const animatedStyles = useAnimatedStyle(() => {
         const x = scaleTranslateX.value + translateX.value + offsetX.value;
         const y = scaleTranslateY.value + translateY.value + offsetY.value;
@@ -745,7 +766,7 @@ const noopWorklet = () => {
 };
 
 // eslint-disable-next-line react/prop-types
-function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}, forwardedRef}) {
+function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}, forwardedRef, onPinchGestureChange = () => {}}) {
     const windowDimensions = useWindowDimensions();
 
     const shouldPagerScroll = useSharedValue(true);
@@ -803,6 +824,7 @@ function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWor
                         isScrolling,
                         pagerRef,
                         shouldPagerScroll,
+                        onPinchGestureChange,
                     }}
                 >
                     <AnimatedPagerView

From 089a792acf1f699a6bf667c11b61842d3a754620 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 17:07:27 +0200
Subject: [PATCH 017/133] fix: minor issues

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js         | 3 +--
 .../AttachmentCarousel/AttachmentCarouselView/index.native.js  | 1 -
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index f41408f3d4d1..031ff87a7415 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -121,8 +121,7 @@ function AttachmentCarouselView(props) {
                     decelerationRate="fast"
                     showsHorizontalScrollIndicator={false}
                     bounces={false}
-                    // Scroll only one image at a time no matter how fast the user swipes
-                    disableIntervalMomentum
+                    disableIntervalMomentum // Scroll only one image at a time no matter how fast the user swipes
                     pagingEnabled
                     snapToAlignment="start"
                     snapToInterval={props.carouselState.containerWidth}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index b3d3f59c26d5..e03fac0787bb 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -7,7 +7,6 @@ import CarouselButtons from '../CarouselButtons';
 const propTypes = {
     // eslint-disable-next-line react/forbid-prop-types
     carouselState: PropTypes.object.isRequired,
-
     updatePageByIndex: PropTypes.func.isRequired,
     toggleArrowsVisibility: PropTypes.func.isRequired,
     autoHideArrow: PropTypes.func.isRequired,

From a8a3b5ae511168f449025fe488a9ef395e8296fc Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 19 Jun 2023 17:07:58 +0200
Subject: [PATCH 018/133] update comment

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 031ff87a7415..b2f41d9663af 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -23,7 +23,7 @@ function AttachmentCarouselView(props) {
     const scrollRef = useRef(null);
     const viewabilityConfig = {
         // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-        // more than 90% visible. When that happens we update the page index in the state.
+        // more than 95% visible. When that happens we update the page index in the state.
         itemVisiblePercentThreshold: 95,
     };
 

From 14abde13bb3d08572326ba3e559568906f69038c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 23 Jun 2023 11:14:06 +0200
Subject: [PATCH 019/133] fix: nothing shown

---
 .../AttachmentCarouselView/index.js           | 31 ++++++++++---------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index b2f41d9663af..c40c735e64a5 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -67,20 +67,23 @@ function AttachmentCarouselView(props) {
      * @param {Object} props
      * @returns {JSX.Element}
      */
-    const renderCell = useCallback((cellProps) => {
-        // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
-        // considering horizontal margin and border width in centered modal
-        const modalStyles = styles.centeredModalStyles(cellProps.isSmallScreenWidth, true);
-        const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(cellProps.windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
-
-        return (
-            <View
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...cellProps}
-                style={style}
-            />
-        );
-    }, []);
+    const renderCell = useCallback(
+        (cellProps) => {
+            // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
+            // considering horizontal margin and border width in centered modal
+            const modalStyles = styles.centeredModalStyles(props.isSmallScreenWidth, true);
+            const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(props.windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
+
+            return (
+                <View
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    {...cellProps}
+                    style={style}
+                />
+            );
+        },
+        [props.isSmallScreenWidth, props.windowWidth],
+    );
 
     /**
      * Defines how a single attachment should be rendered

From 2ca0239f589e439087409e3986e6d68f54908f57 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 08:36:16 +0200
Subject: [PATCH 020/133] add "activeSource" logic

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js         | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index c40c735e64a5..b4a89f9a8e43 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -93,12 +93,13 @@ function AttachmentCarouselView(props) {
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
+                isFocused={props.carouselState.activeSource === item.source}
                 source={item.source}
                 file={item.file}
                 isAuthTokenRequired={item.isAuthTokenRequired}
             />
         ),
-        [],
+        [props.carouselState.activeSource],
     );
 
     return (

From 110faef662542e8c3918294b2a9195298b5aa978 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 09:09:07 +0200
Subject: [PATCH 021/133] fix: reversing attachments

---
 .../AttachmentCarouselView/index.native.js                 | 2 ++
 src/components/AttachmentCarousel/index.js                 | 7 -------
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index e03fac0787bb..06febe611054 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -16,6 +16,8 @@ const propTypes = {
 function AttachmentCarouselView(props) {
     const pagerRef = useRef(null);
 
+    // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
+    // promotes the natural feeling of swiping left/right to go to the next/previous image
     const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
     const reversedPage = useMemo(() => props.carouselState.attachments.length - props.carouselState.page - 1, [props.carouselState.attachments, props.carouselState.page]);
 
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index fe61aa639920..d8496ccc70e5 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -134,13 +134,6 @@ class AttachmentCarousel extends React.Component {
         _.forEach(actions, (action) => htmlParser.write(_.get(action, ['message', 0, 'html'])));
         htmlParser.end();
 
-        // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-        // promotes the natural feeling of swiping left/right to go to the next/previous image
-        // We don't want to invert the list for desktop/web because this interferes with mouse
-        // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically)
-        if (this.canUseTouchScreen) {
-            attachments.reverse();
-        }
         const page = _.findIndex(attachments, (a) => a.source === this.props.source);
         if (page === -1) {
             throw new Error('Attachment not found');

From 62b6d7a488a5a0495cef915f35bdebb6c057b5b3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 10:30:33 +0200
Subject: [PATCH 022/133] use container size measurements in carousel
 implementations

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js       | 2 +-
 .../AttachmentCarouselView/index.native.js                   | 4 +++-
 src/components/AttachmentCarousel/index.js                   | 5 ++++-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index b4a89f9a8e43..d9bb2b1e62a7 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -118,7 +118,7 @@ function AttachmentCarouselView(props) {
                 cancelAutoHideArrow={props.cancelAutoHideArrow}
             />
 
-            {props.carouselState.containerWidth > 0 && (
+            {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && (
                 <FlatList
                     listKey="AttachmentCarousel"
                     horizontal
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 06febe611054..61088153c262 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -58,13 +58,15 @@ function AttachmentCarouselView(props) {
                 cancelAutoHideArrow={props.cancelAutoHideArrow}
             />
 
-            {props.carouselState.containerWidth > 0 && (
+            {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && (
                 <Pager
                     items={reversedAttachments}
                     initialIndex={reversedPage}
                     onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
                     onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
                     itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
+                    containerWidth={props.carouselState.containerWidth}
+                    containerHeight={props.carouselState.containerHeight}
                     ref={pagerRef}
                 />
             )}
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index d8496ccc70e5..b3df80bc1605 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -144,6 +144,7 @@ class AttachmentCarousel extends React.Component {
             attachments,
             shouldShowArrow: this.canUseTouchScreen,
             containerWidth: 0,
+            containerHeight: 0,
             activeSource: null,
         };
     }
@@ -183,7 +184,9 @@ class AttachmentCarousel extends React.Component {
             <View
                 style={[styles.attachmentModalArrowsContainer, styles.flex1]}
                 // eslint-disable-next-line react/no-unused-state
-                onLayout={({nativeEvent}) => this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width)})}
+                onLayout={({nativeEvent}) =>
+                    this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), containerHeight: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
+                }
                 onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
                 onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
             >

From caea7f9987ec8b772fa1c205d453b65f2f185266 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 11:08:36 +0200
Subject: [PATCH 023/133] fix: panning for very tall images

---
 src/components/AttachmentCarousel/Lightbox.js | 122 ++++++++----------
 1 file changed, 52 insertions(+), 70 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index dff551e8bb82..d12eb999e2e3 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -56,30 +56,6 @@ function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight
         width: scaledWidth,
         height: canvasHeight,
     };
-
-    // console.log({
-    //     canvasWidth,
-    //     canvasHeight,
-    //     imageWidth,
-    //     imageHeight,
-    // });
-
-    // const scaleFactor = imageWidth / canvasWidth;
-
-    // const width = canvasWidth;
-    // const height = imageHeight / scaleFactor;
-
-    // console.log({
-    //     canvasWidth,
-    //     canvasHeight,
-    //     imageWidth,
-    //     imageHeight,
-    //     width,
-    //     height,
-    //     scaleFactor,
-    // });
-
-    // return {width, height};
 }
 
 const defaultDimensions = {
@@ -135,6 +111,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         })();
     }, [canvasX, canvasY, canvasWidth, canvasHeight]);
 
+    const targetWidth = useSharedValue(0);
     const targetHeight = useSharedValue(0);
 
     const onLoad = (resolvedDimensions) => {
@@ -145,6 +122,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             imageHeight: resolvedDimensions.height,
         });
 
+        targetWidth.value = width;
         targetHeight.value = height;
 
         setImageDimensions({
@@ -194,14 +172,15 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         };
 
         const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
-        const scaledImage = targetHeight.value * fixedScale;
+        const scaledImageWidth = targetWidth.value * fixedScale;
+        const scaledImageHeight = targetHeight.value * fixedScale;
 
-        const rightBoundary = (canvasX.value / 2) * (fixedScale - 1);
+        const rightBoundary = Math.abs(canvasX.value - scaledImageWidth) / 2;
 
         let topBoundary = 0;
 
-        if (canvasY.value < scaledImage) {
-            topBoundary = Math.abs(scaledImage - canvasY.value) / 2;
+        if (canvasY.value < scaledImageHeight) {
+            topBoundary = Math.abs(scaledImageHeight - canvasY.value) / 2;
         }
 
         const maxVector = {x: rightBoundary, y: topBoundary};
@@ -299,7 +278,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         stopAnimation();
 
         const usableImage = {
-            x: canvasWidth,
+            x: targetWidth.value,
             y: targetHeight.value,
         };
 
@@ -482,7 +461,6 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                             velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
                         },
                         () => {
-                            console.log('success');
                             runOnJS(onSwipeSuccess)();
                         },
                     );
@@ -765,10 +743,18 @@ const noopWorklet = () => {
     // noop
 };
 
-// eslint-disable-next-line react/prop-types
-function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWorklet, onSwipeSuccess = () => {}, forwardedRef, onPinchGestureChange = () => {}}) {
-    const windowDimensions = useWindowDimensions();
-
+function Pager({
+    items,
+    initialIndex = 0,
+    onTap,
+    itemExtractor,
+    onSwipe = noopWorklet,
+    onSwipeSuccess = () => {},
+    forwardedRef,
+    onPinchGestureChange = () => {},
+    containerWidth,
+    containerHeight,
+}) {
     const shouldPagerScroll = useSharedValue(true);
     const pagerRef = useRef(null);
 
@@ -817,44 +803,40 @@ function Pager({items, initialIndex = 0, onTap, itemExtractor, onSwipe = noopWor
     const processedItems = _.map(items, (item, index) => itemExtractor({item, index}));
 
     return (
-        <View style={{flex: 1}}>
-            <View style={{flex: 1}}>
-                <Context.Provider
-                    value={{
-                        isScrolling,
-                        pagerRef,
-                        shouldPagerScroll,
-                        onPinchGestureChange,
-                    }}
-                >
-                    <AnimatedPagerView
-                        pageMargin={40}
-                        onPageScroll={pageScrollHandler}
-                        animatedProps={animatedProps}
-                        ref={pagerRef}
+        <Context.Provider
+            value={{
+                isScrolling,
+                pagerRef,
+                shouldPagerScroll,
+                onPinchGestureChange,
+            }}
+        >
+            <AnimatedPagerView
+                pageMargin={40}
+                onPageScroll={pageScrollHandler}
+                animatedProps={animatedProps}
+                ref={pagerRef}
+                style={{flex: 1}}
+                initialPage={initialIndex}
+            >
+                {_.map(processedItems, (item, index) => (
+                    <View
+                        key={item.key}
                         style={{flex: 1}}
-                        initialPage={initialIndex}
                     >
-                        {_.map(processedItems, (item, index) => (
-                            <View
-                                key={item.key}
-                                style={{flex: 1}}
-                            >
-                                <Page
-                                    onTap={onTap}
-                                    onSwipe={onSwipe}
-                                    onSwipeSuccess={onSwipeSuccess}
-                                    isActive={index === activePage}
-                                    item={item}
-                                    canvasHeight={windowDimensions.height}
-                                    canvasWidth={windowDimensions.width}
-                                />
-                            </View>
-                        ))}
-                    </AnimatedPagerView>
-                </Context.Provider>
-            </View>
-        </View>
+                        <Page
+                            onTap={onTap}
+                            onSwipe={onSwipe}
+                            onSwipeSuccess={onSwipeSuccess}
+                            isActive={index === activePage}
+                            item={item}
+                            canvasHeight={containerHeight}
+                            canvasWidth={containerWidth}
+                        />
+                    </View>
+                ))}
+            </AnimatedPagerView>
+        </Context.Provider>
     );
 }
 

From 5b5fd9b4db627944f194c87997dc5e9c7424f94e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 11:48:18 +0200
Subject: [PATCH 024/133] refine swipe down gesture (currently not in use)

---
 src/components/AttachmentCarousel/Lightbox.js | 25 +++++++++++--------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index d12eb999e2e3..b25dd95071ef 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -381,27 +381,30 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 state.activate();
             }
 
-            // this is for swipe down to close
-            // needs fine tuning to work properly
+            // TODO: Swipe down to close carousel gesture
+            // this needs fine tuning to work properly
             // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
-            //     const velocityX = evt.allTouches[0].x - previousTouch.value.x;
+            //     const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x);
             //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
 
-            //     // this needs tuning
-            //     if (Math.abs(velocityY) > Math.abs(velocityX) && Math.abs(velocityY) > 50) {
+            //     // TODO: this needs tuning
+            //     if (Math.abs(velocityY) > velocityX && velocityY > 20) {
             //         state.activate();
 
             //         isSwiping.value = true;
             //         previousTouch.value = null;
+
+            //         runOnJS(onSwipeDown)();
+            //         return;
             //     }
             // }
 
-            // if (previousTouch.value == null) {
-            //     previousTouch.value = {
-            //         x: evt.allTouches[0].x,
-            //         y: evt.allTouches[0].y,
-            //     };
-            // }
+            if (previousTouch.value == null) {
+                previousTouch.value = {
+                    x: evt.allTouches[0].x,
+                    y: evt.allTouches[0].y,
+                };
+            }
         })
         .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
         .onBegin(() => {

From 842252b6b36c26ce9406bc70237d46034035eb7c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 11:48:53 +0200
Subject: [PATCH 025/133] add propTypes

---
 src/components/AttachmentCarousel/Lightbox.js | 62 ++++++++++++++++---
 1 file changed, 53 insertions(+), 9 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index b25dd95071ef..7d90c040c7db 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,5 +1,6 @@
 import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
+import PropTypes from 'prop-types';
 import {Gesture, GestureDetector, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
     cancelAnimation,
@@ -63,6 +64,7 @@ const defaultDimensions = {
     height: 1,
 };
 
+// eslint-disable-next-line react/prop-types
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
     const windowDimensions = useWindowDimensions();
@@ -640,6 +642,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     );
 }
 
+// eslint-disable-next-line react/prop-types
 function ImageWrapper({children}) {
     return (
         <Animated.View
@@ -659,7 +662,14 @@ function ImageWrapper({children}) {
 
 const cachedDimensions = new Map();
 
-function Page({isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeight, onTap}) {
+const pagePropTypes = {
+    item: PropTypes.shape({
+        url: PropTypes.string.isRequired,
+    }).isRequired,
+};
+
+// eslint-disable-next-line react/prop-types
+function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth, canvasHeight, onTap}) {
     const dimensions = cachedDimensions.get(item.url);
 
     if (!isActive) {
@@ -669,15 +679,15 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeigh
                     source={{uri: item.url}}
                     onLoad={(evt) => {
                         cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent?.width,
-                            height: evt.nativeEvent?.height,
+                            width: evt.nativeEvent.width,
+                            height: evt.nativeEvent.height,
                         });
                     }}
                     style={
                         dimensions
                             ? getScaledDimensions({
-                                  imageHeight: dimensions?.height,
-                                  imageWidth: dimensions?.width,
+                                  imageHeight: dimensions.height,
+                                  imageWidth: dimensions.width,
                                   canvasHeight,
                                   canvasWidth,
                               })
@@ -692,10 +702,11 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeigh
         <ImageTransformer
             onSwipe={onSwipe}
             onSwipeSuccess={onSwipeSuccess}
+            onSwipeDown={onSwipeDown}
             isActive
             onTap={onTap}
-            imageHeight={dimensions?.height}
-            imageWidth={dimensions?.width}
+            imageHeight={dimensions.height}
+            imageWidth={dimensions.width}
             canvasHeight={canvasHeight}
             canvasWidth={canvasWidth}
             renderFallback={() => <ActivityIndicator />}
@@ -719,6 +730,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, canvasWidth, canvasHeigh
         />
     );
 }
+Page.propTypes = pagePropTypes;
 
 const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
 
@@ -746,15 +758,44 @@ const noopWorklet = () => {
     // noop
 };
 
+const pagerPropTypes = {
+    items: PropTypes.arrayOf(
+        PropTypes.shape({
+            url: PropTypes.string.isRequired,
+        }),
+    ).isRequired,
+    initialIndex: PropTypes.number,
+    onTap: PropTypes.func,
+    onSwipe: PropTypes.func,
+    onSwipeSuccess: PropTypes.func,
+    onSwipeDown: PropTypes.func,
+    onPinchGestureChange: PropTypes.func,
+    itemExtractor: PropTypes.func.isRequired,
+    forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+    containerWidth: PropTypes.number.isRequired,
+    containerHeight: PropTypes.number.isRequired,
+};
+
+const pagerDefaultProps = {
+    initialIndex: 0,
+    onTap: () => {},
+    onSwipe: noopWorklet,
+    onSwipeSuccess: () => {},
+    onSwipeDown: () => {},
+    onPinchGestureChange: () => {},
+    forwardedRef: null,
+};
+
 function Pager({
     items,
     initialIndex = 0,
     onTap,
-    itemExtractor,
     onSwipe = noopWorklet,
     onSwipeSuccess = () => {},
-    forwardedRef,
+    onSwipeDown = () => {},
     onPinchGestureChange = () => {},
+    itemExtractor,
+    forwardedRef,
     containerWidth,
     containerHeight,
 }) {
@@ -831,6 +872,7 @@ function Pager({
                             onTap={onTap}
                             onSwipe={onSwipe}
                             onSwipeSuccess={onSwipeSuccess}
+                            onSwipeDown={onSwipeDown}
                             isActive={index === activePage}
                             item={item}
                             canvasHeight={containerHeight}
@@ -842,6 +884,8 @@ function Pager({
         </Context.Provider>
     );
 }
+Pager.propTypes = pagerPropTypes;
+Pager.defaultProps = pagerDefaultProps;
 
 export default React.forwardRef((props, ref) => (
     <Pager

From 8180e82d20ab87244112fc2e2ea95bf394c36017 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 11:49:15 +0200
Subject: [PATCH 026/133] add onClose callback to <AttachmentCarousel />

---
 .../AttachmentCarouselView/index.native.js                 | 7 +++++++
 src/components/AttachmentCarousel/index.js                 | 7 ++++++-
 src/components/AttachmentModal.js                          | 1 +
 3 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 61088153c262..b2f039cc5f6b 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -11,6 +11,11 @@ const propTypes = {
     toggleArrowsVisibility: PropTypes.func.isRequired,
     autoHideArrow: PropTypes.func.isRequired,
     cancelAutoHideArrow: PropTypes.func.isRequired,
+    onClose: PropTypes.func,
+};
+
+const defaultProps = {
+    onClose: () => {},
 };
 
 function AttachmentCarouselView(props) {
@@ -64,6 +69,7 @@ function AttachmentCarouselView(props) {
                     initialIndex={reversedPage}
                     onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
                     onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
+                    onSwipeDown={props.onClose}
                     itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
                     containerWidth={props.carouselState.containerWidth}
                     containerHeight={props.carouselState.containerHeight}
@@ -75,5 +81,6 @@ function AttachmentCarouselView(props) {
 }
 
 AttachmentCarouselView.propTypes = propTypes;
+AttachmentCarouselView.defaultProps = defaultProps;
 
 export default AttachmentCarouselView;
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index b3df80bc1605..15d001c5ce35 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -24,6 +24,9 @@ const propTypes = {
     /** Callback to update the parent modal's state with a source and name from the attachments array */
     onNavigate: PropTypes.func,
 
+    /** Callback to close carousel when user swipes down (on native) */
+    onClose: PropTypes.func,
+
     /** Object of report actions for this report */
     reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
 
@@ -37,6 +40,7 @@ const defaultProps = {
     source: '',
     reportActions: {},
     onNavigate: () => {},
+    onClose: () => {},
 };
 
 class AttachmentCarousel extends React.Component {
@@ -183,8 +187,8 @@ class AttachmentCarousel extends React.Component {
         return (
             <View
                 style={[styles.attachmentModalArrowsContainer, styles.flex1]}
-                // eslint-disable-next-line react/no-unused-state
                 onLayout={({nativeEvent}) =>
+                    // eslint-disable-next-line react/no-unused-state
                     this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), containerHeight: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
                 }
                 onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
@@ -197,6 +201,7 @@ class AttachmentCarousel extends React.Component {
                     toggleArrowsVisibility={this.toggleArrowsVisibility}
                     autoHideArrow={this.autoHideArrow}
                     cancelAutoHideArrow={this.cancelAutoHideArrow}
+                    onClose={this.props.onClose}
                 />
             </View>
         );
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index a481a4026659..a9577ba8d4c3 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -298,6 +298,7 @@ function AttachmentModal(props) {
                             report={props.report}
                             onNavigate={onNavigate}
                             source={props.source}
+                            onClose={closeModal}
                             onToggleKeyboard={updateConfirmButtonVisibility}
                         />
                     ) : (

From 723203db5c080ccc23a53c0eaae279a64e0ad0e7 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 12:02:34 +0200
Subject: [PATCH 027/133] fix: null checking

---
 src/components/AttachmentCarousel/Lightbox.js | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 7d90c040c7db..03b4af1893ef 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,3 +1,4 @@
+/* eslint-disable es/no-optional-chaining */
 import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
 import PropTypes from 'prop-types';
@@ -684,14 +685,14 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                         });
                     }}
                     style={
-                        dimensions
-                            ? getScaledDimensions({
+                        dimensions == null
+                            ? {}
+                            : getScaledDimensions({
                                   imageHeight: dimensions.height,
                                   imageWidth: dimensions.width,
                                   canvasHeight,
                                   canvasWidth,
                               })
-                            : {}
                     }
                 />
             </ImageWrapper>
@@ -705,8 +706,8 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
             onSwipeDown={onSwipeDown}
             isActive
             onTap={onTap}
-            imageHeight={dimensions.height}
-            imageWidth={dimensions.width}
+            imageHeight={dimensions?.height}
+            imageWidth={dimensions?.width}
             canvasHeight={canvasHeight}
             canvasWidth={canvasWidth}
             renderFallback={() => <ActivityIndicator />}
@@ -716,13 +717,13 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                     style={[style, {width, height}]}
                     onLoad={(evt) => {
                         cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent.width,
-                            height: evt.nativeEvent.height,
+                            width: evt.nativeEvent?.width,
+                            height: evt.nativeEvent?.height,
                         });
 
                         onResolveImageDimensions({
-                            width: evt.nativeEvent.width / PixelRatio.get(),
-                            height: evt.nativeEvent.height / PixelRatio.get(),
+                            width: evt.nativeEvent?.width / PixelRatio.get(),
+                            height: evt.nativeEvent?.height / PixelRatio.get(),
                         });
                     }}
                 />

From d80255a1761377842da3e84169e13baa3bd8f513 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 27 Jun 2023 14:51:33 +0200
Subject: [PATCH 028/133] wrap Pager in GestureHandlerRootView for android

---
 src/components/AttachmentCarousel/Lightbox.js | 75 ++++++++++---------
 1 file changed, 38 insertions(+), 37 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 03b4af1893ef..4fd2b9a5e431 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -2,7 +2,7 @@
 import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
 import PropTypes from 'prop-types';
-import {Gesture, GestureDetector, createNativeWrapper} from 'react-native-gesture-handler';
+import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
     cancelAnimation,
     runOnJS,
@@ -137,7 +137,6 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     };
 
     // used for pan gesture
-
     const translateY = useSharedValue(0);
     const translateX = useSharedValue(0);
     const offsetX = useSharedValue(0);
@@ -665,7 +664,7 @@ const cachedDimensions = new Map();
 
 const pagePropTypes = {
     item: PropTypes.shape({
-        url: PropTypes.string.isRequired,
+        url: PropTypes.string,
     }).isRequired,
 };
 
@@ -848,41 +847,43 @@ function Pager({
     const processedItems = _.map(items, (item, index) => itemExtractor({item, index}));
 
     return (
-        <Context.Provider
-            value={{
-                isScrolling,
-                pagerRef,
-                shouldPagerScroll,
-                onPinchGestureChange,
-            }}
-        >
-            <AnimatedPagerView
-                pageMargin={40}
-                onPageScroll={pageScrollHandler}
-                animatedProps={animatedProps}
-                ref={pagerRef}
-                style={{flex: 1}}
-                initialPage={initialIndex}
+        <GestureHandlerRootView style={{flex: 1}}>
+            <Context.Provider
+                value={{
+                    isScrolling,
+                    pagerRef,
+                    shouldPagerScroll,
+                    onPinchGestureChange,
+                }}
             >
-                {_.map(processedItems, (item, index) => (
-                    <View
-                        key={item.key}
-                        style={{flex: 1}}
-                    >
-                        <Page
-                            onTap={onTap}
-                            onSwipe={onSwipe}
-                            onSwipeSuccess={onSwipeSuccess}
-                            onSwipeDown={onSwipeDown}
-                            isActive={index === activePage}
-                            item={item}
-                            canvasHeight={containerHeight}
-                            canvasWidth={containerWidth}
-                        />
-                    </View>
-                ))}
-            </AnimatedPagerView>
-        </Context.Provider>
+                <AnimatedPagerView
+                    pageMargin={40}
+                    onPageScroll={pageScrollHandler}
+                    animatedProps={animatedProps}
+                    ref={pagerRef}
+                    style={{flex: 1}}
+                    initialPage={initialIndex}
+                >
+                    {_.map(processedItems, (item, index) => (
+                        <View
+                            key={item.key}
+                            style={{flex: 1}}
+                        >
+                            <Page
+                                onTap={onTap}
+                                onSwipe={onSwipe}
+                                onSwipeSuccess={onSwipeSuccess}
+                                onSwipeDown={onSwipeDown}
+                                isActive={index === activePage}
+                                item={item}
+                                canvasHeight={containerHeight}
+                                canvasWidth={containerWidth}
+                            />
+                        </View>
+                    ))}
+                </AnimatedPagerView>
+            </Context.Provider>
+        </GestureHandlerRootView>
     );
 }
 Pager.propTypes = pagerPropTypes;

From 9e7075e93f163be9a9b85045ae295535c64ba45d Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 11:47:10 +0200
Subject: [PATCH 029/133] fix: prop types

---
 src/components/AttachmentCarousel/Lightbox.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 4fd2b9a5e431..3fa36a64e045 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -761,7 +761,7 @@ const noopWorklet = () => {
 const pagerPropTypes = {
     items: PropTypes.arrayOf(
         PropTypes.shape({
-            url: PropTypes.string.isRequired,
+            url: PropTypes.string,
         }),
     ).isRequired,
     initialIndex: PropTypes.number,
@@ -771,7 +771,7 @@ const pagerPropTypes = {
     onSwipeDown: PropTypes.func,
     onPinchGestureChange: PropTypes.func,
     itemExtractor: PropTypes.func.isRequired,
-    forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+    forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
     containerWidth: PropTypes.number.isRequired,
     containerHeight: PropTypes.number.isRequired,
 };

From 2bdd106b1025235b0442a62126d99c2bb2b4f516 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 12:13:34 +0200
Subject: [PATCH 030/133] use styles from global stylesheet

---
 src/components/AttachmentCarousel/Lightbox.js | 29 +++++++++----------
 1 file changed, 13 insertions(+), 16 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 3fa36a64e045..7fc0997bdb8a 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -21,6 +21,7 @@ import Animated, {
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
 import Image from '../Image';
+import styles from '../../styles/styles';
 
 const Context = createContext(null);
 
@@ -603,10 +604,12 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     return (
         <View
             collapsable={false}
-            style={{
-                flex: 1,
-                width: canvasWidth,
-            }}
+            style={[
+                styles.flex1,
+                {
+                    width: canvasWidth,
+                },
+            ]}
         >
             <GestureDetector gesture={pinchGesture}>
                 <Animated.View
@@ -616,7 +619,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                     <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
                         <Animated.View
                             collapsable
-                            style={[StyleSheet.absoluteFill]}
+                            style={StyleSheet.absoluteFill}
                         >
                             <ImageWrapper>
                                 <GestureDetector gesture={doubleTap}>
@@ -629,7 +632,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                                         {renderImage({
                                             onResolveImageDimensions: onLoad,
                                             ...dimensionsWithScale,
-                                            style: showFallback ? {opacity: 0, position: 'absolute'} : {},
+                                            style: showFallback ? {...styles.opacity0, ...styles.pAbsolute} : {},
                                         })}
                                     </Animated.View>
                                 </GestureDetector>
@@ -647,13 +650,7 @@ function ImageWrapper({children}) {
     return (
         <Animated.View
             collapsable={false}
-            style={[
-                {
-                    ...StyleSheet.absoluteFillObject,
-                    justifyContent: 'center',
-                    alignItems: 'center',
-                },
-            ]}
+            style={[StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter]}
         >
             {children}
         </Animated.View>
@@ -847,7 +844,7 @@ function Pager({
     const processedItems = _.map(items, (item, index) => itemExtractor({item, index}));
 
     return (
-        <GestureHandlerRootView style={{flex: 1}}>
+        <GestureHandlerRootView style={styles.flex1}>
             <Context.Provider
                 value={{
                     isScrolling,
@@ -861,13 +858,13 @@ function Pager({
                     onPageScroll={pageScrollHandler}
                     animatedProps={animatedProps}
                     ref={pagerRef}
-                    style={{flex: 1}}
+                    style={styles.flex1}
                     initialPage={initialIndex}
                 >
                     {_.map(processedItems, (item, index) => (
                         <View
                             key={item.key}
-                            style={{flex: 1}}
+                            style={styles.flex1}
                         >
                             <Page
                                 onTap={onTap}

From 7bbe039a3c655eeb16fe9bd78e034e8295d2058d Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 12:16:11 +0200
Subject: [PATCH 031/133] move constants to top

---
 src/components/AttachmentCarousel/Lightbox.js | 56 +++++++++----------
 1 file changed, 28 insertions(+), 28 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 7fc0997bdb8a..d7d2c25a3589 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -23,24 +23,29 @@ import _ from 'underscore';
 import Image from '../Image';
 import styles from '../../styles/styles';
 
-const Context = createContext(null);
-
-function clamp(value, lowerBound, upperBound) {
-    'worklet';
-
-    return Math.min(Math.max(lowerBound, value), upperBound);
-}
-
 const DOUBLE_TAP_SCALE = 3;
 const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
 
-const config = {
+const SPRING_CONFIG = {
     mass: 3,
     stiffness: 1000,
     damping: 500,
 };
 
+const DEFAULT_DIMENSIONS = {
+    width: 1,
+    height: 1,
+};
+
+const Context = createContext(null);
+
+function clamp(value, lowerBound, upperBound) {
+    'worklet';
+
+    return Math.min(Math.max(lowerBound, value), upperBound);
+}
+
 function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = imageWidth / canvasWidth;
     const scaleFactorY = imageHeight / canvasHeight;
@@ -61,11 +66,6 @@ function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight
     };
 }
 
-const defaultDimensions = {
-    width: 1,
-    height: 1,
-};
-
 // eslint-disable-next-line react/prop-types
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
@@ -75,7 +75,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
 
     const [imageDimensions, setImageDimensions] = useState(
         showFallback
-            ? defaultDimensions
+            ? DEFAULT_DIMENSIONS
             : getScaledDimensions({
                   canvasWidth,
                   canvasHeight,
@@ -210,7 +210,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
 
         if (!canPanVertically.value) {
-            offsetY.value = withSpring(target.y, config);
+            offsetY.value = withSpring(target.y, SPRING_CONFIG);
         }
 
         if (
@@ -229,8 +229,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         if (scale.value <= 1) {
             // just center it
             // reset(true);
-            offsetX.value = withSpring(0, config);
-            offsetY.value = withSpring(0, config);
+            offsetX.value = withSpring(0, SPRING_CONFIG);
+            offsetY.value = withSpring(0, SPRING_CONFIG);
             return;
         }
 
@@ -246,7 +246,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 });
             }
         } else {
-            offsetX.value = withSpring(target.x, config);
+            offsetX.value = withSpring(target.x, SPRING_CONFIG);
         }
 
         if (isInBoundaryY) {
@@ -264,7 +264,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 });
             }
         } else {
-            offsetY.value = withSpring(target.y, config, () => {
+            offsetY.value = withSpring(target.y, SPRING_CONFIG, () => {
                 isSwiping.value = false;
             });
         }
@@ -321,9 +321,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             target.y = 0;
         }
 
-        offsetX.value = withSpring(target.x, config);
-        offsetY.value = withSpring(target.y, config);
-        scale.value = withSpring(DOUBLE_TAP_SCALE, config);
+        offsetX.value = withSpring(target.x, SPRING_CONFIG);
+        offsetY.value = withSpring(target.y, SPRING_CONFIG);
+        scale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
         scaleOffset.value = DOUBLE_TAP_SCALE;
     });
 
@@ -333,9 +333,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         stopAnimation();
 
         if (animated) {
-            offsetX.value = withSpring(0, config);
-            offsetY.value = withSpring(0, config);
-            scale.value = withSpring(1, config);
+            offsetX.value = withSpring(0, SPRING_CONFIG);
+            offsetY.value = withSpring(0, SPRING_CONFIG);
+            scale.value = withSpring(1, SPRING_CONFIG);
         } else {
             scale.value = 1;
             translateX.value = 0;
@@ -533,10 +533,10 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 scaleOffset.value = 1;
 
                 // this runs the timing animation
-                scale.value = withSpring(1, config);
+                scale.value = withSpring(1, SPRING_CONFIG);
             } else if (scaleOffset.value > MAX_SCALE) {
                 scaleOffset.value = MAX_SCALE;
-                scale.value = withSpring(MAX_SCALE, config);
+                scale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
             }
 
             afterGesture();

From 8acdb552ccc244249c848825ae053e681884922e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 12:57:53 +0200
Subject: [PATCH 032/133] update page scrolling

---
 .../AttachmentCarouselView/index.js           |  9 ++--
 .../AttachmentCarouselView/index.native.js    | 45 ++++++++++++++-----
 src/components/AttachmentCarousel/Lightbox.js |  5 +++
 src/components/AttachmentCarousel/index.js    | 13 ------
 4 files changed, 44 insertions(+), 28 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index d9bb2b1e62a7..ab43cdd68053 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -10,7 +10,6 @@ import CarouselButtons from '../CarouselButtons';
 const propTypes = {
     // eslint-disable-next-line react/forbid-prop-types
     carouselState: PropTypes.object.isRequired,
-
     updatePage: PropTypes.func.isRequired,
     toggleArrowsVisibility: PropTypes.func.isRequired,
     autoHideArrow: PropTypes.func.isRequired,
@@ -103,7 +102,11 @@ function AttachmentCarouselView(props) {
     );
 
     return (
-        <>
+        <View
+            onMouseEnter={() => props.toggleArrowsVisibility(true)}
+            onMouseLeave={() => props.toggleArrowsVisibility(false)}
+            style={styles.flex1}
+        >
             <CarouselButtons
                 carouselState={props.carouselState}
                 onBack={() => {
@@ -146,7 +149,7 @@ function AttachmentCarouselView(props) {
             )}
 
             <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
-        </>
+        </View>
     );
 }
 
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index b2f039cc5f6b..4b81b7ff0b1a 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -7,7 +7,7 @@ import CarouselButtons from '../CarouselButtons';
 const propTypes = {
     // eslint-disable-next-line react/forbid-prop-types
     carouselState: PropTypes.object.isRequired,
-    updatePageByIndex: PropTypes.func.isRequired,
+    updatePage: PropTypes.func.isRequired,
     toggleArrowsVisibility: PropTypes.func.isRequired,
     autoHideArrow: PropTypes.func.isRequired,
     cancelAutoHideArrow: PropTypes.func.isRequired,
@@ -24,27 +24,44 @@ function AttachmentCarouselView(props) {
     // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
     // promotes the natural feeling of swiping left/right to go to the next/previous image
     const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
-    const reversedPage = useMemo(() => props.carouselState.attachments.length - props.carouselState.page - 1, [props.carouselState.attachments, props.carouselState.page]);
+
+    const reversePage = useCallback(
+        () => Math.max(0, Math.min(props.carouselState.attachments.length - props.carouselState.page - 1, props.carouselState.attachments.length)),
+        [props.carouselState.attachments.length, props.carouselState.page],
+    );
+
+    const reversedPage = useMemo(() => reversePage(), [reversePage]);
 
     /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
+     * Update carousel page based on next page index
+     * @param {Number} newPageIndex
      */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide) => {
-            const nextCarouselPage = props.carouselState.page - deltaSlide;
-            const nextItem = reversedAttachments[nextCarouselPage];
+    const updatePage = useCallback(
+        (newPageIndex) => {
+            const nextItem = reversedAttachments[newPageIndex];
 
-            const nextPagerIndex = reversedPage + deltaSlide;
+            console.log('updatePage', newPageIndex);
 
             if (!nextItem) {
                 return;
             }
 
-            pagerRef.current.setPage(nextPagerIndex);
-            props.updatePageByIndex(nextCarouselPage);
+            // pagerRef.current.setPage(nextPagerIndex);
+            props.updatePage([nextItem]);
+        },
+        [props, reversedAttachments],
+    );
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide) => {
+            const nextPageIndex = props.carouselState.page - deltaSlide;
+            updatePage(nextPageIndex);
         },
-        [props, reversedAttachments, reversedPage],
+        [props.carouselState.page, updatePage],
     );
 
     return (
@@ -67,6 +84,10 @@ function AttachmentCarouselView(props) {
                 <Pager
                     items={reversedAttachments}
                     initialIndex={reversedPage}
+                    onPageScroll={({position: newPage}) => {
+                        console.log(newPage);
+                        updatePage(reversePage(newPage));
+                    }}
                     onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
                     onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
                     onSwipeDown={props.onClose}
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index d7d2c25a3589..1674536f332c 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -762,6 +762,7 @@ const pagerPropTypes = {
         }),
     ).isRequired,
     initialIndex: PropTypes.number,
+    onPageScroll: PropTypes.func,
     onTap: PropTypes.func,
     onSwipe: PropTypes.func,
     onSwipeSuccess: PropTypes.func,
@@ -775,6 +776,7 @@ const pagerPropTypes = {
 
 const pagerDefaultProps = {
     initialIndex: 0,
+    onPageScroll: () => {},
     onTap: () => {},
     onSwipe: noopWorklet,
     onSwipeSuccess: () => {},
@@ -786,6 +788,7 @@ const pagerDefaultProps = {
 function Pager({
     items,
     initialIndex = 0,
+    onPageScroll,
     onTap,
     onSwipe = noopWorklet,
     onSwipeSuccess = () => {},
@@ -809,6 +812,8 @@ function Pager({
 
                 activeIndex.value = e.position;
                 isScrolling.value = e.offset !== 0;
+
+                runOnJS(onPageScroll)(e);
             },
         },
         [],
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 15d001c5ce35..cc3ce7652581 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -52,7 +52,6 @@ class AttachmentCarousel extends React.Component {
         this.autoHideArrow = this.autoHideArrow.bind(this);
         this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
         this.updatePage = this.updatePage.bind(this);
-        this.updatePageByIndex = this.updatePageByIndex.bind(this);
         this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
         this.createInitialState = this.createInitialState.bind(this);
 
@@ -174,15 +173,6 @@ class AttachmentCarousel extends React.Component {
         this.setState({page, activeSource: entry.item.source});
     }
 
-    /**
-     * Updates the page state by index when the user navigates between attachments
-     * @param {number} index
-     */
-    updatePageByIndex(index) {
-        // eslint-disable-next-line react/no-unused-state
-        this.setState({page: index});
-    }
-
     render() {
         return (
             <View
@@ -191,13 +181,10 @@ class AttachmentCarousel extends React.Component {
                     // eslint-disable-next-line react/no-unused-state
                     this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), containerHeight: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
                 }
-                onMouseEnter={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(true)}
-                onMouseLeave={() => !this.canUseTouchScreen && this.toggleArrowsVisibility(false)}
             >
                 <AttachmentCarouselView
                     carouselState={this.state}
                     updatePage={this.updatePage}
-                    updatePageByIndex={this.updatePageByIndex}
                     toggleArrowsVisibility={this.toggleArrowsVisibility}
                     autoHideArrow={this.autoHideArrow}
                     cancelAutoHideArrow={this.cancelAutoHideArrow}

From 36b858e159ddf2c424af33fb3384386634b1ee69 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 13:26:58 +0200
Subject: [PATCH 033/133] fix paging

---
 .../AttachmentCarouselView/index.js           |  8 +++++-
 .../AttachmentCarouselView/index.native.js    | 26 ++++++++-----------
 src/components/AttachmentCarousel/Lightbox.js |  9 +++----
 src/components/AttachmentCarousel/index.js    | 17 ++++++------
 4 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index ab43cdd68053..5092e7ca1974 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -1,6 +1,7 @@
 import React, {useRef, useCallback} from 'react';
 import {View, FlatList, PixelRatio} from 'react-native';
 import PropTypes from 'prop-types';
+import _ from 'underscore';
 import styles from '../../../styles/styles';
 import CarouselActions from '../CarouselActions';
 import AttachmentView from '../../AttachmentView';
@@ -144,7 +145,12 @@ function AttachmentCarouselView(props) {
                     getItemLayout={getItemLayout}
                     keyExtractor={(item) => item.source}
                     viewabilityConfig={viewabilityConfig}
-                    onViewableItemsChanged={props.updatePage}
+                    onViewableItemsChanged={({viewableItems}) => {
+                        // Since we can have only one item in view at a time, we can use the first item in the array
+                        // to get the index of the current page
+                        const entry = _.first(viewableItems);
+                        props.updatePage({item: entry.item, index: entry.index});
+                    }}
                 />
             )}
 
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 4b81b7ff0b1a..93d96e521207 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -26,11 +26,11 @@ function AttachmentCarouselView(props) {
     const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
 
     const reversePage = useCallback(
-        () => Math.max(0, Math.min(props.carouselState.attachments.length - props.carouselState.page - 1, props.carouselState.attachments.length)),
-        [props.carouselState.attachments.length, props.carouselState.page],
+        (page) => Math.max(0, Math.min(props.carouselState.attachments.length - page - 1, props.carouselState.attachments.length)),
+        [props.carouselState.attachments.length],
     );
 
-    const reversedPage = useMemo(() => reversePage(), [reversePage]);
+    const reversedPage = useMemo(() => reversePage(props.carouselState.page), [props.carouselState.page, reversePage]);
 
     /**
      * Update carousel page based on next page index
@@ -39,17 +39,13 @@ function AttachmentCarouselView(props) {
     const updatePage = useCallback(
         (newPageIndex) => {
             const nextItem = reversedAttachments[newPageIndex];
-
-            console.log('updatePage', newPageIndex);
-
             if (!nextItem) {
                 return;
             }
 
-            // pagerRef.current.setPage(nextPagerIndex);
-            props.updatePage([nextItem]);
+            props.updatePage({item: nextItem, index: reversePage(newPageIndex)});
         },
-        [props, reversedAttachments],
+        [props, reversePage, reversedAttachments],
     );
 
     /**
@@ -58,10 +54,13 @@ function AttachmentCarouselView(props) {
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextPageIndex = props.carouselState.page - deltaSlide;
+            const nextPageIndex = reversedPage + deltaSlide;
             updatePage(nextPageIndex);
+            pagerRef.current.setPage(nextPageIndex);
+
+            props.autoHideArrow();
         },
-        [props.carouselState.page, updatePage],
+        [props, reversedPage, updatePage],
     );
 
     return (
@@ -84,10 +83,7 @@ function AttachmentCarouselView(props) {
                 <Pager
                     items={reversedAttachments}
                     initialIndex={reversedPage}
-                    onPageScroll={({position: newPage}) => {
-                        console.log(newPage);
-                        updatePage(reversePage(newPage));
-                    }}
+                    onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
                     onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
                     onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
                     onSwipeDown={props.onClose}
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 1674536f332c..9f8f5a0173b2 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -762,7 +762,7 @@ const pagerPropTypes = {
         }),
     ).isRequired,
     initialIndex: PropTypes.number,
-    onPageScroll: PropTypes.func,
+    onPageSelected: PropTypes.func,
     onTap: PropTypes.func,
     onSwipe: PropTypes.func,
     onSwipeSuccess: PropTypes.func,
@@ -776,7 +776,7 @@ const pagerPropTypes = {
 
 const pagerDefaultProps = {
     initialIndex: 0,
-    onPageScroll: () => {},
+    onPageSelected: () => {},
     onTap: () => {},
     onSwipe: noopWorklet,
     onSwipeSuccess: () => {},
@@ -788,7 +788,7 @@ const pagerDefaultProps = {
 function Pager({
     items,
     initialIndex = 0,
-    onPageScroll,
+    onPageSelected,
     onTap,
     onSwipe = noopWorklet,
     onSwipeSuccess = () => {},
@@ -812,8 +812,6 @@ function Pager({
 
                 activeIndex.value = e.position;
                 isScrolling.value = e.offset !== 0;
-
-                runOnJS(onPageScroll)(e);
             },
         },
         [],
@@ -862,6 +860,7 @@ function Pager({
                     pageMargin={40}
                     onPageScroll={pageScrollHandler}
                     animatedProps={animatedProps}
+                    onPageSelected={onPageSelected}
                     ref={pagerRef}
                     style={styles.flex1}
                     initialPage={initialIndex}
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index cc3ce7652581..afe5f6278011 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -154,23 +154,22 @@ class AttachmentCarousel extends React.Component {
 
     /**
      * Updates the page state when the user navigates between attachments
-     * @param {Array<{item: {source, file}, index: Number}>} viewableItems
+     * @param {Object} item
+     * @param {number} index
      */
-    updatePage({viewableItems}) {
+    updatePage({item, index}) {
         Keyboard.dismiss();
-        // Since we can have only one item in view at a time, we can use the first item in the array
-        // to get the index of the current page
-        const entry = _.first(viewableItems);
-        if (!entry) {
+
+        if (!item) {
             // eslint-disable-next-line react/no-unused-state
             this.setState({activeSource: null});
             return;
         }
 
-        const page = entry.index;
-        this.props.onNavigate(entry.item);
+        const page = index;
+        this.props.onNavigate(item);
         // eslint-disable-next-line react/no-unused-state
-        this.setState({page, activeSource: entry.item.source});
+        this.setState({page, activeSource: item.source});
     }
 
     render() {

From 6995620636de112a81130c0a89284ac152fd5924 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 13:56:36 +0200
Subject: [PATCH 034/133] fix: attachment carousel on web

---
 .../AttachmentCarouselView/index.js           | 33 +++++++++++--------
 .../AttachmentCarouselView/index.native.js    |  6 ++--
 src/components/AttachmentCarousel/index.js    |  2 +-
 src/styles/styles.js                          |  9 +++--
 4 files changed, 31 insertions(+), 19 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 5092e7ca1974..e14a4284e626 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -8,6 +8,12 @@ import AttachmentView from '../../AttachmentView';
 import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
 import CarouselButtons from '../CarouselButtons';
 
+const VIEWABILITY_CONFIG = {
+    // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+    // more than 95% visible. When that happens we update the page index in the state.
+    itemVisiblePercentThreshold: 95,
+};
+
 const propTypes = {
     // eslint-disable-next-line react/forbid-prop-types
     carouselState: PropTypes.object.isRequired,
@@ -21,11 +27,6 @@ const propTypes = {
 
 function AttachmentCarouselView(props) {
     const scrollRef = useRef(null);
-    const viewabilityConfig = {
-        // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-        // more than 95% visible. When that happens we update the page index in the state.
-        itemVisiblePercentThreshold: 95,
-    };
 
     /**
      * Calculate items layout information to optimize scrolling performance
@@ -102,11 +103,23 @@ function AttachmentCarouselView(props) {
         [props.carouselState.activeSource],
     );
 
+    const handleViewableItemsChange = useCallback(
+        ({viewableItems}) => {
+            // Since we can have only one item in view at a time, we can use the first item in the array
+            // to get the index of the current page
+            const entry = _.first([...viewableItems]);
+            props.updatePage({item: entry.item, index: entry.index});
+        },
+        [props],
+    );
+
+    const viewabilityConfigCallbackPairs = useRef([{viewabilityConfig: VIEWABILITY_CONFIG, onViewableItemsChanged: handleViewableItemsChange}]);
+
     return (
         <View
             onMouseEnter={() => props.toggleArrowsVisibility(true)}
             onMouseLeave={() => props.toggleArrowsVisibility(false)}
-            style={styles.flex1}
+            style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}
         >
             <CarouselButtons
                 carouselState={props.carouselState}
@@ -144,13 +157,7 @@ function AttachmentCarouselView(props) {
                     renderItem={renderItem}
                     getItemLayout={getItemLayout}
                     keyExtractor={(item) => item.source}
-                    viewabilityConfig={viewabilityConfig}
-                    onViewableItemsChanged={({viewableItems}) => {
-                        // Since we can have only one item in view at a time, we can use the first item in the array
-                        // to get the index of the current page
-                        const entry = _.first(viewableItems);
-                        props.updatePage({item: entry.item, index: entry.index});
-                    }}
+                    viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
                 />
             )}
 
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 93d96e521207..32379009eb01 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,7 +1,9 @@
 import React, {useCallback, useMemo, useRef} from 'react';
+import {View} from 'react-native';
 import PropTypes from 'prop-types';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import Pager from '../Lightbox';
+import styles from '../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
 
 const propTypes = {
@@ -64,7 +66,7 @@ function AttachmentCarouselView(props) {
     );
 
     return (
-        <>
+        <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
                 carouselState={props.carouselState}
                 onBack={() => {
@@ -93,7 +95,7 @@ function AttachmentCarouselView(props) {
                     ref={pagerRef}
                 />
             )}
-        </>
+        </View>
     );
 }
 
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index afe5f6278011..18d833c8d79a 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -175,7 +175,7 @@ class AttachmentCarousel extends React.Component {
     render() {
         return (
             <View
-                style={[styles.attachmentModalArrowsContainer, styles.flex1]}
+                style={[styles.flex1, styles.attachmentCarouselContainer]}
                 onLayout={({nativeEvent}) =>
                     // eslint-disable-next-line react/no-unused-state
                     this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), containerHeight: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 74066af5e20a..09ad5632d624 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2392,14 +2392,17 @@ const styles = {
         alignSelf: 'flex-start',
     },
 
-    attachmentModalArrowsContainer: {
-        display: 'flex',
-        justifyContent: 'center',
+    attachmentCarouselContainer: {
         height: '100%',
         width: '100%',
         ...cursor.cursorUnset,
     },
 
+    attachmentCarouselButtonsContainer: {
+        display: 'flex',
+        justifyContent: 'center',
+    },
+
     attachmentArrow: {
         zIndex: 23,
         position: 'absolute',

From 6504ebd2f1ad7d84dec6bd08d5e041fd8d7d5938 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 28 Jun 2023 14:04:43 +0200
Subject: [PATCH 035/133] extract propTypes

---
 .../AttachmentCarouselView/index.js           | 12 ++----
 .../AttachmentCarouselView/index.native.js    | 19 +---------
 .../AttachmentCarouselView/propTypes.js       | 37 +++++++++++++++++++
 3 files changed, 42 insertions(+), 26 deletions(-)
 create mode 100644 src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index e14a4284e626..5a3e54fa88fd 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -1,12 +1,12 @@
 import React, {useRef, useCallback} from 'react';
 import {View, FlatList, PixelRatio} from 'react-native';
-import PropTypes from 'prop-types';
 import _ from 'underscore';
 import styles from '../../../styles/styles';
 import CarouselActions from '../CarouselActions';
 import AttachmentView from '../../AttachmentView';
 import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
 import CarouselButtons from '../CarouselButtons';
+import attachmentCarouselViewPropTypes from './propTypes';
 
 const VIEWABILITY_CONFIG = {
     // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -15,13 +15,7 @@ const VIEWABILITY_CONFIG = {
 };
 
 const propTypes = {
-    // eslint-disable-next-line react/forbid-prop-types
-    carouselState: PropTypes.object.isRequired,
-    updatePage: PropTypes.func.isRequired,
-    toggleArrowsVisibility: PropTypes.func.isRequired,
-    autoHideArrow: PropTypes.func.isRequired,
-    cancelAutoHideArrow: PropTypes.func.isRequired,
-
+    ...attachmentCarouselViewPropTypes,
     ...windowDimensionsPropTypes,
 };
 
@@ -35,7 +29,7 @@ function AttachmentCarouselView(props) {
      * @returns {{offset: Number, length: Number, index: Number}}
      */
     const getItemLayout = useCallback(
-        (data, index) => ({
+        (_data, index) => ({
             length: props.carouselState.containerWidth,
             offset: props.carouselState.containerWidth * index,
             index,
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 32379009eb01..395daca2f8ee 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,24 +1,10 @@
 import React, {useCallback, useMemo, useRef} from 'react';
 import {View} from 'react-native';
-import PropTypes from 'prop-types';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import Pager from '../Lightbox';
 import styles from '../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
-
-const propTypes = {
-    // eslint-disable-next-line react/forbid-prop-types
-    carouselState: PropTypes.object.isRequired,
-    updatePage: PropTypes.func.isRequired,
-    toggleArrowsVisibility: PropTypes.func.isRequired,
-    autoHideArrow: PropTypes.func.isRequired,
-    cancelAutoHideArrow: PropTypes.func.isRequired,
-    onClose: PropTypes.func,
-};
-
-const defaultProps = {
-    onClose: () => {},
-};
+import attachmentCarouselViewPropTypes from './propTypes';
 
 function AttachmentCarouselView(props) {
     const pagerRef = useRef(null);
@@ -99,7 +85,6 @@ function AttachmentCarouselView(props) {
     );
 }
 
-AttachmentCarouselView.propTypes = propTypes;
-AttachmentCarouselView.defaultProps = defaultProps;
+AttachmentCarouselView.propTypes = attachmentCarouselViewPropTypes;
 
 export default AttachmentCarouselView;
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
new file mode 100644
index 000000000000..14bb08c0eda8
--- /dev/null
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -0,0 +1,37 @@
+import PropTypes from 'prop-types';
+
+const propTypes = {
+    /**
+     * The current state of the carousel
+     */
+    carouselState: PropTypes.shape({
+        page: PropTypes.number.isRequired,
+        attachments: PropTypes.arrayOf(PropTypes.element).isRequired,
+        shouldShowArrow: PropTypes.bool.isRequired,
+        containerWidth: PropTypes.number.isRequired,
+        containerHeight: PropTypes.number.isRequired,
+        activeSource: PropTypes.string,
+    }).isRequired,
+
+    /**
+     * A callback to update the page in the carousel component
+     */
+    updatePage: PropTypes.func.isRequired,
+
+    /**
+     * A callback for toggling the visibility of the arrows
+     */
+    toggleArrowsVisibility: PropTypes.func.isRequired,
+
+    /**
+     * Trigger "auto-hiding" of the arrow buttons in the carousel
+     */
+    autoHideArrow: PropTypes.func.isRequired,
+
+    /**
+     * Cancel "auto-hiding" of the arrow buttons in the carousel
+     */
+    cancelAutoHideArrow: PropTypes.func.isRequired,
+};
+
+export default propTypes;

From 4f19c0c2d38c8b900d0795ed567aef2d822542fc Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Fri, 14 Jul 2023 11:45:44 +0200
Subject: [PATCH 036/133] fix: lint error

---
 src/components/AttachmentCarousel/Lightbox.js | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 9f8f5a0173b2..039b2e8f55fa 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,6 +1,6 @@
 /* eslint-disable es/no-optional-chaining */
 import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
-import {ActivityIndicator, PixelRatio, StyleSheet, View, useWindowDimensions} from 'react-native';
+import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
 import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
 import Animated, {
@@ -20,6 +20,7 @@ import Animated, {
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
 import Image from '../Image';
 import styles from '../../styles/styles';
 
@@ -69,7 +70,7 @@ function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight
 // eslint-disable-next-line react/prop-types
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
-    const windowDimensions = useWindowDimensions();
+    const {windowWidth, windowHeight} = useWindowDimensions();
 
     const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
 
@@ -85,12 +86,12 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     );
 
     const dimensionsWithScale = useMemo(() => {
-        const scaleFactorX = windowDimensions.width / imageDimensions.width;
-        const scaleFactorY = windowDimensions.height / imageDimensions.height;
+        const scaleFactorX = windowWidth / imageDimensions.width;
+        const scaleFactorY = windowHeight / imageDimensions.height;
 
         const scaledWidth = imageDimensions.width * scaleFactorY;
 
-        if (scaledWidth > windowDimensions.width) {
+        if (scaledWidth > windowWidth) {
             return {
                 ...imageDimensions,
                 scale: scaleFactorX,
@@ -101,7 +102,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             ...imageDimensions,
             scale: scaleFactorY,
         };
-    }, [imageDimensions, windowDimensions]);
+    }, [imageDimensions, windowHeight, windowWidth]);
 
     const canvasX = useSharedValue(canvasWidth);
     const canvasY = useSharedValue(canvasHeight);

From a1fd5c2b77500e47cb5ae3cef2c6892fa7237caf Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 17 Jul 2023 16:38:56 +0200
Subject: [PATCH 037/133] feat: implement real size image rendering

---
 src/components/AttachmentCarousel/Lightbox.js | 200 +++++++-----------
 1 file changed, 75 insertions(+), 125 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 039b2e8f55fa..7235c68f06db 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,5 +1,5 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {createContext, useContext, useEffect, useRef, useState, useMemo, useImperativeHandle} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
 import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
@@ -20,7 +20,7 @@ import Animated, {
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
-import useWindowDimensions from '../../hooks/useWindowDimensions';
+import FastImage from 'react-native-fast-image';
 import Image from '../Image';
 import styles from '../../styles/styles';
 
@@ -34,11 +34,6 @@ const SPRING_CONFIG = {
     damping: 500,
 };
 
-const DEFAULT_DIMENSIONS = {
-    width: 1,
-    height: 1,
-};
-
 const Context = createContext(null);
 
 function clamp(value, lowerBound, upperBound) {
@@ -47,93 +42,55 @@ function clamp(value, lowerBound, upperBound) {
     return Math.min(Math.max(lowerBound, value), upperBound);
 }
 
-function getScaledDimensions({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
-    const scaleFactorX = imageWidth / canvasWidth;
-    const scaleFactorY = imageHeight / canvasHeight;
-
-    const scaledWidth = imageWidth / scaleFactorY;
-    const scaledHeight = imageHeight / scaleFactorX;
-
-    if (scaledWidth > canvasWidth) {
-        return {
-            width: canvasWidth,
-            height: scaledHeight,
-        };
-    }
+function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
+    const scaleFactorX = canvasWidth / imageWidth;
+    const scaleFactorY = canvasHeight / imageHeight;
 
-    return {
-        width: scaledWidth,
-        height: canvasHeight,
-    };
+    return scaleFactorY > scaleFactorX ? scaleFactorX : scaleFactorY;
 }
 
 // eslint-disable-next-line react/prop-types
 function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
-    const {windowWidth, windowHeight} = useWindowDimensions();
 
     const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
 
-    const [imageDimensions, setImageDimensions] = useState(
-        showFallback
-            ? DEFAULT_DIMENSIONS
-            : getScaledDimensions({
-                  canvasWidth,
-                  canvasHeight,
-                  imageWidth,
-                  imageHeight,
-              }),
-    );
-
-    const dimensionsWithScale = useMemo(() => {
-        const scaleFactorX = windowWidth / imageDimensions.width;
-        const scaleFactorY = windowHeight / imageDimensions.height;
-
-        const scaledWidth = imageDimensions.width * scaleFactorY;
+    const [imageDimensions, setImageDimensions] = useState(showFallback ? {width: 0, height: 0} : {width: imageWidth, height: imageHeight});
 
-        if (scaledWidth > windowWidth) {
-            return {
-                ...imageDimensions,
-                scale: scaleFactorX,
-            };
-        }
-
-        return {
-            ...imageDimensions,
-            scale: scaleFactorY,
-        };
-    }, [imageDimensions, windowHeight, windowWidth]);
+    const targetDimensions = {
+        width: useSharedValue(0),
+        height: useSharedValue(0),
+    };
 
-    const canvasX = useSharedValue(canvasWidth);
-    const canvasY = useSharedValue(canvasHeight);
+    const canvas = {
+        width: useSharedValue(canvasWidth),
+        height: useSharedValue(canvasHeight),
+    };
 
+    // Update shared values on UI thread when canvas dimensions change
     useEffect(() => {
         runOnUI(() => {
             'worklet';
 
-            canvasX.value = canvasWidth;
-            canvasY.value = canvasHeight;
+            canvas.width.value = canvasWidth;
+            canvas.height.value = canvasHeight;
         })();
-    }, [canvasX, canvasY, canvasWidth, canvasHeight]);
+    }, [canvas.width, canvas.height, canvasHeight, canvasWidth]);
+
+    const canvasFitScale = useSharedValue(0);
+    const zoomScale = useSharedValue(1);
 
-    const targetWidth = useSharedValue(0);
-    const targetHeight = useSharedValue(0);
+    // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
+    // Substracting 1, because both scales have the initial image as the base reference
+    const totalScale = useDerivedValue(() => zoomScale.value + canvasFitScale.value - 1);
 
     const onLoad = (resolvedDimensions) => {
-        const {width, height} = getScaledDimensions({
-            canvasWidth,
-            canvasHeight,
-            imageWidth: resolvedDimensions.width,
-            imageHeight: resolvedDimensions.height,
-        });
+        setImageDimensions(resolvedDimensions);
 
-        targetWidth.value = width;
-        targetHeight.value = height;
+        targetDimensions.width.value = resolvedDimensions.width;
+        targetDimensions.height.value = resolvedDimensions.height;
 
-        setImageDimensions({
-            width,
-            height,
-        });
+        canvasFitScale.value = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth: resolvedDimensions.width, imageHeight: resolvedDimensions.height});
 
         setShowFallback(false);
     };
@@ -159,39 +116,36 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     const panVelocityX = useSharedValue(0);
     const panVelocityY = useSharedValue(0);
 
-    const scale = useSharedValue(1);
     // store scale in between gestures
     const scaleOffset = useSharedValue(1);
 
     // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvasY.value < targetHeight.value * scale.value);
+    const canPanVertically = useDerivedValue(() => canvas.height.value < targetDimensions.height.value * totalScale.value);
 
     // calculates bounds of the scaled image
     // can we pan left/right/up/down
     // can be used to limit gesture or implementing tension effect
     const getBounds = useWorkletCallback(() => {
-        const target = {
-            x: 0,
-            y: 0,
-        };
+        const finalScale = zoomScale.value + canvasFitScale.value - 1;
 
-        const fixedScale = clamp(MIN_SCALE, scale.value, MAX_SCALE);
-        const scaledImageWidth = targetWidth.value * fixedScale;
-        const scaledImageHeight = targetHeight.value * fixedScale;
+        const scaledImageWidth = targetDimensions.width.value * finalScale;
+        const scaledImageHeight = targetDimensions.height.value * finalScale;
 
-        const rightBoundary = Math.abs(canvasX.value - scaledImageWidth) / 2;
+        const rightBoundary = Math.abs(canvas.width.value - scaledImageWidth) / 2;
 
         let topBoundary = 0;
 
-        if (canvasY.value < scaledImageHeight) {
-            topBoundary = Math.abs(scaledImageHeight - canvasY.value) / 2;
+        if (canvas.height.value < scaledImageHeight) {
+            topBoundary = Math.abs(scaledImageHeight - canvas.height.value) / 2;
         }
 
         const maxVector = {x: rightBoundary, y: topBoundary};
         const minVector = {x: -rightBoundary, y: -topBoundary};
 
-        target.x = clamp(offsetX.value, minVector.x, maxVector.x);
-        target.y = clamp(offsetY.value, minVector.y, maxVector.y);
+        const target = {
+            x: clamp(offsetX.value, minVector.x, maxVector.x),
+            y: clamp(offsetY.value, minVector.y, maxVector.y),
+        };
 
         const isInBoundaryX = target.x === offsetX.value;
         const isInBoundaryY = target.y === offsetY.value;
@@ -205,7 +159,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             canPanLeft: target.x < maxVector.x,
             canPanRight: target.x > minVector.x,
         };
-    });
+    }, [imageDimensions]);
 
     const afterGesture = useWorkletCallback(() => {
         const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
@@ -215,7 +169,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         }
 
         if (
-            scale.value === 1 &&
+            zoomScale.value === 1 &&
             offsetX.value === 0 &&
             offsetY.value === 0 &&
             translateX.value === 0 &&
@@ -227,7 +181,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             return;
         }
 
-        if (scale.value <= 1) {
+        if (zoomScale.value <= 1) {
             // just center it
             // reset(true);
             offsetX.value = withSpring(0, SPRING_CONFIG);
@@ -238,7 +192,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         const deceleration = 0.9915;
 
         if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && scale.value <= MAX_SCALE) {
+            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_SCALE) {
                 offsetX.value = withDecay({
                     velocity: panVelocityX.value,
                     clamp: [minVector.x, maxVector.x],
@@ -253,7 +207,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         if (isInBoundaryY) {
             if (
                 Math.abs(panVelocityY.value) > 0 &&
-                scale.value <= MAX_SCALE &&
+                zoomScale.value <= MAX_SCALE &&
                 // limit vertical pan only when image is smaller than screen
                 offsetY.value !== minVector.y &&
                 offsetY.value !== maxVector.y
@@ -282,8 +236,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         stopAnimation();
 
         const usableImage = {
-            x: targetWidth.value,
-            y: targetHeight.value,
+            x: targetDimensions.width.value * canvasFitScale.value,
+            y: targetDimensions.height.value * canvasFitScale.value,
         };
 
         const targetImageSize = {
@@ -292,8 +246,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         };
 
         const CENTER = {
-            x: canvasX.value / 2,
-            y: canvasY.value / 2,
+            x: canvas.width.value / 2,
+            y: canvas.height.value / 2,
         };
 
         const imageCenter = {
@@ -318,13 +272,13 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             y: currentOrigin.y * koef.y,
         };
 
-        if (targetImageSize.y < canvasY.value) {
+        if (targetImageSize.y < canvas.height.value) {
             target.y = 0;
         }
 
         offsetX.value = withSpring(target.x, SPRING_CONFIG);
         offsetY.value = withSpring(target.y, SPRING_CONFIG);
-        scale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
+        zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
         scaleOffset.value = DOUBLE_TAP_SCALE;
     });
 
@@ -336,9 +290,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         if (animated) {
             offsetX.value = withSpring(0, SPRING_CONFIG);
             offsetY.value = withSpring(0, SPRING_CONFIG);
-            scale.value = withSpring(1, SPRING_CONFIG);
+            zoomScale.value = withSpring(1, SPRING_CONFIG);
         } else {
-            scale.value = 1;
+            zoomScale.value = 1;
             translateX.value = 0;
             translateY.value = 0;
             offsetX.value = 0;
@@ -353,7 +307,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         .maxDelay(150)
         .maxDistance(20)
         .onEnd((evt) => {
-            if (scale.value > 1) {
+            if (zoomScale.value > 1) {
                 reset(true);
             } else {
                 zoomToCoordinates(evt.x, evt.y);
@@ -381,7 +335,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         .manualActivation(true)
         .averageTouches(true)
         .onTouchesMove((evt, state) => {
-            if (scale.value > 1) {
+            if (zoomScale.value > 1) {
                 state.activate();
             }
 
@@ -457,7 +411,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                     };
 
                     offsetY.value = withSpring(
-                        maybeInvert(targetHeight.value * 2),
+                        maybeInvert(targetDimensions.height.value * 2),
                         {
                             stiffness: 50,
                             damping: 30,
@@ -483,8 +437,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         .withRef(panGestureRef);
 
     const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
-        x: focalX - (canvasX.value / 2 + offsetX.value),
-        y: focalY - (canvasY.value / 2 + offsetY.value),
+        x: focalX - (canvas.width.value / 2 + offsetX.value),
+        y: focalY - (canvas.height.value / 2 + offsetY.value),
     }));
 
     // used to store event scale value when we limit scale
@@ -510,9 +464,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            scale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
 
-            if (scale.value > MIN_SCALE && scale.value < MAX_SCALE) {
+            if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
                 gestureScale.value = evt.scale;
             }
 
@@ -526,7 +480,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
             offsetY.value += scaleTranslateY.value;
             scaleTranslateX.value = 0;
             scaleTranslateY.value = 0;
-            scaleOffset.value = scale.value;
+            scaleOffset.value = zoomScale.value;
             gestureScale.value = 1;
 
             if (scaleOffset.value < 1) {
@@ -534,10 +488,10 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 scaleOffset.value = 1;
 
                 // this runs the timing animation
-                scale.value = withSpring(1, SPRING_CONFIG);
+                zoomScale.value = withSpring(1, SPRING_CONFIG);
             } else if (scaleOffset.value > MAX_SCALE) {
                 scaleOffset.value = MAX_SCALE;
-                scale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
+                zoomScale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
             }
 
             afterGesture();
@@ -547,7 +501,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
 
     const isPinchGestureInUse = useSharedValue(false);
     useAnimatedReaction(
-        () => [scale.value, pinchGestureRunning.value],
+        () => [zoomScale.value, pinchGestureRunning.value],
         ([s, running]) => {
             const newIsPinchGestureInUse = s !== 1 || running;
             if (isPinchGestureInUse.value !== newIsPinchGestureInUse) {
@@ -577,16 +531,16 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                 {
                     translateY: y,
                 },
-                {scale: scale.value},
+                {scale: totalScale.value},
             ],
         };
     }, []);
 
     // reacts to scale change and enables/disables pager scroll
     useAnimatedReaction(
-        () => scale.value,
+        () => zoomScale.value,
         () => {
-            shouldPagerScroll.value = scale.value === 1;
+            shouldPagerScroll.value = zoomScale.value === 1;
         },
     );
 
@@ -602,6 +556,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         }
     }, [isActive, mounted, reset]);
 
+    const showImage = imageDimensions.width !== 0 && imageDimensions.height === 0;
+
     return (
         <View
             collapsable={false}
@@ -632,7 +588,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
 
                                         {renderImage({
                                             onResolveImageDimensions: onLoad,
-                                            ...dimensionsWithScale,
+                                            ...imageDimensions,
+                                            resizeMode: showImage ? undefined : 'contain',
                                             style: showFallback ? {...styles.opacity0, ...styles.pAbsolute} : {},
                                         })}
                                     </Animated.View>
@@ -671,6 +628,8 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
     const dimensions = cachedDimensions.get(item.url);
 
     if (!isActive) {
+        const scale = dimensions == null ? 1 : getCanvasFitScale({imageWidth: dimensions.width, height: dimensions.height, canvasWidth, canvasHeight});
+
         return (
             <ImageWrapper>
                 <Image
@@ -681,16 +640,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                             height: evt.nativeEvent.height,
                         });
                     }}
-                    style={
-                        dimensions == null
-                            ? {}
-                            : getScaledDimensions({
-                                  imageHeight: dimensions.height,
-                                  imageWidth: dimensions.width,
-                                  canvasHeight,
-                                  canvasWidth,
-                              })
-                    }
+                    style={dimensions == null ? undefined : {...dimensions, scale}}
                 />
             </ImageWrapper>
         );
@@ -710,7 +660,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
             renderFallback={() => <ActivityIndicator />}
             renderImage={({onResolveImageDimensions, width, height, style}) => (
                 <Image
-                    source={{uri: item.url, width: 100, height: 100}}
+                    source={{uri: item.url}}
                     style={[style, {width, height}]}
                     onLoad={(evt) => {
                         cachedDimensions.set(item.url, {

From 2575c486433b4b8ed270c1a5eb69bbf862adeea9 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Mon, 17 Jul 2023 17:00:52 +0200
Subject: [PATCH 038/133] fix: resizing

---
 src/components/AttachmentCarousel/Lightbox.js | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 7235c68f06db..39de373626ec 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -20,7 +20,6 @@ import Animated, {
 } from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
-import FastImage from 'react-native-fast-image';
 import Image from '../Image';
 import styles from '../../styles/styles';
 
@@ -50,7 +49,7 @@ function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight})
 }
 
 // eslint-disable-next-line react/prop-types
-function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
+function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, imageScale = 1, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
     const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
@@ -77,7 +76,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         })();
     }, [canvas.width, canvas.height, canvasHeight, canvasWidth]);
 
-    const canvasFitScale = useSharedValue(0);
+    const canvasFitScale = useSharedValue(imageScale);
     const zoomScale = useSharedValue(1);
 
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
@@ -556,8 +555,6 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         }
     }, [isActive, mounted, reset]);
 
-    const showImage = imageDimensions.width !== 0 && imageDimensions.height === 0;
-
     return (
         <View
             collapsable={false}
@@ -589,7 +586,6 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                                         {renderImage({
                                             onResolveImageDimensions: onLoad,
                                             ...imageDimensions,
-                                            resizeMode: showImage ? undefined : 'contain',
                                             style: showFallback ? {...styles.opacity0, ...styles.pAbsolute} : {},
                                         })}
                                     </Animated.View>
@@ -627,9 +623,9 @@ const pagePropTypes = {
 function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth, canvasHeight, onTap}) {
     const dimensions = cachedDimensions.get(item.url);
 
-    if (!isActive) {
-        const scale = dimensions == null ? 1 : getCanvasFitScale({imageWidth: dimensions.width, height: dimensions.height, canvasWidth, canvasHeight});
+    const imageScale = dimensions == null ? 1 : getCanvasFitScale({imageWidth: dimensions.width, imageHeight: dimensions.height, canvasWidth, canvasHeight});
 
+    if (!isActive) {
         return (
             <ImageWrapper>
                 <Image
@@ -640,7 +636,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                             height: evt.nativeEvent.height,
                         });
                     }}
-                    style={dimensions == null ? undefined : {...dimensions, scale}}
+                    style={dimensions == null ? undefined : {...dimensions, transform: [{scale: imageScale}]}}
                 />
             </ImageWrapper>
         );
@@ -655,6 +651,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
             onTap={onTap}
             imageHeight={dimensions?.height}
             imageWidth={dimensions?.width}
+            iamgeScale={imageScale}
             canvasHeight={canvasHeight}
             canvasWidth={canvasWidth}
             renderFallback={() => <ActivityIndicator />}

From efcab94595154f57dee7198947a3137ab90e394f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 11:51:27 +0200
Subject: [PATCH 039/133] fix: typo

---
 src/components/AttachmentCarousel/Lightbox.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 39de373626ec..bf88784cdfd2 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -651,7 +651,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
             onTap={onTap}
             imageHeight={dimensions?.height}
             imageWidth={dimensions?.width}
-            iamgeScale={imageScale}
+            imageScale={dimensions?.scale}
             canvasHeight={canvasHeight}
             canvasWidth={canvasWidth}
             renderFallback={() => <ActivityIndicator />}

From 312d9365d9e5b77764b85415649fa4d6bae67f3c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 14:25:30 +0200
Subject: [PATCH 040/133] Fix: implement scaling of non active images and
 improve loading state

---
 src/components/AttachmentCarousel/Lightbox.js | 190 ++++++++++--------
 1 file changed, 105 insertions(+), 85 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index bf88784cdfd2..f581ea0714ee 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -41,20 +41,23 @@ function clamp(value, lowerBound, upperBound) {
     return Math.min(Math.max(lowerBound, value), upperBound);
 }
 
-function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
-    const scaleFactorX = canvasWidth / imageWidth;
-    const scaleFactorY = canvasHeight / imageHeight;
-
-    return scaleFactorY > scaleFactorX ? scaleFactorX : scaleFactorY;
+// eslint-disable-next-line react/prop-types
+function ImageWrapper({children}) {
+    return (
+        <Animated.View
+            collapsable={false}
+            style={[StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter]}
+        >
+            {children}
+        </Animated.View>
+    );
 }
 
 // eslint-disable-next-line react/prop-types
-function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, imageScale = 1, isActive, onSwipe, onSwipeSuccess, renderImage, renderFallback, onTap}) {
+function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeight = 0, imageScale = 1, isActive, onSwipe, onSwipeSuccess, renderImage, onTap}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
-    const [showFallback, setShowFallback] = useState(typeof imageHeight === 'undefined' || typeof imageWidth === 'undefined');
-
-    const [imageDimensions, setImageDimensions] = useState(showFallback ? {width: 0, height: 0} : {width: imageWidth, height: imageHeight});
+    const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight});
 
     const targetDimensions = {
         width: useSharedValue(0),
@@ -66,33 +69,31 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
         height: useSharedValue(canvasHeight),
     };
 
+    const canvasFitScale = useSharedValue(imageScale);
+    const zoomScale = useSharedValue(1);
+    // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
+    // Substracting 1, because both scales have the initial image as the base reference
+    const totalScale = useDerivedValue(() => zoomScale.value + canvasFitScale.value - 1);
+
     // Update shared values on UI thread when canvas dimensions change
     useEffect(() => {
         runOnUI(() => {
             'worklet';
 
+            canvasFitScale.value = imageScale;
             canvas.width.value = canvasWidth;
             canvas.height.value = canvasHeight;
         })();
-    }, [canvas.width, canvas.height, canvasHeight, canvasWidth]);
-
-    const canvasFitScale = useSharedValue(imageScale);
-    const zoomScale = useSharedValue(1);
-
-    // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
-    // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => zoomScale.value + canvasFitScale.value - 1);
+    }, [canvas.height, canvas.width, canvasFitScale, canvasHeight, canvasWidth, imageScale]);
 
-    const onLoad = (resolvedDimensions) => {
-        setImageDimensions(resolvedDimensions);
-
-        targetDimensions.width.value = resolvedDimensions.width;
-        targetDimensions.height.value = resolvedDimensions.height;
+    useEffect(() => {
+        if (imageWidth === 0 || imageHeight === 0) return;
 
-        canvasFitScale.value = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth: resolvedDimensions.width, imageHeight: resolvedDimensions.height});
+        setImageDimensions({width: imageWidth, height: imageHeight});
 
-        setShowFallback(false);
-    };
+        targetDimensions.width.value = imageWidth;
+        targetDimensions.height.value = imageHeight;
+    }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth, targetDimensions.height, targetDimensions.width]);
 
     // used for pan gesture
     const translateY = useSharedValue(0);
@@ -581,12 +582,8 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
                                         collapsable={false}
                                         style={[animatedStyles]}
                                     >
-                                        {showFallback && renderFallback()}
-
                                         {renderImage({
-                                            onResolveImageDimensions: onLoad,
                                             ...imageDimensions,
-                                            style: showFallback ? {...styles.opacity0, ...styles.pAbsolute} : {},
                                         })}
                                     </Animated.View>
                                 </GestureDetector>
@@ -599,16 +596,11 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth, imageHeight, i
     );
 }
 
-// eslint-disable-next-line react/prop-types
-function ImageWrapper({children}) {
-    return (
-        <Animated.View
-            collapsable={false}
-            style={[StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter]}
-        >
-            {children}
-        </Animated.View>
-    );
+function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
+    const scaleFactorX = canvasWidth / imageWidth;
+    const scaleFactorY = canvasHeight / imageHeight;
+
+    return scaleFactorY > scaleFactorX ? scaleFactorX : scaleFactorY;
 }
 
 const cachedDimensions = new Map();
@@ -623,56 +615,84 @@ const pagePropTypes = {
 function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth, canvasHeight, onTap}) {
     const dimensions = cachedDimensions.get(item.url);
 
-    const imageScale = dimensions == null ? 1 : getCanvasFitScale({imageWidth: dimensions.width, imageHeight: dimensions.height, canvasWidth, canvasHeight});
-
-    if (!isActive) {
-        return (
-            <ImageWrapper>
-                <Image
-                    source={{uri: item.url}}
-                    onLoad={(evt) => {
-                        cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent.width,
-                            height: evt.nativeEvent.height,
-                        });
-                    }}
-                    style={dimensions == null ? undefined : {...dimensions, transform: [{scale: imageScale}]}}
-                />
-            </ImageWrapper>
-        );
-    }
+    const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0;
+
+    const [isImageLoading, setIsImageLoading] = useState(!areImageDimensionsSet);
+
+    useEffect(() => {
+        if (!isActive && !areImageDimensionsSet) return;
+        setIsImageLoading(true);
+    }, [areImageDimensionsSet, isActive]);
 
     return (
-        <ImageTransformer
-            onSwipe={onSwipe}
-            onSwipeSuccess={onSwipeSuccess}
-            onSwipeDown={onSwipeDown}
-            isActive
-            onTap={onTap}
-            imageHeight={dimensions?.height}
-            imageWidth={dimensions?.width}
-            imageScale={dimensions?.scale}
-            canvasHeight={canvasHeight}
-            canvasWidth={canvasWidth}
-            renderFallback={() => <ActivityIndicator />}
-            renderImage={({onResolveImageDimensions, width, height, style}) => (
-                <Image
-                    source={{uri: item.url}}
-                    style={[style, {width, height}]}
-                    onLoad={(evt) => {
-                        cachedDimensions.set(item.url, {
-                            width: evt.nativeEvent?.width,
-                            height: evt.nativeEvent?.height,
-                        });
-
-                        onResolveImageDimensions({
-                            width: evt.nativeEvent?.width / PixelRatio.get(),
-                            height: evt.nativeEvent?.height / PixelRatio.get(),
-                        });
-                    }}
+        <>
+            {isActive && (
+                <ImageTransformer
+                    onSwipe={onSwipe}
+                    onSwipeSuccess={onSwipeSuccess}
+                    onSwipeDown={onSwipeDown}
+                    isActive
+                    onTap={onTap}
+                    imageWidth={dimensions?.imageWidth}
+                    imageHeight={dimensions?.imageHeight}
+                    imageScale={dimensions?.imageScale}
+                    canvasHeight={canvasHeight}
+                    canvasWidth={canvasWidth}
+                    renderImage={({width, height, style}) => (
+                        <Image
+                            source={{uri: item.url}}
+                            style={[style, {width, height}]}
+                            onLoad={(evt) => {
+                                const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
+                                const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
+
+                                const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
+
+                                cachedDimensions.set(item.url, {
+                                    imageWidth,
+                                    imageHeight,
+                                    imageScale,
+                                });
+
+                                if (imageWidth === 0 || imageHeight === 0) return;
+                                console.log('set iamge loading false');
+                                setIsImageLoading(false);
+                            }}
+                        />
+                    )}
+                />
+            )}
+
+            {(!isActive || isImageLoading) && (
+                <ImageWrapper>
+                    <Image
+                        source={{uri: item.url}}
+                        onLoad={(evt) => {
+                            const imageWidth = evt.nativeEvent.width;
+                            const imageHeight = evt.nativeEvent.height;
+
+                            const scale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
+
+                            const scaledImageWidth = imageWidth * scale;
+                            const scaledImageHeight = imageHeight * scale;
+
+                            cachedDimensions.set(item.url, {
+                                scaledImageWidth,
+                                scaledImageHeight,
+                            });
+                        }}
+                        style={dimensions == null ? undefined : {width: dimensions.scaledImageWidth, height: dimensions.scaledImageHeight}}
+                    />
+                </ImageWrapper>
+            )}
+
+            {isActive && isImageLoading && (
+                <ActivityIndicator
+                    size="large"
+                    style={StyleSheet.absoluteFill}
                 />
             )}
-        />
+        </>
     );
 }
 Page.propTypes = pagePropTypes;

From 70f7688c6d6ef7bf90f0ebefd17213613fd33e08 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 14:44:56 +0200
Subject: [PATCH 041/133] simplify ImageTransformer

---
 src/components/AttachmentCarousel/Lightbox.js | 44 +++++++++----------
 1 file changed, 22 insertions(+), 22 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index f581ea0714ee..77df60a4bded 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -54,7 +54,7 @@ function ImageWrapper({children}) {
 }
 
 // eslint-disable-next-line react/prop-types
-function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeight = 0, imageScale = 1, isActive, onSwipe, onSwipeSuccess, renderImage, onTap}) {
+function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeight = 0, imageScale = 1, isActive, onSwipe, onSwipeSuccess, onTap, children}) {
     const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
     const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight});
@@ -582,9 +582,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
                                         collapsable={false}
                                         style={[animatedStyles]}
                                     >
-                                        {renderImage({
-                                            ...imageDimensions,
-                                        })}
+                                        {children}
                                     </Animated.View>
                                 </GestureDetector>
                             </ImageWrapper>
@@ -620,28 +618,29 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
     const [isImageLoading, setIsImageLoading] = useState(!areImageDimensionsSet);
 
     useEffect(() => {
-        if (!isActive && !areImageDimensionsSet) return;
+        if (!isActive) return;
         setIsImageLoading(true);
-    }, [areImageDimensionsSet, isActive]);
+    }, [isActive]);
 
     return (
         <>
             {isActive && (
-                <ImageTransformer
-                    onSwipe={onSwipe}
-                    onSwipeSuccess={onSwipeSuccess}
-                    onSwipeDown={onSwipeDown}
-                    isActive
-                    onTap={onTap}
-                    imageWidth={dimensions?.imageWidth}
-                    imageHeight={dimensions?.imageHeight}
-                    imageScale={dimensions?.imageScale}
-                    canvasHeight={canvasHeight}
-                    canvasWidth={canvasWidth}
-                    renderImage={({width, height, style}) => (
+                <View style={[StyleSheet.absoluteFill, {opacity: isImageLoading ? 0 : 1}]}>
+                    <ImageTransformer
+                        onSwipe={onSwipe}
+                        onSwipeSuccess={onSwipeSuccess}
+                        onSwipeDown={onSwipeDown}
+                        isActive
+                        onTap={onTap}
+                        imageWidth={dimensions?.imageWidth}
+                        imageHeight={dimensions?.imageHeight}
+                        imageScale={dimensions?.imageScale}
+                        canvasHeight={canvasHeight}
+                        canvasWidth={canvasWidth}
+                    >
                         <Image
                             source={{uri: item.url}}
-                            style={[style, {width, height}]}
+                            style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             onLoad={(evt) => {
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
                                 const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
@@ -655,14 +654,14 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                                 });
 
                                 if (imageWidth === 0 || imageHeight === 0) return;
-                                console.log('set iamge loading false');
                                 setIsImageLoading(false);
                             }}
                         />
-                    )}
-                />
+                    </ImageTransformer>
+                </View>
             )}
 
+            {/* Keep rendering the image without gestures as fallback while ImageTransformer is loading the image */}
             {(!isActive || isImageLoading) && (
                 <ImageWrapper>
                     <Image
@@ -686,6 +685,7 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
                 </ImageWrapper>
             )}
 
+            {/* Show activity indicator while ImageTransfomer is still loading the image. */}
             {isActive && isImageLoading && (
                 <ActivityIndicator
                     size="large"

From acb9ddfd2a8a601a5d3dddd94548cc37c5b4fc6a Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 15:29:33 +0200
Subject: [PATCH 042/133] fix: propTypes and extract itemExtractor from
 lightbox

---
 .../AttachmentCarouselView/index.native.js            | 11 ++++++++---
 .../AttachmentCarouselView/propTypes.js               |  9 ++++++++-
 src/components/AttachmentCarousel/Lightbox.js         |  7 ++-----
 3 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 395daca2f8ee..86f8356fa5c1 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,5 +1,6 @@
 import React, {useCallback, useMemo, useRef} from 'react';
 import {View} from 'react-native';
+import _ from 'underscore';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import Pager from '../Lightbox';
 import styles from '../../../styles/styles';
@@ -51,6 +52,8 @@ function AttachmentCarouselView(props) {
         [props, reversedPage, updatePage],
     );
 
+    const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
+
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
@@ -69,13 +72,15 @@ function AttachmentCarouselView(props) {
 
             {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && (
                 <Pager
-                    items={reversedAttachments}
+                    items={processedItems}
                     initialIndex={reversedPage}
-                    onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
+                    onPageSelected={({nativeEvent: {position: newPage}}) => {
+                        console.log('page updated');
+                        updatePage(newPage);
+                    }}
                     onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
                     onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
                     onSwipeDown={props.onClose}
-                    itemExtractor={({item}) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})}
                     containerWidth={props.carouselState.containerWidth}
                     containerHeight={props.carouselState.containerHeight}
                     ref={pagerRef}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index 14bb08c0eda8..6a7d401dbaa4 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -6,7 +6,14 @@ const propTypes = {
      */
     carouselState: PropTypes.shape({
         page: PropTypes.number.isRequired,
-        attachments: PropTypes.arrayOf(PropTypes.element).isRequired,
+        attachments: PropTypes.arrayOf(
+            PropTypes.shape({
+                // eslint-disable-next-line react/forbid-prop-types
+                file: PropTypes.object.isRequired,
+                isAuthTokenRequired: PropTypes.bool.isRequired,
+                source: PropTypes.string.isRequired,
+            }),
+        ).isRequired,
         shouldShowArrow: PropTypes.bool.isRequired,
         containerWidth: PropTypes.number.isRequired,
         containerHeight: PropTypes.number.isRequired,
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 77df60a4bded..14ac48a9d759 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -726,6 +726,7 @@ const noopWorklet = () => {
 const pagerPropTypes = {
     items: PropTypes.arrayOf(
         PropTypes.shape({
+            key: PropTypes.string,
             url: PropTypes.string,
         }),
     ).isRequired,
@@ -736,7 +737,6 @@ const pagerPropTypes = {
     onSwipeSuccess: PropTypes.func,
     onSwipeDown: PropTypes.func,
     onPinchGestureChange: PropTypes.func,
-    itemExtractor: PropTypes.func.isRequired,
     forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
     containerWidth: PropTypes.number.isRequired,
     containerHeight: PropTypes.number.isRequired,
@@ -762,7 +762,6 @@ function Pager({
     onSwipeSuccess = () => {},
     onSwipeDown = () => {},
     onPinchGestureChange = () => {},
-    itemExtractor,
     forwardedRef,
     containerWidth,
     containerHeight,
@@ -812,8 +811,6 @@ function Pager({
         scrollEnabled: shouldPagerScroll.value,
     }));
 
-    const processedItems = _.map(items, (item, index) => itemExtractor({item, index}));
-
     return (
         <GestureHandlerRootView style={styles.flex1}>
             <Context.Provider
@@ -833,7 +830,7 @@ function Pager({
                     style={styles.flex1}
                     initialPage={initialIndex}
                 >
-                    {_.map(processedItems, (item, index) => (
+                    {_.map(items, (item, index) => (
                         <View
                             key={item.key}
                             style={styles.flex1}

From 8e308aee99e2cd2b554479bddce4dfa2a653350f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 15:33:30 +0200
Subject: [PATCH 043/133] simplify

---
 .../AttachmentCarousel/AttachmentCarouselView/index.native.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 86f8356fa5c1..cd8bc9f25587 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -46,10 +46,8 @@ function AttachmentCarouselView(props) {
             const nextPageIndex = reversedPage + deltaSlide;
             updatePage(nextPageIndex);
             pagerRef.current.setPage(nextPageIndex);
-
-            props.autoHideArrow();
         },
-        [props, reversedPage, updatePage],
+        [reversedPage, updatePage],
     );
 
     const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);

From 1621dd0d3ae055d207ad86efce6ae2d2a1f95a25 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 17:02:27 +0200
Subject: [PATCH 044/133] move autoHideArrow logic to native

---
 .../AttachmentCarouselView/index.js           |  4 +-
 .../AttachmentCarouselView/index.native.js    | 29 ++++++++--
 .../AttachmentCarouselView/propTypes.js       |  2 +-
 src/components/AttachmentCarousel/index.js    | 56 +++----------------
 4 files changed, 34 insertions(+), 57 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 5a3e54fa88fd..9d9b9910dcf7 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -111,8 +111,8 @@ function AttachmentCarouselView(props) {
 
     return (
         <View
-            onMouseEnter={() => props.toggleArrowsVisibility(true)}
-            onMouseLeave={() => props.toggleArrowsVisibility(false)}
+            onMouseEnter={() => props.setArrowsVisibility(true)}
+            onMouseLeave={() => props.setArrowsVisibility(false)}
             style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}
         >
             <CarouselButtons
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index cd8bc9f25587..c15012096da7 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -6,6 +6,7 @@ import Pager from '../Lightbox';
 import styles from '../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
 import attachmentCarouselViewPropTypes from './propTypes';
+import CONST from '../../../CONST';
 
 function AttachmentCarouselView(props) {
     const pagerRef = useRef(null);
@@ -14,6 +15,8 @@ function AttachmentCarouselView(props) {
     // promotes the natural feeling of swiping left/right to go to the next/previous image
     const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
 
+    const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
+
     const reversePage = useCallback(
         (page) => Math.max(0, Math.min(props.carouselState.attachments.length - page - 1, props.carouselState.attachments.length)),
         [props.carouselState.attachments.length],
@@ -50,7 +53,23 @@ function AttachmentCarouselView(props) {
         [reversedPage, updatePage],
     );
 
-    const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
+    const autoHideArrowTimeout = useRef(null);
+
+    /**
+     * Cancels the automatic hiding of the arrows.
+     */
+    const cancelAutoHideArrow = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
+
+    /**
+     * On a touch screen device, automatically hide the arrows
+     * if there is no interaction for 3 seconds.
+     */
+    const autoHideArrow = useCallback(() => {
+        cancelAutoHideArrow();
+        autoHideArrowTimeout.current = setTimeout(() => {
+            props.setArrowsVisibility(false);
+        }, CONST.ARROW_HIDE_DELAY);
+    }, [cancelAutoHideArrow, props]);
 
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
@@ -58,11 +77,11 @@ function AttachmentCarouselView(props) {
                 carouselState={props.carouselState}
                 onBack={() => {
                     cycleThroughAttachments(-1);
-                    props.autoHideArrow();
+                    autoHideArrow();
                 }}
                 onForward={() => {
                     cycleThroughAttachments(1);
-                    props.autoHideArrow();
+                    autoHideArrow();
                 }}
                 autoHideArrow={props.autoHideArrow}
                 cancelAutoHideArrow={props.cancelAutoHideArrow}
@@ -76,8 +95,8 @@ function AttachmentCarouselView(props) {
                         console.log('page updated');
                         updatePage(newPage);
                     }}
-                    onTap={() => props.toggleArrowsVisibility(!props.carouselState.shouldShowArrow)}
-                    onPinchGestureChange={(isPinchGestureRunning) => props.toggleArrowsVisibility(!isPinchGestureRunning)}
+                    onTap={() => props.setArrowsVisibility(!props.carouselState.shouldShowArrow)}
+                    onPinchGestureChange={(isPinchGestureRunning) => props.setArrowsVisibility(!isPinchGestureRunning)}
                     onSwipeDown={props.onClose}
                     containerWidth={props.carouselState.containerWidth}
                     containerHeight={props.carouselState.containerHeight}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index 6a7d401dbaa4..fda059acaf1c 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -28,7 +28,7 @@ const propTypes = {
     /**
      * A callback for toggling the visibility of the arrows
      */
-    toggleArrowsVisibility: PropTypes.func.isRequired,
+    setArrowsVisibility: PropTypes.func.isRequired,
 
     /**
      * Trigger "auto-hiding" of the arrow buttons in the carousel
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 94fda0478ec6..9cc2306b0f1b 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -49,63 +49,23 @@ class AttachmentCarousel extends React.Component {
 
         this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
 
-        this.autoHideArrow = this.autoHideArrow.bind(this);
-        this.cancelAutoHideArrow = this.cancelAutoHideArrow.bind(this);
         this.updatePage = this.updatePage.bind(this);
-        this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
+        this.setArrowsVisibility = this.setArrowsVisibility.bind(this);
         this.createInitialState = this.createInitialState.bind(this);
 
         this.state = this.createInitialState();
     }
 
-    componentDidMount() {
-        this.autoHideArrow();
-    }
-
-    /**
-     * On a touch screen device, automatically hide the arrows
-     * if there is no interaction for 3 seconds.
-     */
-    autoHideArrow() {
-        if (!this.canUseTouchScreen) {
-            return;
-        }
-        this.cancelAutoHideArrow();
-        this.autoHideArrowTimeout = setTimeout(() => {
-            this.toggleArrowsVisibility(false);
-        }, CONST.ARROW_HIDE_DELAY);
-    }
-
-    /**
-     * Cancels the automatic hiding of the arrows.
-     */
-    cancelAutoHideArrow() {
-        clearTimeout(this.autoHideArrowTimeout);
-    }
-
     /**
      * Toggles the visibility of the arrows
      * @param {Boolean} shouldShowArrow
      * @param {Boolean} isGestureInUse
      */
-    toggleArrowsVisibility(shouldShowArrow, isGestureInUse = false) {
-        // Don't toggle arrows in a zoomed state
-        if (isGestureInUse) {
-            return;
-        }
-        this.setState(
-            (current) => {
-                const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-                return {shouldShowArrow: newShouldShowArrow};
-            },
-            () => {
-                if (this.state.shouldShowArrow) {
-                    this.autoHideArrow();
-                } else {
-                    this.cancelAutoHideArrow();
-                }
-            },
-        );
+    setArrowsVisibility(shouldShowArrow) {
+        this.setState((current) => {
+            const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
+            return {shouldShowArrow: newShouldShowArrow};
+        });
     }
 
     /**
@@ -192,9 +152,7 @@ class AttachmentCarousel extends React.Component {
                 <AttachmentCarouselView
                     carouselState={this.state}
                     updatePage={this.updatePage}
-                    toggleArrowsVisibility={this.toggleArrowsVisibility}
-                    autoHideArrow={this.autoHideArrow}
-                    cancelAutoHideArrow={this.cancelAutoHideArrow}
+                    setArrowsVisibility={this.setArrowsVisibility}
                     onClose={this.props.onClose}
                 />
             </View>

From 00136796ef73426cec074b77fc13c0f6f2b2a574 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 17:06:40 +0200
Subject: [PATCH 045/133] move initial arrow showing logic to native
 implementation

---
 .../AttachmentCarouselView/index.native.js                   | 4 +++-
 src/components/AttachmentCarousel/index.js                   | 5 +----
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index c15012096da7..e17db4f6ef24 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,4 +1,4 @@
-import React, {useCallback, useMemo, useRef} from 'react';
+import React, {useCallback, useMemo, useRef, useEffect} from 'react';
 import {View} from 'react-native';
 import _ from 'underscore';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
@@ -71,6 +71,8 @@ function AttachmentCarouselView(props) {
         }, CONST.ARROW_HIDE_DELAY);
     }, [cancelAutoHideArrow, props]);
 
+    useEffect(() => props.setArrowsVisibility(true), [props]);
+
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 9cc2306b0f1b..d142ceda4899 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -6,7 +6,6 @@ import _ from 'underscore';
 import {Parser as HtmlParser} from 'htmlparser2';
 import styles from '../../styles/styles';
 import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
-import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
 import CONST from '../../CONST';
 import ONYXKEYS from '../../ONYXKEYS';
 import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
@@ -47,8 +46,6 @@ class AttachmentCarousel extends React.Component {
     constructor(props) {
         super(props);
 
-        this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
-
         this.updatePage = this.updatePage.bind(this);
         this.setArrowsVisibility = this.setArrowsVisibility.bind(this);
         this.createInitialState = this.createInitialState.bind(this);
@@ -113,7 +110,7 @@ class AttachmentCarousel extends React.Component {
         return {
             page,
             attachments,
-            shouldShowArrow: this.canUseTouchScreen,
+            shouldShowArrow: false,
             containerWidth: 0,
             containerHeight: 0,
             activeSource: null,

From 567c5f1d58c5a8ebbf5726f0b025cdbaed21cbc6 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 17:41:31 +0200
Subject: [PATCH 046/133] simplify Lightbox and add propTypes to
 ImageTransformer

---
 src/components/AttachmentCarousel/Lightbox.js | 162 +++++++++---------
 1 file changed, 81 insertions(+), 81 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 14ac48a9d759..bb123206d33a 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -53,16 +53,20 @@ function ImageWrapper({children}) {
     );
 }
 
-// eslint-disable-next-line react/prop-types
-function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeight = 0, imageScale = 1, isActive, onSwipe, onSwipeSuccess, onTap, children}) {
-    const {pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
+const imageTransformerPropTypes = {
+    imageWidth: PropTypes.number.isRequired,
+    imageHeight: PropTypes.number.isRequired,
+    imageScale: PropTypes.number.isRequired,
+    scaledImageWidth: PropTypes.number.isRequired,
+    scaledImageHeight: PropTypes.number.isRequired,
+    isActive: PropTypes.bool.isRequired,
+    children: PropTypes.node.isRequired,
+};
 
-    const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight});
+function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scaledImageWidth = 0, scaledImageHeight = 0, isActive, children}) {
+    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
-    const targetDimensions = {
-        width: useSharedValue(0),
-        height: useSharedValue(0),
-    };
+    const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight});
 
     const canvas = {
         width: useSharedValue(canvasWidth),
@@ -90,10 +94,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
         if (imageWidth === 0 || imageHeight === 0) return;
 
         setImageDimensions({width: imageWidth, height: imageHeight});
-
-        targetDimensions.width.value = imageWidth;
-        targetDimensions.height.value = imageHeight;
-    }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth, targetDimensions.height, targetDimensions.width]);
+    }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth]);
 
     // used for pan gesture
     const translateY = useSharedValue(0);
@@ -120,17 +121,12 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
     const scaleOffset = useSharedValue(1);
 
     // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvas.height.value < targetDimensions.height.value * totalScale.value);
+    const canPanVertically = useDerivedValue(() => canvas.height.value < imageDimensions.height * totalScale.value, [imageDimensions.height]);
 
     // calculates bounds of the scaled image
     // can we pan left/right/up/down
     // can be used to limit gesture or implementing tension effect
     const getBounds = useWorkletCallback(() => {
-        const finalScale = zoomScale.value + canvasFitScale.value - 1;
-
-        const scaledImageWidth = targetDimensions.width.value * finalScale;
-        const scaledImageHeight = targetDimensions.height.value * finalScale;
-
         const rightBoundary = Math.abs(canvas.width.value - scaledImageWidth) / 2;
 
         let topBoundary = 0;
@@ -230,57 +226,60 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
         cancelAnimation(offsetY);
     });
 
-    const zoomToCoordinates = useWorkletCallback((x, y) => {
-        'worklet';
-
-        stopAnimation();
-
-        const usableImage = {
-            x: targetDimensions.width.value * canvasFitScale.value,
-            y: targetDimensions.height.value * canvasFitScale.value,
-        };
-
-        const targetImageSize = {
-            x: usableImage.x * DOUBLE_TAP_SCALE,
-            y: usableImage.y * DOUBLE_TAP_SCALE,
-        };
-
-        const CENTER = {
-            x: canvas.width.value / 2,
-            y: canvas.height.value / 2,
-        };
-
-        const imageCenter = {
-            x: usableImage.x / 2,
-            y: usableImage.y / 2,
-        };
-
-        const focal = {x, y};
-
-        const currentOrigin = {
-            x: (targetImageSize.x / 2 - CENTER.x) * -1,
-            y: (targetImageSize.y / 2 - CENTER.y) * -1,
-        };
-
-        const koef = {
-            x: (1 / imageCenter.x) * focal.x - 1,
-            y: (1 / imageCenter.y) * focal.y - 1,
-        };
+    const zoomToCoordinates = useWorkletCallback(
+        (x, y) => {
+            'worklet';
 
-        const target = {
-            x: currentOrigin.x * koef.x,
-            y: currentOrigin.y * koef.y,
-        };
+            stopAnimation();
 
-        if (targetImageSize.y < canvas.height.value) {
-            target.y = 0;
-        }
+            const usableImage = {
+                x: imageDimensions.width * canvasFitScale.value,
+                y: imageDimensions.height * canvasFitScale.value,
+            };
+
+            const targetImageSize = {
+                x: usableImage.x * DOUBLE_TAP_SCALE,
+                y: usableImage.y * DOUBLE_TAP_SCALE,
+            };
+
+            const CENTER = {
+                x: canvas.width.value / 2,
+                y: canvas.height.value / 2,
+            };
+
+            const imageCenter = {
+                x: usableImage.x / 2,
+                y: usableImage.y / 2,
+            };
+
+            const focal = {x, y};
+
+            const currentOrigin = {
+                x: (targetImageSize.x / 2 - CENTER.x) * -1,
+                y: (targetImageSize.y / 2 - CENTER.y) * -1,
+            };
+
+            const koef = {
+                x: (1 / imageCenter.x) * focal.x - 1,
+                y: (1 / imageCenter.y) * focal.y - 1,
+            };
+
+            const target = {
+                x: currentOrigin.x * koef.x,
+                y: currentOrigin.y * koef.y,
+            };
+
+            if (targetImageSize.y < canvas.height.value) {
+                target.y = 0;
+            }
 
-        offsetX.value = withSpring(target.x, SPRING_CONFIG);
-        offsetY.value = withSpring(target.y, SPRING_CONFIG);
-        zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
-        scaleOffset.value = DOUBLE_TAP_SCALE;
-    });
+            offsetX.value = withSpring(target.x, SPRING_CONFIG);
+            offsetY.value = withSpring(target.y, SPRING_CONFIG);
+            zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
+            scaleOffset.value = DOUBLE_TAP_SCALE;
+        },
+        [imageDimensions],
+    );
 
     const reset = useWorkletCallback((animated) => {
         scaleOffset.value = 1;
@@ -411,7 +410,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
                     };
 
                     offsetY.value = withSpring(
-                        maybeInvert(targetDimensions.height.value * 2),
+                        maybeInvert(imageDimensions.height * 2),
                         {
                             stiffness: 50,
                             damping: 30,
@@ -464,7 +463,9 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+            const newZoomScale = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+
+            zoomScale.value = newZoomScale;
 
             if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
                 gestureScale.value = evt.scale;
@@ -593,6 +594,7 @@ function ImageTransformer({canvasWidth, canvasHeight, imageWidth = 0, imageHeigh
         </View>
     );
 }
+ImageTransformer.propTypes = imageTransformerPropTypes;
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = canvasWidth / imageWidth;
@@ -610,7 +612,9 @@ const pagePropTypes = {
 };
 
 // eslint-disable-next-line react/prop-types
-function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth, canvasHeight, onTap}) {
+function Page({isActive, item}) {
+    const {canvasWidth, canvasHeight} = useContext(Context);
+
     const dimensions = cachedDimensions.get(item.url);
 
     const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0;
@@ -627,16 +631,12 @@ function Page({isActive, item, onSwipe, onSwipeSuccess, onSwipeDown, canvasWidth
             {isActive && (
                 <View style={[StyleSheet.absoluteFill, {opacity: isImageLoading ? 0 : 1}]}>
                     <ImageTransformer
-                        onSwipe={onSwipe}
-                        onSwipeSuccess={onSwipeSuccess}
-                        onSwipeDown={onSwipeDown}
                         isActive
-                        onTap={onTap}
                         imageWidth={dimensions?.imageWidth}
                         imageHeight={dimensions?.imageHeight}
+                        scaledImageWidth={dimensions?.scaledImageWidth}
+                        scaledImageHeight={dimensions?.scaledImageHeight}
                         imageScale={dimensions?.imageScale}
-                        canvasHeight={canvasHeight}
-                        canvasWidth={canvasWidth}
                     >
                         <Image
                             source={{uri: item.url}}
@@ -815,10 +815,16 @@ function Pager({
         <GestureHandlerRootView style={styles.flex1}>
             <Context.Provider
                 value={{
+                    canvasWidth: containerWidth,
+                    canvasHeight: containerHeight,
                     isScrolling,
                     pagerRef,
                     shouldPagerScroll,
                     onPinchGestureChange,
+                    onTap,
+                    onSwipe,
+                    onSwipeSuccess,
+                    onSwipeDown,
                 }}
             >
                 <AnimatedPagerView
@@ -836,14 +842,8 @@ function Pager({
                             style={styles.flex1}
                         >
                             <Page
-                                onTap={onTap}
-                                onSwipe={onSwipe}
-                                onSwipeSuccess={onSwipeSuccess}
-                                onSwipeDown={onSwipeDown}
                                 isActive={index === activePage}
                                 item={item}
-                                canvasHeight={containerHeight}
-                                canvasWidth={containerWidth}
                             />
                         </View>
                     ))}

From 80e5bee8042fdf5680e05d92854a5d57640fcc7e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 17:41:58 +0200
Subject: [PATCH 047/133] extract more logic from AttachmentCarousel and
 improve propTypes and memoizing

---
 .../AttachmentCarouselView/index.js           | 48 ++++++++-----------
 .../AttachmentCarouselView/index.native.js    | 44 ++++++++---------
 .../AttachmentCarouselView/propTypes.js       | 10 ----
 .../AttachmentCarousel/CarouselButtons.js     | 10 +++-
 4 files changed, 49 insertions(+), 63 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 9d9b9910dcf7..7aacf9c608b7 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -19,7 +19,7 @@ const propTypes = {
     ...windowDimensionsPropTypes,
 };
 
-function AttachmentCarouselView(props) {
+function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, ...props}) {
     const scrollRef = useRef(null);
 
     /**
@@ -30,11 +30,11 @@ function AttachmentCarouselView(props) {
      */
     const getItemLayout = useCallback(
         (_data, index) => ({
-            length: props.carouselState.containerWidth,
-            offset: props.carouselState.containerWidth * index,
+            length: carouselState.containerWidth,
+            offset: carouselState.containerWidth * index,
             index,
         }),
-        [props.carouselState.containerWidth],
+        [carouselState.containerWidth],
     );
 
     /**
@@ -43,8 +43,8 @@ function AttachmentCarouselView(props) {
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextIndex = props.carouselState.page - deltaSlide;
-            const nextItem = props.carouselState.attachments[nextIndex];
+            const nextIndex = carouselState.page - deltaSlide;
+            const nextItem = carouselState.attachments[nextIndex];
 
             if (!nextItem || !scrollRef.current) {
                 return;
@@ -54,7 +54,7 @@ function AttachmentCarouselView(props) {
             // so we only enable it for mobile
             scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
         },
-        [props.carouselState.attachments, props.carouselState.page],
+        [carouselState.attachments, carouselState.page],
     );
 
     /**
@@ -88,13 +88,13 @@ function AttachmentCarouselView(props) {
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
-                isFocused={props.carouselState.activeSource === item.source}
+                isFocused={carouselState.activeSource === item.source}
                 source={item.source}
                 file={item.file}
                 isAuthTokenRequired={item.isAuthTokenRequired}
             />
         ),
-        [props.carouselState.activeSource],
+        [carouselState.activeSource],
     );
 
     const handleViewableItemsChange = useCallback(
@@ -102,34 +102,26 @@ function AttachmentCarouselView(props) {
             // Since we can have only one item in view at a time, we can use the first item in the array
             // to get the index of the current page
             const entry = _.first([...viewableItems]);
-            props.updatePage({item: entry.item, index: entry.index});
+            updatePage({item: entry.item, index: entry.index});
         },
-        [props],
+        [updatePage],
     );
 
     const viewabilityConfigCallbackPairs = useRef([{viewabilityConfig: VIEWABILITY_CONFIG, onViewableItemsChanged: handleViewableItemsChange}]);
 
     return (
         <View
-            onMouseEnter={() => props.setArrowsVisibility(true)}
-            onMouseLeave={() => props.setArrowsVisibility(false)}
+            onMouseEnter={() => setArrowsVisibility(true)}
+            onMouseLeave={() => setArrowsVisibility(false)}
             style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}
         >
             <CarouselButtons
-                carouselState={props.carouselState}
-                onBack={() => {
-                    cycleThroughAttachments(-1);
-                    props.autoHideArrow();
-                }}
-                onForward={() => {
-                    cycleThroughAttachments(1);
-                    props.autoHideArrow();
-                }}
-                autoHideArrow={props.autoHideArrow}
-                cancelAutoHideArrow={props.cancelAutoHideArrow}
+                carouselState={carouselState}
+                onBack={() => cycleThroughAttachments(-1)}
+                onForward={() => cycleThroughAttachments(1)}
             />
 
-            {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && (
+            {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && (
                 <FlatList
                     listKey="AttachmentCarousel"
                     horizontal
@@ -139,14 +131,14 @@ function AttachmentCarouselView(props) {
                     disableIntervalMomentum // Scroll only one image at a time no matter how fast the user swipes
                     pagingEnabled
                     snapToAlignment="start"
-                    snapToInterval={props.carouselState.containerWidth}
+                    snapToInterval={carouselState.containerWidth}
                     scrollEnabled={false}
                     ref={scrollRef}
-                    initialScrollIndex={props.carouselState.page}
+                    initialScrollIndex={carouselState.page}
                     initialNumToRender={3}
                     windowSize={5}
                     maxToRenderPerBatch={3}
-                    data={props.carouselState.attachments}
+                    data={carouselState.attachments}
                     CellRendererComponent={renderCell}
                     renderItem={renderItem}
                     getItemLayout={getItemLayout}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index e17db4f6ef24..df37cfd9eeb3 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -8,36 +8,33 @@ import CarouselButtons from '../CarouselButtons';
 import attachmentCarouselViewPropTypes from './propTypes';
 import CONST from '../../../CONST';
 
-function AttachmentCarouselView(props) {
+function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, onClose}) {
     const pagerRef = useRef(null);
 
     // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
     // promotes the natural feeling of swiping left/right to go to the next/previous image
-    const reversedAttachments = useMemo(() => props.carouselState.attachments.reverse(), [props.carouselState.attachments]);
+    const reversedAttachments = useMemo(() => carouselState.attachments.reverse(), [carouselState.attachments]);
 
     const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
 
-    const reversePage = useCallback(
-        (page) => Math.max(0, Math.min(props.carouselState.attachments.length - page - 1, props.carouselState.attachments.length)),
-        [props.carouselState.attachments.length],
-    );
+    const reversePage = useCallback((page) => Math.max(0, Math.min(carouselState.attachments.length - page - 1, carouselState.attachments.length)), [carouselState.attachments.length]);
 
-    const reversedPage = useMemo(() => reversePage(props.carouselState.page), [props.carouselState.page, reversePage]);
+    const reversedPage = useMemo(() => reversePage(carouselState.page), [carouselState.page, reversePage]);
 
     /**
      * Update carousel page based on next page index
      * @param {Number} newPageIndex
      */
-    const updatePage = useCallback(
+    const updatePageInternal = useCallback(
         (newPageIndex) => {
             const nextItem = reversedAttachments[newPageIndex];
             if (!nextItem) {
                 return;
             }
 
-            props.updatePage({item: nextItem, index: reversePage(newPageIndex)});
+            updatePage({item: nextItem, index: reversePage(newPageIndex)});
         },
-        [props, reversePage, reversedAttachments],
+        [reversePage, reversedAttachments, updatePage],
     );
 
     /**
@@ -67,16 +64,17 @@ function AttachmentCarouselView(props) {
     const autoHideArrow = useCallback(() => {
         cancelAutoHideArrow();
         autoHideArrowTimeout.current = setTimeout(() => {
-            props.setArrowsVisibility(false);
+            setArrowsVisibility(false);
         }, CONST.ARROW_HIDE_DELAY);
-    }, [cancelAutoHideArrow, props]);
+    }, [cancelAutoHideArrow, setArrowsVisibility]);
 
-    useEffect(() => props.setArrowsVisibility(true), [props]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    useEffect(() => setArrowsVisibility(true), []);
 
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
-                carouselState={props.carouselState}
+                carouselState={carouselState}
                 onBack={() => {
                     cycleThroughAttachments(-1);
                     autoHideArrow();
@@ -85,23 +83,23 @@ function AttachmentCarouselView(props) {
                     cycleThroughAttachments(1);
                     autoHideArrow();
                 }}
-                autoHideArrow={props.autoHideArrow}
-                cancelAutoHideArrow={props.cancelAutoHideArrow}
+                autoHideArrow={autoHideArrow}
+                cancelAutoHideArrow={cancelAutoHideArrow}
             />
 
-            {props.carouselState.containerWidth > 0 && props.carouselState.containerHeight > 0 && (
+            {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && (
                 <Pager
                     items={processedItems}
                     initialIndex={reversedPage}
                     onPageSelected={({nativeEvent: {position: newPage}}) => {
                         console.log('page updated');
-                        updatePage(newPage);
+                        updatePageInternal(newPage);
                     }}
-                    onTap={() => props.setArrowsVisibility(!props.carouselState.shouldShowArrow)}
-                    onPinchGestureChange={(isPinchGestureRunning) => props.setArrowsVisibility(!isPinchGestureRunning)}
-                    onSwipeDown={props.onClose}
-                    containerWidth={props.carouselState.containerWidth}
-                    containerHeight={props.carouselState.containerHeight}
+                    onTap={() => setArrowsVisibility()}
+                    onPinchGestureChange={(isPinchGestureRunning) => setArrowsVisibility(!isPinchGestureRunning)}
+                    onSwipeDown={onClose}
+                    containerWidth={carouselState.containerWidth}
+                    containerHeight={carouselState.containerHeight}
                     ref={pagerRef}
                 />
             )}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index fda059acaf1c..7d3b50f446a5 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -29,16 +29,6 @@ const propTypes = {
      * A callback for toggling the visibility of the arrows
      */
     setArrowsVisibility: PropTypes.func.isRequired,
-
-    /**
-     * Trigger "auto-hiding" of the arrow buttons in the carousel
-     */
-    autoHideArrow: PropTypes.func.isRequired,
-
-    /**
-     * Cancel "auto-hiding" of the arrow buttons in the carousel
-     */
-    cancelAutoHideArrow: PropTypes.func.isRequired,
 };
 
 export default propTypes;
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js
index 10eabb502582..6cefefd37a53 100644
--- a/src/components/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/AttachmentCarousel/CarouselButtons.js
@@ -19,12 +19,17 @@ const propTypes = {
     /** Callback to go one page forward */
     onForward: PropTypes.func.isRequired,
 
-    autoHideArrow: PropTypes.func.isRequired,
-    cancelAutoHideArrow: PropTypes.func.isRequired,
+    autoHideArrow: PropTypes.func,
+    cancelAutoHideArrow: PropTypes.func,
 
     ...withLocalizePropTypes,
 };
 
+const defaultProps = {
+    autoHideArrow: () => {},
+    cancelAutoHideArrow: () => {},
+};
+
 function CarouselButtons(props) {
     const isForwardDisabled = props.carouselState.page === 0;
     const isBackDisabled = props.carouselState.page === _.size(props.carouselState.attachments) - 1;
@@ -68,5 +73,6 @@ function CarouselButtons(props) {
 }
 
 CarouselButtons.propTypes = propTypes;
+CarouselButtons.defaultProps = defaultProps;
 
 export default withLocalize(CarouselButtons);

From 05bc9791cd7cda2b43c66475c23f49c73aa2651f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 17:43:28 +0200
Subject: [PATCH 048/133] add more propTypes

---
 src/components/AttachmentCarousel/Lightbox.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index bb123206d33a..d6944d91d355 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -41,7 +41,10 @@ function clamp(value, lowerBound, upperBound) {
     return Math.min(Math.max(lowerBound, value), upperBound);
 }
 
-// eslint-disable-next-line react/prop-types
+const imageWrapperPropTypes = {
+    children: PropTypes.node.isRequired,
+};
+
 function ImageWrapper({children}) {
     return (
         <Animated.View
@@ -52,6 +55,7 @@ function ImageWrapper({children}) {
         </Animated.View>
     );
 }
+ImageWrapper.propTypes = imageWrapperPropTypes;
 
 const imageTransformerPropTypes = {
     imageWidth: PropTypes.number.isRequired,
@@ -606,12 +610,12 @@ function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight})
 const cachedDimensions = new Map();
 
 const pagePropTypes = {
+    isActive: PropTypes.bool.isRequired,
     item: PropTypes.shape({
         url: PropTypes.string,
     }).isRequired,
 };
 
-// eslint-disable-next-line react/prop-types
 function Page({isActive, item}) {
     const {canvasWidth, canvasHeight} = useContext(Context);
 

From a530909c07934509b7cfb0980120eaeaf329cddd Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 18:55:36 +0200
Subject: [PATCH 049/133] further simplify Lightbox

---
 src/components/AttachmentCarousel/Lightbox.js | 40 +++++++++----------
 1 file changed, 19 insertions(+), 21 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index d6944d91d355..a405439a5579 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -58,19 +58,23 @@ function ImageWrapper({children}) {
 ImageWrapper.propTypes = imageWrapperPropTypes;
 
 const imageTransformerPropTypes = {
-    imageWidth: PropTypes.number.isRequired,
-    imageHeight: PropTypes.number.isRequired,
-    imageScale: PropTypes.number.isRequired,
-    scaledImageWidth: PropTypes.number.isRequired,
-    scaledImageHeight: PropTypes.number.isRequired,
+    imageHeight: PropTypes.number,
+    imageScale: PropTypes.number,
+    scaledImageWidth: PropTypes.number,
+    scaledImageHeight: PropTypes.number,
     isActive: PropTypes.bool.isRequired,
     children: PropTypes.node.isRequired,
 };
 
-function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scaledImageWidth = 0, scaledImageHeight = 0, isActive, children}) {
-    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
+const imageTransformerDefaultProps = {
+    imageHeight: 0,
+    imageScale: 1,
+    scaledImageWidth: 0,
+    scaledImageHeight: 0,
+};
 
-    const [imageDimensions, setImageDimensions] = useState({width: imageWidth, height: imageHeight});
+function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
+    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
     const canvas = {
         width: useSharedValue(canvasWidth),
@@ -94,12 +98,6 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
         })();
     }, [canvas.height, canvas.width, canvasFitScale, canvasHeight, canvasWidth, imageScale]);
 
-    useEffect(() => {
-        if (imageWidth === 0 || imageHeight === 0) return;
-
-        setImageDimensions({width: imageWidth, height: imageHeight});
-    }, [imageDimensions.height, imageDimensions.width, imageHeight, imageWidth]);
-
     // used for pan gesture
     const translateY = useSharedValue(0);
     const translateX = useSharedValue(0);
@@ -125,7 +123,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
     const scaleOffset = useSharedValue(1);
 
     // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvas.height.value < imageDimensions.height * totalScale.value, [imageDimensions.height]);
+    const canPanVertically = useDerivedValue(() => canvas.height.value < imageHeight * totalScale.value, []);
 
     // calculates bounds of the scaled image
     // can we pan left/right/up/down
@@ -159,7 +157,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
             canPanLeft: target.x < maxVector.x,
             canPanRight: target.x > minVector.x,
         };
-    }, [imageDimensions]);
+    });
 
     const afterGesture = useWorkletCallback(() => {
         const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
@@ -237,8 +235,8 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
             stopAnimation();
 
             const usableImage = {
-                x: imageDimensions.width * canvasFitScale.value,
-                y: imageDimensions.height * canvasFitScale.value,
+                x: scaledImageWidth,
+                y: scaledImageHeight,
             };
 
             const targetImageSize = {
@@ -282,7 +280,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
             zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
             scaleOffset.value = DOUBLE_TAP_SCALE;
         },
-        [imageDimensions],
+        [scaledImageWidth, scaledImageHeight],
     );
 
     const reset = useWorkletCallback((animated) => {
@@ -414,7 +412,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
                     };
 
                     offsetY.value = withSpring(
-                        maybeInvert(imageDimensions.height * 2),
+                        maybeInvert(imageHeight * 2),
                         {
                             stiffness: 50,
                             damping: 30,
@@ -599,6 +597,7 @@ function ImageTransformer({imageWidth = 0, imageHeight = 0, imageScale = 1, scal
     );
 }
 ImageTransformer.propTypes = imageTransformerPropTypes;
+ImageTransformer.defaultProps = imageTransformerDefaultProps;
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = canvasWidth / imageWidth;
@@ -636,7 +635,6 @@ function Page({isActive, item}) {
                 <View style={[StyleSheet.absoluteFill, {opacity: isImageLoading ? 0 : 1}]}>
                     <ImageTransformer
                         isActive
-                        imageWidth={dimensions?.imageWidth}
                         imageHeight={dimensions?.imageHeight}
                         scaledImageWidth={dimensions?.scaledImageWidth}
                         scaledImageHeight={dimensions?.scaledImageHeight}

From b87f796a049f1d88b253c65bda955225a4390ac3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 20:10:32 +0200
Subject: [PATCH 050/133] omit updating the image dimensions if already set

---
 src/components/AttachmentCarousel/Lightbox.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index a405439a5579..c14249326720 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -649,7 +649,11 @@ function Page({isActive, item}) {
 
                                 const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
 
+                                // Don't update the dimensions if they are already set
+                                if (dimensions?.imageWidth === imageWidth && dimensions?.imageHeight === imageHeight && dimensions?.imageScale === imageScale) return;
+
                                 cachedDimensions.set(item.url, {
+                                    ...dimensions,
                                     imageWidth,
                                     imageHeight,
                                     imageScale,
@@ -677,7 +681,11 @@ function Page({isActive, item}) {
                             const scaledImageWidth = imageWidth * scale;
                             const scaledImageHeight = imageHeight * scale;
 
+                            // Don't update the dimensions if they are already set
+                            if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return;
+
                             cachedDimensions.set(item.url, {
+                                ...dimensions,
                                 scaledImageWidth,
                                 scaledImageHeight,
                             });

From d23c2ccef8898878cd3e5c34e366690f8f900429 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 20:42:40 +0200
Subject: [PATCH 051/133] fix: further simplify Lightbox

---
 src/components/AttachmentCarousel/Lightbox.js | 58 ++++++++-----------
 1 file changed, 24 insertions(+), 34 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index c14249326720..edeb6873502d 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -58,6 +58,7 @@ function ImageWrapper({children}) {
 ImageWrapper.propTypes = imageWrapperPropTypes;
 
 const imageTransformerPropTypes = {
+    imageWidth: PropTypes.number,
     imageHeight: PropTypes.number,
     imageScale: PropTypes.number,
     scaledImageWidth: PropTypes.number,
@@ -67,36 +68,23 @@ const imageTransformerPropTypes = {
 };
 
 const imageTransformerDefaultProps = {
+    imageWidth: 0,
     imageHeight: 0,
     imageScale: 1,
     scaledImageWidth: 0,
     scaledImageHeight: 0,
 };
 
-function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
+function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
     const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
 
-    const canvas = {
-        width: useSharedValue(canvasWidth),
-        height: useSharedValue(canvasHeight),
-    };
-
-    const canvasFitScale = useSharedValue(imageScale);
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
     // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => zoomScale.value + canvasFitScale.value - 1);
-
-    // Update shared values on UI thread when canvas dimensions change
-    useEffect(() => {
-        runOnUI(() => {
-            'worklet';
+    const totalScale = useDerivedValue(() => zoomScale.value + imageScale - 1, [imageScale]);
 
-            canvasFitScale.value = imageScale;
-            canvas.width.value = canvasWidth;
-            canvas.height.value = canvasHeight;
-        })();
-    }, [canvas.height, canvas.width, canvasFitScale, canvasHeight, canvasWidth, imageScale]);
+    const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
+    const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
 
     // used for pan gesture
     const translateY = useSharedValue(0);
@@ -123,18 +111,18 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
     const scaleOffset = useSharedValue(1);
 
     // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvas.height.value < imageHeight * totalScale.value, []);
+    const canPanVertically = useDerivedValue(() => canvasHeight < zoomScaledImageHeight.value, [canvasHeight]);
 
     // calculates bounds of the scaled image
     // can we pan left/right/up/down
     // can be used to limit gesture or implementing tension effect
     const getBounds = useWorkletCallback(() => {
-        const rightBoundary = Math.abs(canvas.width.value - scaledImageWidth) / 2;
+        const rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2;
 
         let topBoundary = 0;
 
-        if (canvas.height.value < scaledImageHeight) {
-            topBoundary = Math.abs(scaledImageHeight - canvas.height.value) / 2;
+        if (canvasHeight < zoomScaledImageHeight.value) {
+            topBoundary = Math.abs(zoomScaledImageHeight.value - canvasHeight) / 2;
         }
 
         const maxVector = {x: rightBoundary, y: topBoundary};
@@ -157,7 +145,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
             canPanLeft: target.x < maxVector.x,
             canPanRight: target.x > minVector.x,
         };
-    });
+    }, [canvasWidth, canvasHeight]);
 
     const afterGesture = useWorkletCallback(() => {
         const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
@@ -245,8 +233,8 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
             };
 
             const CENTER = {
-                x: canvas.width.value / 2,
-                y: canvas.height.value / 2,
+                x: canvasWidth / 2,
+                y: canvasHeight / 2,
             };
 
             const imageCenter = {
@@ -271,7 +259,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
                 y: currentOrigin.y * koef.y,
             };
 
-            if (targetImageSize.y < canvas.height.value) {
+            if (targetImageSize.y < canvasHeight) {
                 target.y = 0;
             }
 
@@ -280,7 +268,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
             zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
             scaleOffset.value = DOUBLE_TAP_SCALE;
         },
-        [scaledImageWidth, scaledImageHeight],
+        [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight],
     );
 
     const reset = useWorkletCallback((animated) => {
@@ -437,10 +425,13 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
         })
         .withRef(panGestureRef);
 
-    const getAdjustedFocal = useWorkletCallback((focalX, focalY) => ({
-        x: focalX - (canvas.width.value / 2 + offsetX.value),
-        y: focalY - (canvas.height.value / 2 + offsetY.value),
-    }));
+    const getAdjustedFocal = useWorkletCallback(
+        (focalX, focalY) => ({
+            x: focalX - (canvasWidth / 2 + offsetX.value),
+            y: focalY - (canvasHeight / 2 + offsetY.value),
+        }),
+        [canvasWidth, canvasHeight],
+    );
 
     // used to store event scale value when we limit scale
     const gestureScale = useSharedValue(1);
@@ -465,9 +456,7 @@ function ImageTransformer({imageHeight, imageScale, scaledImageWidth, scaledImag
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            const newZoomScale = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
-
-            zoomScale.value = newZoomScale;
+            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
 
             if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
                 gestureScale.value = evt.scale;
@@ -635,6 +624,7 @@ function Page({isActive, item}) {
                 <View style={[StyleSheet.absoluteFill, {opacity: isImageLoading ? 0 : 1}]}>
                     <ImageTransformer
                         isActive
+                        imageWidth={dimensions?.imageWidth}
                         imageHeight={dimensions?.imageHeight}
                         scaledImageWidth={dimensions?.scaledImageWidth}
                         scaledImageHeight={dimensions?.scaledImageHeight}

From b0046a1a244324bd45ea75086c16a9b9f9cfa264 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 20:54:29 +0200
Subject: [PATCH 052/133] fix: loading state never ending

---
 src/components/AttachmentCarousel/Lightbox.js | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index edeb6873502d..965cf8b55f7f 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -640,14 +640,14 @@ function Page({isActive, item}) {
                                 const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
 
                                 // Don't update the dimensions if they are already set
-                                if (dimensions?.imageWidth === imageWidth && dimensions?.imageHeight === imageHeight && dimensions?.imageScale === imageScale) return;
-
-                                cachedDimensions.set(item.url, {
-                                    ...dimensions,
-                                    imageWidth,
-                                    imageHeight,
-                                    imageScale,
-                                });
+                                if (dimensions?.imageWidth !== imageWidth || dimensions?.imageHeight !== imageHeight || dimensions?.imageScale !== imageScale) {
+                                    cachedDimensions.set(item.url, {
+                                        ...dimensions,
+                                        imageWidth,
+                                        imageHeight,
+                                        imageScale,
+                                    });
+                                }
 
                                 if (imageWidth === 0 || imageHeight === 0) return;
                                 setIsImageLoading(false);

From 7608579bae39054d425582eff4dc178aa0b5b2cd Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 21:48:57 +0200
Subject: [PATCH 053/133] improve state in AttachmentCarousel

---
 .../AttachmentCarouselView/index.js           | 68 +++++++++++++------
 .../AttachmentCarouselView/index.native.js    | 62 ++++++++---------
 .../AttachmentCarouselView/propTypes.js       | 45 ++++++------
 .../AttachmentCarousel/CarouselButtons.js     | 29 ++++----
 src/components/AttachmentCarousel/index.js    | 54 +++------------
 5 files changed, 130 insertions(+), 128 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 7aacf9c608b7..74e229db20b8 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -1,12 +1,12 @@
-import React, {useRef, useCallback} from 'react';
-import {View, FlatList, PixelRatio} from 'react-native';
+import React, {useRef, useCallback, useState} from 'react';
+import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
 import _ from 'underscore';
 import styles from '../../../styles/styles';
 import CarouselActions from '../CarouselActions';
 import AttachmentView from '../../AttachmentView';
 import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
 import CarouselButtons from '../CarouselButtons';
-import attachmentCarouselViewPropTypes from './propTypes';
+import {propTypes as attachmentCarouselViewPropTypes} from './propTypes';
 
 const VIEWABILITY_CONFIG = {
     // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -19,9 +19,37 @@ const propTypes = {
     ...windowDimensionsPropTypes,
 };
 
-function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, ...props}) {
+function AttachmentCarouselView({containerDimensions, attachments, initialPage, initialActiveSource, onNavigate, ...props}) {
     const scrollRef = useRef(null);
 
+    const [shouldShowArrows, setShouldShowArrows] = useState(false);
+    const [page, setPage] = useState(initialPage);
+    const [activeSource, setActiveSource] = useState(initialActiveSource);
+
+    /**
+     * Updates the page state when the user navigates between attachments
+     * @param {Object} item
+     * @param {number} index
+     */
+    const updatePage = useCallback(
+        ({item, index}) => {
+            Keyboard.dismiss();
+
+            if (!item) {
+                // eslint-disable-next-line react/no-unused-state
+                setActiveSource(null);
+                return;
+            }
+
+            const pageIndex = index;
+            onNavigate(item);
+            // eslint-disable-next-line react/no-unused-state
+            setPage(pageIndex);
+            setActiveSource(item.source);
+        },
+        [onNavigate],
+    );
+
     /**
      * Calculate items layout information to optimize scrolling performance
      * @param {*} data
@@ -30,11 +58,11 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
      */
     const getItemLayout = useCallback(
         (_data, index) => ({
-            length: carouselState.containerWidth,
-            offset: carouselState.containerWidth * index,
+            length: containerDimensions.width,
+            offset: containerDimensions.height * index,
             index,
         }),
-        [carouselState.containerWidth],
+        [containerDimensions.height, containerDimensions.width],
     );
 
     /**
@@ -43,8 +71,8 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextIndex = carouselState.page - deltaSlide;
-            const nextItem = carouselState.attachments[nextIndex];
+            const nextIndex = page - deltaSlide;
+            const nextItem = attachments[nextIndex];
 
             if (!nextItem || !scrollRef.current) {
                 return;
@@ -54,7 +82,7 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
             // so we only enable it for mobile
             scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
         },
-        [carouselState.attachments, carouselState.page],
+        [attachments, page],
     );
 
     /**
@@ -88,13 +116,13 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
-                isFocused={carouselState.activeSource === item.source}
+                isFocused={activeSource === item.source}
                 source={item.source}
                 file={item.file}
                 isAuthTokenRequired={item.isAuthTokenRequired}
             />
         ),
-        [carouselState.activeSource],
+        [activeSource],
     );
 
     const handleViewableItemsChange = useCallback(
@@ -111,17 +139,19 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
 
     return (
         <View
-            onMouseEnter={() => setArrowsVisibility(true)}
-            onMouseLeave={() => setArrowsVisibility(false)}
+            onMouseEnter={() => setShouldShowArrows(true)}
+            onMouseLeave={() => setShouldShowArrows(false)}
             style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}
         >
             <CarouselButtons
-                carouselState={carouselState}
+                shouldShowArrows={shouldShowArrows}
+                page={page}
+                attachments={attachments}
                 onBack={() => cycleThroughAttachments(-1)}
                 onForward={() => cycleThroughAttachments(1)}
             />
 
-            {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && (
+            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
                 <FlatList
                     listKey="AttachmentCarousel"
                     horizontal
@@ -131,14 +161,14 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
                     disableIntervalMomentum // Scroll only one image at a time no matter how fast the user swipes
                     pagingEnabled
                     snapToAlignment="start"
-                    snapToInterval={carouselState.containerWidth}
+                    snapToInterval={containerDimensions.width}
                     scrollEnabled={false}
                     ref={scrollRef}
-                    initialScrollIndex={carouselState.page}
+                    initialScrollIndex={page}
                     initialNumToRender={3}
                     windowSize={5}
                     maxToRenderPerBatch={3}
-                    data={carouselState.attachments}
+                    data={attachments}
                     CellRendererComponent={renderCell}
                     renderItem={renderItem}
                     getItemLayout={getItemLayout}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index df37cfd9eeb3..3610f477ce44 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,40 +1,42 @@
-import React, {useCallback, useMemo, useRef, useEffect} from 'react';
-import {View} from 'react-native';
+import React, {useCallback, useMemo, useRef, useState} from 'react';
+import {View, Keyboard} from 'react-native';
 import _ from 'underscore';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import Pager from '../Lightbox';
 import styles from '../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
-import attachmentCarouselViewPropTypes from './propTypes';
+import {propTypes as attachmentCarouselViewPropTypes} from './propTypes';
 import CONST from '../../../CONST';
 
-function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility, onClose}) {
+function AttachmentCarouselView({attachments, initialPage, containerDimensions, onClose, onNavigate}) {
     const pagerRef = useRef(null);
 
     // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
     // promotes the natural feeling of swiping left/right to go to the next/previous image
-    const reversedAttachments = useMemo(() => carouselState.attachments.reverse(), [carouselState.attachments]);
+    const reversedAttachments = useMemo(() => attachments.reverse(), [attachments]);
 
     const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
 
-    const reversePage = useCallback((page) => Math.max(0, Math.min(carouselState.attachments.length - page - 1, carouselState.attachments.length)), [carouselState.attachments.length]);
+    const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
 
-    const reversedPage = useMemo(() => reversePage(carouselState.page), [carouselState.page, reversePage]);
+    const [shouldShowArrows, setShouldShowArrows] = useState(true);
+    const [page, setPage] = useState(reversePage(initialPage));
 
     /**
-     * Update carousel page based on next page index
-     * @param {Number} newPageIndex
+     * Updates the page state when the user navigates between attachments
+     * @param {Object} item
+     * @param {number} index
      */
-    const updatePageInternal = useCallback(
+    const updatePage = useCallback(
         (newPageIndex) => {
-            const nextItem = reversedAttachments[newPageIndex];
-            if (!nextItem) {
-                return;
-            }
+            setPage(newPageIndex);
+            Keyboard.dismiss();
 
-            updatePage({item: nextItem, index: reversePage(newPageIndex)});
+            const item = reversedAttachments[newPageIndex];
+
+            onNavigate(item);
         },
-        [reversePage, reversedAttachments, updatePage],
+        [onNavigate, reversedAttachments],
     );
 
     /**
@@ -43,11 +45,11 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextPageIndex = reversedPage + deltaSlide;
+            const nextPageIndex = page + deltaSlide;
             updatePage(nextPageIndex);
             pagerRef.current.setPage(nextPageIndex);
         },
-        [reversedPage, updatePage],
+        [page, updatePage],
     );
 
     const autoHideArrowTimeout = useRef(null);
@@ -64,17 +66,15 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
     const autoHideArrow = useCallback(() => {
         cancelAutoHideArrow();
         autoHideArrowTimeout.current = setTimeout(() => {
-            setArrowsVisibility(false);
+            setShouldShowArrows(false);
         }, CONST.ARROW_HIDE_DELAY);
-    }, [cancelAutoHideArrow, setArrowsVisibility]);
-
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    useEffect(() => setArrowsVisibility(true), []);
+    }, [cancelAutoHideArrow]);
 
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
-                carouselState={carouselState}
+                page={page}
+                attachments={attachments}
                 onBack={() => {
                     cycleThroughAttachments(-1);
                     autoHideArrow();
@@ -87,19 +87,19 @@ function AttachmentCarouselView({carouselState, updatePage, setArrowsVisibility,
                 cancelAutoHideArrow={cancelAutoHideArrow}
             />
 
-            {carouselState.containerWidth > 0 && carouselState.containerHeight > 0 && (
+            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
                 <Pager
                     items={processedItems}
-                    initialIndex={reversedPage}
+                    initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => {
                         console.log('page updated');
-                        updatePageInternal(newPage);
+                        updatePage(newPage);
                     }}
-                    onTap={() => setArrowsVisibility()}
-                    onPinchGestureChange={(isPinchGestureRunning) => setArrowsVisibility(!isPinchGestureRunning)}
+                    onTap={() => setShouldShowArrows(!shouldShowArrows)}
+                    onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)}
                     onSwipeDown={onClose}
-                    containerWidth={carouselState.containerWidth}
-                    containerHeight={carouselState.containerHeight}
+                    containerWidth={containerDimensions.width}
+                    containerHeight={containerDimensions.height}
                     ref={pagerRef}
                 />
             )}
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index 7d3b50f446a5..bac717cd0442 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -1,34 +1,37 @@
 import PropTypes from 'prop-types';
 
+const attachmentsPropType = PropTypes.arrayOf(
+    PropTypes.shape({
+        // eslint-disable-next-line react/forbid-prop-types
+        file: PropTypes.object.isRequired,
+        isAuthTokenRequired: PropTypes.bool.isRequired,
+        source: PropTypes.string.isRequired,
+    }),
+);
+
 const propTypes = {
     /**
-     * The current state of the carousel
+     * The initial page of the carousel
      */
-    carouselState: PropTypes.shape({
-        page: PropTypes.number.isRequired,
-        attachments: PropTypes.arrayOf(
-            PropTypes.shape({
-                // eslint-disable-next-line react/forbid-prop-types
-                file: PropTypes.object.isRequired,
-                isAuthTokenRequired: PropTypes.bool.isRequired,
-                source: PropTypes.string.isRequired,
-            }),
-        ).isRequired,
-        shouldShowArrow: PropTypes.bool.isRequired,
-        containerWidth: PropTypes.number.isRequired,
-        containerHeight: PropTypes.number.isRequired,
-        activeSource: PropTypes.string,
-    }).isRequired,
+    initialPage: PropTypes.number.isRequired,
+
+    /**
+     * The attachments of the carousel
+     */
+    attachments: attachmentsPropType.isRequired,
 
     /**
-     * A callback to update the page in the carousel component
+     * The initial active ource of the carousel
      */
-    updatePage: PropTypes.func.isRequired,
+    initialActiveSource: PropTypes.string.isRequired,
 
     /**
-     * A callback for toggling the visibility of the arrows
+     * The container dimensions
      */
-    setArrowsVisibility: PropTypes.func.isRequired,
+    containerDimensions: PropTypes.shape({
+        width: PropTypes.number.isRequired,
+        height: PropTypes.number.isRequired,
+    }).isRequired,
 };
 
-export default propTypes;
+export {propTypes, attachmentsPropType};
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js
index 6cefefd37a53..d94a4114b677 100644
--- a/src/components/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/AttachmentCarousel/CarouselButtons.js
@@ -8,11 +8,14 @@ import Tooltip from '../Tooltip';
 import Button from '../Button';
 import styles from '../../styles/styles';
 import themeColors from '../../styles/themes/default';
+import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes';
 
 const propTypes = {
-    /** Callback to go one page back */
-    // eslint-disable-next-line react/forbid-prop-types
-    carouselState: PropTypes.object.isRequired,
+    /** The current page index */
+    page: PropTypes.number.isRequired,
+
+    /** The attachments from the carousel */
+    attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired,
 
     /** Callback to go one page back */
     onBack: PropTypes.func.isRequired,
@@ -30,11 +33,11 @@ const defaultProps = {
     cancelAutoHideArrow: () => {},
 };
 
-function CarouselButtons(props) {
-    const isForwardDisabled = props.carouselState.page === 0;
-    const isBackDisabled = props.carouselState.page === _.size(props.carouselState.attachments) - 1;
+function CarouselButtons({page, attachments, shouldShowArrow, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) {
+    const isForwardDisabled = page === 0;
+    const isBackDisabled = page === _.size(attachments) - 1;
 
-    return props.carouselState.shouldShowArrow ? (
+    return shouldShowArrow ? (
         <>
             {!isBackDisabled && (
                 <Tooltip text={props.translate('common.previous')}>
@@ -45,9 +48,9 @@ function CarouselButtons(props) {
                             icon={Expensicons.BackArrow}
                             iconFill={themeColors.text}
                             iconStyles={[styles.mr0]}
-                            onPress={props.onBack}
-                            onPressIn={props.cancelAutoHideArrow}
-                            onPressOut={props.autoHideArrow}
+                            onPress={onBack}
+                            onPressIn={cancelAutoHideArrow}
+                            onPressOut={autoHideArrow}
                         />
                     </View>
                 </Tooltip>
@@ -61,9 +64,9 @@ function CarouselButtons(props) {
                             icon={Expensicons.ArrowRight}
                             iconFill={themeColors.text}
                             iconStyles={[styles.mr0]}
-                            onPress={props.onForward}
-                            onPressIn={props.cancelAutoHideArrow}
-                            onPressOut={props.autoHideArrow}
+                            onPress={onForward}
+                            onPressIn={cancelAutoHideArrow}
+                            onPressOut={autoHideArrow}
                         />
                     </View>
                 </Tooltip>
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index d142ceda4899..99fc9ebe2c54 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import {View, PixelRatio, Keyboard} from 'react-native';
+import {View, PixelRatio} from 'react-native';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
 import _ from 'underscore';
@@ -46,25 +46,11 @@ class AttachmentCarousel extends React.Component {
     constructor(props) {
         super(props);
 
-        this.updatePage = this.updatePage.bind(this);
-        this.setArrowsVisibility = this.setArrowsVisibility.bind(this);
         this.createInitialState = this.createInitialState.bind(this);
 
         this.state = this.createInitialState();
     }
 
-    /**
-     * Toggles the visibility of the arrows
-     * @param {Boolean} shouldShowArrow
-     * @param {Boolean} isGestureInUse
-     */
-    setArrowsVisibility(shouldShowArrow) {
-        this.setState((current) => {
-            const newShouldShowArrow = _.isBoolean(shouldShowArrow) ? shouldShowArrow : !current.shouldShowArrow;
-            return {shouldShowArrow: newShouldShowArrow};
-        });
-    }
-
     /**
      * Constructs the initial component state from report actions
      * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}}
@@ -99,44 +85,23 @@ class AttachmentCarousel extends React.Component {
         });
         htmlParser.end();
 
-        const page = _.findIndex(attachments, (a) => a.source === this.props.source);
-        if (page === -1) {
+        const initialPage = _.findIndex(attachments, (a) => a.source === this.props.source);
+        if (initialPage === -1) {
             throw new Error('Attachment not found');
         }
 
         // Update the parent modal's state with the source and name from the mapped attachments
-        this.props.onNavigate(attachments[page]);
+        this.props.onNavigate(attachments[initialPage]);
 
         return {
-            page,
+            initialPage,
             attachments,
-            shouldShowArrow: false,
             containerWidth: 0,
             containerHeight: 0,
-            activeSource: null,
+            initialActiveSource: null,
         };
     }
 
-    /**
-     * Updates the page state when the user navigates between attachments
-     * @param {Object} item
-     * @param {number} index
-     */
-    updatePage({item, index}) {
-        Keyboard.dismiss();
-
-        if (!item) {
-            // eslint-disable-next-line react/no-unused-state
-            this.setState({activeSource: null});
-            return;
-        }
-
-        const page = index;
-        this.props.onNavigate(item);
-        // eslint-disable-next-line react/no-unused-state
-        this.setState({page, activeSource: item.source});
-    }
-
     render() {
         return (
             <View
@@ -147,9 +112,10 @@ class AttachmentCarousel extends React.Component {
                 }
             >
                 <AttachmentCarouselView
-                    carouselState={this.state}
-                    updatePage={this.updatePage}
-                    setArrowsVisibility={this.setArrowsVisibility}
+                    attachments={this.state.attachments}
+                    initialPage={this.state.initialPage}
+                    initialActiveSource={this.state.initialActiveSource}
+                    containerDimensions={{width: this.state.containerWidth, height: this.state.containerHeight}}
                     onClose={this.props.onClose}
                 />
             </View>

From ef0952d7740b8e49608eb17c5995625d0c19dce8 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 21:57:08 +0200
Subject: [PATCH 054/133] fix: state

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js   | 5 +++--
 .../AttachmentCarouselView/index.native.js               | 9 +++++----
 .../AttachmentCarouselView/propTypes.js                  | 2 +-
 src/components/AttachmentCarousel/CarouselButtons.js     | 7 +++++--
 src/components/AttachmentCarousel/index.js               | 1 +
 5 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
index 74e229db20b8..d09d831e143d 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -42,10 +42,11 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
             }
 
             const pageIndex = index;
-            onNavigate(item);
-            // eslint-disable-next-line react/no-unused-state
+
             setPage(pageIndex);
             setActiveSource(item.source);
+
+            onNavigate(item);
         },
         [onNavigate],
     );
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 3610f477ce44..50326d46bc6d 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -19,8 +19,8 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
 
     const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
 
-    const [shouldShowArrows, setShouldShowArrows] = useState(true);
     const [page, setPage] = useState(reversePage(initialPage));
+    const [shouldShowArrows, setShouldShowArrows] = useState(true);
 
     /**
      * Updates the page state when the user navigates between attachments
@@ -29,11 +29,11 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
      */
     const updatePage = useCallback(
         (newPageIndex) => {
-            setPage(newPageIndex);
             Keyboard.dismiss();
 
-            const item = reversedAttachments[newPageIndex];
+            setPage(newPageIndex);
 
+            const item = reversedAttachments[newPageIndex];
             onNavigate(item);
         },
         [onNavigate, reversedAttachments],
@@ -73,7 +73,8 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
-                page={page}
+                shouldShowArrows={shouldShowArrows}
+                page={reversePage(page)}
                 attachments={attachments}
                 onBack={() => {
                     cycleThroughAttachments(-1);
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index bac717cd0442..19874ada5e21 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -23,7 +23,7 @@ const propTypes = {
     /**
      * The initial active ource of the carousel
      */
-    initialActiveSource: PropTypes.string.isRequired,
+    initialActiveSource: PropTypes.string,
 
     /**
      * The container dimensions
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/AttachmentCarousel/CarouselButtons.js
index d94a4114b677..59b4037f41c5 100644
--- a/src/components/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/AttachmentCarousel/CarouselButtons.js
@@ -11,6 +11,9 @@ import themeColors from '../../styles/themes/default';
 import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes';
 
 const propTypes = {
+    /** Where the arrows should be visible */
+    shouldShowArrows: PropTypes.bool.isRequired,
+
     /** The current page index */
     page: PropTypes.number.isRequired,
 
@@ -33,11 +36,11 @@ const defaultProps = {
     cancelAutoHideArrow: () => {},
 };
 
-function CarouselButtons({page, attachments, shouldShowArrow, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) {
+function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) {
     const isForwardDisabled = page === 0;
     const isBackDisabled = page === _.size(attachments) - 1;
 
-    return shouldShowArrow ? (
+    return shouldShowArrows ? (
         <>
             {!isBackDisabled && (
                 <Tooltip text={props.translate('common.previous')}>
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 99fc9ebe2c54..08f3fd12ea9c 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -117,6 +117,7 @@ class AttachmentCarousel extends React.Component {
                     initialActiveSource={this.state.initialActiveSource}
                     containerDimensions={{width: this.state.containerWidth, height: this.state.containerHeight}}
                     onClose={this.props.onClose}
+                    onNavigate={this.props.onNavigate}
                 />
             </View>
         );

From b6a87218f89d7b364d7297f8547ca703fc0adb1c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 21:58:46 +0200
Subject: [PATCH 055/133] fix: image should not be "flippable"

---
 src/components/AttachmentCarousel/Lightbox.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 965cf8b55f7f..41d0de8c6f8f 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -81,7 +81,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
     // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => zoomScale.value + imageScale - 1, [imageScale]);
+    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + imageScale - 1, 0), [imageScale]);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
     const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);

From 975df225577cb661692507bde95e9c828b5bbc35 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 21:58:58 +0200
Subject: [PATCH 056/133] remove log

---
 .../AttachmentCarouselView/index.native.js                   | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 50326d46bc6d..5f5bd81b7cab 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -92,10 +92,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
                 <Pager
                     items={processedItems}
                     initialIndex={page}
-                    onPageSelected={({nativeEvent: {position: newPage}}) => {
-                        console.log('page updated');
-                        updatePage(newPage);
-                    }}
+                    onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
                     onTap={() => setShouldShowArrows(!shouldShowArrows)}
                     onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)}
                     onSwipeDown={onClose}

From 77b4ea45df9a2876c3ec419e219c8a92a44876ec Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 22:03:35 +0200
Subject: [PATCH 057/133] fix: on tap not working

---
 .../AttachmentCarouselView/index.native.js    | 43 +++++++++----------
 1 file changed, 20 insertions(+), 23 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 5f5bd81b7cab..86b95b3a98b8 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -20,6 +20,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
 
     const [page, setPage] = useState(reversePage(initialPage));
+    const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
     const [shouldShowArrows, setShouldShowArrows] = useState(true);
 
     /**
@@ -39,19 +40,6 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
         [onNavigate, reversedAttachments],
     );
 
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide) => {
-            const nextPageIndex = page + deltaSlide;
-            updatePage(nextPageIndex);
-            pagerRef.current.setPage(nextPageIndex);
-        },
-        [page, updatePage],
-    );
-
     const autoHideArrowTimeout = useRef(null);
 
     /**
@@ -70,20 +58,29 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
         }, CONST.ARROW_HIDE_DELAY);
     }, [cancelAutoHideArrow]);
 
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide) => {
+            const nextPageIndex = page + deltaSlide;
+            updatePage(nextPageIndex);
+            pagerRef.current.setPage(nextPageIndex);
+
+            autoHideArrow();
+        },
+        [autoHideArrow, page, updatePage],
+    );
+
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
-                shouldShowArrows={shouldShowArrows}
+                shouldShowArrows={shouldShowArrows && !isPinchGestureRunning}
                 page={reversePage(page)}
                 attachments={attachments}
-                onBack={() => {
-                    cycleThroughAttachments(-1);
-                    autoHideArrow();
-                }}
-                onForward={() => {
-                    cycleThroughAttachments(1);
-                    autoHideArrow();
-                }}
+                onBack={() => cycleThroughAttachments(-1)}
+                onForward={() => cycleThroughAttachments(1)}
                 autoHideArrow={autoHideArrow}
                 cancelAutoHideArrow={cancelAutoHideArrow}
             />
@@ -94,7 +91,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
                     initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
                     onTap={() => setShouldShowArrows(!shouldShowArrows)}
-                    onPinchGestureChange={(isPinchGestureRunning) => setShouldShowArrows(!isPinchGestureRunning)}
+                    onPinchGestureChange={(newIsPinchtGestureRunning) => setIsPinchGestureRunning(newIsPinchtGestureRunning)}
                     onSwipeDown={onClose}
                     containerWidth={containerDimensions.width}
                     containerHeight={containerDimensions.height}

From be8bac1570c5630cac54a8fedceddd8404ffac50 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 22:15:57 +0200
Subject: [PATCH 058/133] fix: arrows

---
 .../AttachmentCarouselView/index.native.js    | 58 ++++++++++++-------
 src/components/AttachmentCarousel/Lightbox.js | 31 +++-------
 2 files changed, 44 insertions(+), 45 deletions(-)

diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 86b95b3a98b8..bfc10feaea8e 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,4 +1,4 @@
-import React, {useCallback, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {View, Keyboard} from 'react-native';
 import _ from 'underscore';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
@@ -23,23 +23,6 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
     const [shouldShowArrows, setShouldShowArrows] = useState(true);
 
-    /**
-     * Updates the page state when the user navigates between attachments
-     * @param {Object} item
-     * @param {number} index
-     */
-    const updatePage = useCallback(
-        (newPageIndex) => {
-            Keyboard.dismiss();
-
-            setPage(newPageIndex);
-
-            const item = reversedAttachments[newPageIndex];
-            onNavigate(item);
-        },
-        [onNavigate, reversedAttachments],
-    );
-
     const autoHideArrowTimeout = useRef(null);
 
     /**
@@ -48,8 +31,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     const cancelAutoHideArrow = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
 
     /**
-     * On a touch screen device, automatically hide the arrows
-     * if there is no interaction for 3 seconds.
+     * Automatically hide the arrows if there is no interaction for 3 seconds.
      */
     const autoHideArrow = useCallback(() => {
         cancelAutoHideArrow();
@@ -58,6 +40,30 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
         }, CONST.ARROW_HIDE_DELAY);
     }, [cancelAutoHideArrow]);
 
+    const showArrows = useCallback(() => {
+        setShouldShowArrows(true);
+        autoHideArrow();
+    }, [autoHideArrow]);
+
+    /**
+     * Updates the page state when the user navigates between attachments
+     * @param {Object} item
+     * @param {number} index
+     */
+    const updatePage = useCallback(
+        (newPageIndex) => {
+            Keyboard.dismiss();
+
+            setPage(newPageIndex);
+
+            showArrows();
+
+            const item = reversedAttachments[newPageIndex];
+            onNavigate(item);
+        },
+        [onNavigate, reversedAttachments, showArrows],
+    );
+
     /**
      * Increments or decrements the index to get another selected item
      * @param {Number} deltaSlide
@@ -73,6 +79,11 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
         [autoHideArrow, page, updatePage],
     );
 
+    useEffect(() => {
+        autoHideArrow();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, []);
+
     return (
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
@@ -90,8 +101,11 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
                     items={processedItems}
                     initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                    onTap={() => setShouldShowArrows(!shouldShowArrows)}
-                    onPinchGestureChange={(newIsPinchtGestureRunning) => setIsPinchGestureRunning(newIsPinchtGestureRunning)}
+                    onTap={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())}
+                    onPinchGestureChange={(newIsPinchGestureRunning) => {
+                        setIsPinchGestureRunning(newIsPinchGestureRunning);
+                        if (!newIsPinchGestureRunning && !shouldShowArrows) showArrows();
+                    }}
                     onSwipeDown={onClose}
                     containerWidth={containerDimensions.width}
                     containerHeight={containerDimensions.height}
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index 41d0de8c6f8f..d9c95a0681dc 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -491,21 +491,18 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
             pinchGestureRunning.value = false;
         });
 
-    const isPinchGestureInUse = useSharedValue(false);
+    const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false);
     useAnimatedReaction(
         () => [zoomScale.value, pinchGestureRunning.value],
-        ([s, running]) => {
-            const newIsPinchGestureInUse = s !== 1 || running;
-            if (isPinchGestureInUse.value !== newIsPinchGestureInUse) {
-                isPinchGestureInUse.value = newIsPinchGestureInUse;
+        ([zoom, running]) => {
+            const newIsPinchGestureInUse = zoom !== 1 || running;
+            if (isPinchGestureInUse !== newIsPinchGestureInUse) {
+                runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse);
             }
         },
     );
-
-    useAnimatedReaction(
-        () => isPinchGestureInUse.value,
-        (zoomed) => runOnJS(onPinchGestureChange)(zoomed),
-    );
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]);
 
     const animatedStyles = useAnimatedStyle(() => {
         const x = scaleTranslateX.value + translateX.value + offsetX.value;
@@ -753,19 +750,7 @@ const pagerDefaultProps = {
     forwardedRef: null,
 };
 
-function Pager({
-    items,
-    initialIndex = 0,
-    onPageSelected,
-    onTap,
-    onSwipe = noopWorklet,
-    onSwipeSuccess = () => {},
-    onSwipeDown = () => {},
-    onPinchGestureChange = () => {},
-    forwardedRef,
-    containerWidth,
-    containerHeight,
-}) {
+function Pager({items, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef, containerWidth, containerHeight}) {
     const shouldPagerScroll = useSharedValue(true);
     const pagerRef = useRef(null);
 

From 56d1d50e45a0e8ba4f0489a8175de6c58eb9d000 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 18 Jul 2023 22:35:03 +0200
Subject: [PATCH 059/133] fix: flashing

---
 src/components/AttachmentCarousel/Lightbox.js | 28 +++++++++++++++----
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/AttachmentCarousel/Lightbox.js
index d9c95a0681dc..5e24d4c54d5d 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/AttachmentCarousel/Lightbox.js
@@ -1,5 +1,5 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle, useMemo} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
 import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
@@ -601,7 +601,7 @@ const pagePropTypes = {
     }).isRequired,
 };
 
-function Page({isActive, item}) {
+function Page({isActive: isActiveProp, item}) {
     const {canvasWidth, canvasHeight} = useContext(Context);
 
     const dimensions = cachedDimensions.get(item.url);
@@ -610,15 +610,31 @@ function Page({isActive, item}) {
 
     const [isImageLoading, setIsImageLoading] = useState(!areImageDimensionsSet);
 
+    const [isActive, setIsActive] = useState(isActiveProp);
+    // We delay setting a page to active state by a (few) millisecond(s),
+    // to prevent the image transformer from flashing while still rendering
+    // Instead, we show the fallback image while the image transformer is loading the image
     useEffect(() => {
-        if (!isActive) return;
+        if (isActiveProp) setTimeout(() => setIsActive(true), 1);
+        else setIsActive(false);
+    }, [isActiveProp]);
+
+    useMemo(() => {
+        if (!isActiveProp) return;
         setIsImageLoading(true);
-    }, [isActive]);
+    }, [isActiveProp]);
+
+    const [showFallback, setShowFallback] = useState(isImageLoading);
+    // We delay hiding the fallback image while image transformer is still rendering
+    useEffect(() => {
+        if (isImageLoading) setShowFallback(true);
+        else setTimeout(() => setShowFallback(false), 100);
+    }, [isImageLoading]);
 
     return (
         <>
             {isActive && (
-                <View style={[StyleSheet.absoluteFill, {opacity: isImageLoading ? 0 : 1}]}>
+                <View style={StyleSheet.absoluteFill}>
                     <ImageTransformer
                         isActive
                         imageWidth={dimensions?.imageWidth}
@@ -655,7 +671,7 @@ function Page({isActive, item}) {
             )}
 
             {/* Keep rendering the image without gestures as fallback while ImageTransformer is loading the image */}
-            {(!isActive || isImageLoading) && (
+            {(!isActive || showFallback) && (
                 <ImageWrapper>
                     <Image
                         source={{uri: item.url}}

From 4cc0b876a3cb3ee4318d9fa18568b21cf2d0d3df Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 10:42:41 +0200
Subject: [PATCH 060/133] rename component

---
 src/components/AttachmentCarousel/CarouselActions.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/AttachmentCarousel/CarouselActions.js b/src/components/AttachmentCarousel/CarouselActions.js
index a33bb854d872..f74aa5e5fcbf 100644
--- a/src/components/AttachmentCarousel/CarouselActions.js
+++ b/src/components/AttachmentCarousel/CarouselActions.js
@@ -9,7 +9,7 @@ const propTypes = {
     onCycleThroughAttachments: PropTypes.func.isRequired,
 };
 
-function Carousel(props) {
+function CarouselActions(props) {
     useEffect(() => {
         const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT;
         const unsubscribeLeftKey = KeyboardShortcut.subscribe(
@@ -51,6 +51,6 @@ function Carousel(props) {
     return null;
 }
 
-Carousel.propTypes = propTypes;
+CarouselActions.propTypes = propTypes;
 
-export default Carousel;
+export default CarouselActions;

From f44fff7187e5ccad1868c1ac3bcf9f6170d778c4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:23:50 +0200
Subject: [PATCH 061/133] major re-structure

---
 src/components/AttachmentModal.js             |   2 +-
 src/components/AttachmentView.js              | 164 ----------------
 .../AttachmentCarouselView/index.js           |  10 +-
 .../AttachmentCarouselView/index.native.js    |  41 ++--
 .../AttachmentCarouselView/propTypes.js       |  14 +-
 .../AttachmentCarousel/CarouselActions.js     |   4 +-
 .../AttachmentCarousel/CarouselButtons.js     |  12 +-
 .../AttachmentCarousel/index.js               |   0
 .../AttachmentCarouselPage.js}                | 174 +----------------
 .../AttachmentCarouselPagerContext.js         |   5 +
 .../AttachmentCarouselPager/index.js          | 175 ++++++++++++++++++
 .../AttachmentViewImage/index.js              |  61 ++++++
 .../AttachmentViewImage/index.native.js       |  64 +++++++
 .../Attachments/AttachmentView/index.js       | 146 +++++++++++++++
 src/components/Attachments/propTypes.js       |  18 ++
 15 files changed, 523 insertions(+), 367 deletions(-)
 delete mode 100755 src/components/AttachmentView.js
 rename src/components/{ => Attachments}/AttachmentCarousel/AttachmentCarouselView/index.js (95%)
 rename src/components/{ => Attachments}/AttachmentCarousel/AttachmentCarouselView/index.native.js (78%)
 rename src/components/{ => Attachments}/AttachmentCarousel/AttachmentCarouselView/propTypes.js (58%)
 rename src/components/{ => Attachments}/AttachmentCarousel/CarouselActions.js (94%)
 rename src/components/{ => Attachments}/AttachmentCarousel/CarouselButtons.js (90%)
 rename src/components/{ => Attachments}/AttachmentCarousel/index.js (100%)
 rename src/components/{AttachmentCarousel/Lightbox.js => Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js} (82%)
 create mode 100644 src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js
 create mode 100644 src/components/Attachments/AttachmentCarouselPager/index.js
 create mode 100755 src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
 create mode 100755 src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
 create mode 100755 src/components/Attachments/AttachmentView/index.js
 create mode 100644 src/components/Attachments/propTypes.js

diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index aa23c03c1a4c..4e49d89ab9de 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -7,7 +7,7 @@ import lodashExtend from 'lodash/extend';
 import _ from 'underscore';
 import CONST from '../CONST';
 import Modal from './Modal';
-import AttachmentView from './AttachmentView';
+import AttachmentView from './AttachmentView/AttachmentView';
 import AttachmentCarousel from './AttachmentCarousel';
 import styles from '../styles/styles';
 import * as StyleUtils from '../styles/StyleUtils';
diff --git a/src/components/AttachmentView.js b/src/components/AttachmentView.js
deleted file mode 100755
index d880ac9b9076..000000000000
--- a/src/components/AttachmentView.js
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, {memo, useState} from 'react';
-import {View, ActivityIndicator} from 'react-native';
-import _ from 'underscore';
-import PropTypes from 'prop-types';
-import Str from 'expensify-common/lib/str';
-import styles from '../styles/styles';
-import PDFView from './PDFView';
-import ImageView from './ImageView';
-import Icon from './Icon';
-import * as Expensicons from './Icon/Expensicons';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-import compose from '../libs/compose';
-import Text from './Text';
-import Tooltip from './Tooltip';
-import themeColors from '../styles/themes/default';
-import variables from '../styles/variables';
-import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
-import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
-import CONST from '../CONST';
-
-const propTypes = {
-    /** Whether source url requires authentication */
-    isAuthTokenRequired: PropTypes.bool,
-
-    /** URL to full-sized attachment or SVG function */
-    source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
-
-    /** File object maybe be instance of File or Object */
-    file: PropTypes.shape({
-        name: PropTypes.string,
-    }),
-
-    /** Flag to show/hide download icon */
-    shouldShowDownloadIcon: PropTypes.bool,
-
-    /** Flag to show the loading indicator */
-    shouldShowLoadingSpinnerIcon: PropTypes.bool,
-
-    /** Whether this view is the active screen  */
-    isFocused: PropTypes.bool,
-
-    /** Function for handle on press */
-    onPress: PropTypes.func,
-
-    /** Handles scale changed event */
-    onScaleChanged: PropTypes.func,
-
-    /** Notify parent that the UI should be modified to accommodate keyboard */
-    onToggleKeyboard: PropTypes.func,
-
-    /** Extra styles to pass to View wrapper */
-    // eslint-disable-next-line react/forbid-prop-types
-    containerStyles: PropTypes.arrayOf(PropTypes.object),
-
-    ...withLocalizePropTypes,
-};
-
-const defaultProps = {
-    isAuthTokenRequired: false,
-    file: {
-        name: '',
-    },
-    shouldShowDownloadIcon: false,
-    shouldShowLoadingSpinnerIcon: false,
-    onPress: undefined,
-    onScaleChanged: () => {},
-    onToggleKeyboard: () => {},
-    containerStyles: [],
-    isFocused: false,
-};
-
-function AttachmentView(props) {
-    const [loadComplete, setLoadComplete] = useState(false);
-    const containerStyles = [styles.flex1, styles.flexRow, styles.alignSelfStretch];
-
-    // Handles case where source is a component (ex: SVG)
-    if (_.isFunction(props.source)) {
-        return (
-            <Icon
-                src={props.source}
-                height={variables.defaultAvatarPreviewSize}
-                width={variables.defaultAvatarPreviewSize}
-            />
-        );
-    }
-
-    // Check both source and file.name since PDFs dragged into the the text field
-    // will appear with a source that is a blob
-    if (Str.isPDF(props.source) || (props.file && Str.isPDF(props.file.name || props.translate('attachmentView.unknownFilename')))) {
-        const sourceURL = props.isAuthTokenRequired ? addEncryptedAuthTokenToURL(props.source) : props.source;
-        return (
-            <PDFView
-                onPress={props.onPress}
-                isFocused={props.isFocused}
-                sourceURL={sourceURL}
-                fileName={props.file.name}
-                style={styles.imageModalPDF}
-                onToggleKeyboard={props.onToggleKeyboard}
-                onScaleChanged={props.onScaleChanged}
-                onLoadComplete={() => !loadComplete && setLoadComplete(true)}
-            />
-        );
-    }
-
-    // For this check we use both source and file.name since temporary file source is a blob
-    // both PDFs and images will appear as images when pasted into the the text field
-    const isImage = Str.isImage(props.source);
-    if (isImage || (props.file && Str.isImage(props.file.name))) {
-        const children = (
-            <ImageView
-                onScaleChanged={props.onScaleChanged}
-                url={props.source}
-                fileName={props.file.name}
-                isAuthTokenRequired={isImage && props.isAuthTokenRequired}
-            />
-        );
-        return props.onPress ? (
-            <PressableWithoutFeedback
-                onPress={props.onPress}
-                disabled={loadComplete}
-                style={containerStyles}
-                accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
-                accessibilityLabel={props.file.name || props.translate('attachmentView.unknownFilename')}
-            >
-                {children}
-            </PressableWithoutFeedback>
-        ) : (
-            children
-        );
-    }
-
-    return (
-        <View style={[styles.defaultAttachmentView, ...props.containerStyles]}>
-            <View style={styles.mr2}>
-                <Icon src={Expensicons.Paperclip} />
-            </View>
-
-            <Text style={[styles.textStrong, styles.flexShrink1, styles.breakAll, styles.flexWrap, styles.mw100]}>{props.file && props.file.name}</Text>
-            {!props.shouldShowLoadingSpinnerIcon && props.shouldShowDownloadIcon && (
-                <Tooltip text={props.translate('common.download')}>
-                    <View style={styles.ml2}>
-                        <Icon src={Expensicons.Download} />
-                    </View>
-                </Tooltip>
-            )}
-            {props.shouldShowLoadingSpinnerIcon && (
-                <View style={styles.ml2}>
-                    <Tooltip text={props.translate('common.downloading')}>
-                        <ActivityIndicator
-                            size="small"
-                            color={themeColors.textSupporting}
-                        />
-                    </Tooltip>
-                </View>
-            )}
-        </View>
-    );
-}
-
-AttachmentView.propTypes = propTypes;
-AttachmentView.defaultProps = defaultProps;
-AttachmentView.displayName = 'AttachmentView';
-
-export default compose(memo, withLocalize)(AttachmentView);
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
similarity index 95%
rename from src/components/AttachmentCarousel/AttachmentCarouselView/index.js
rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
index d09d831e143d..37ccd2f3b71c 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -1,12 +1,12 @@
 import React, {useRef, useCallback, useState} from 'react';
 import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
 import _ from 'underscore';
-import styles from '../../../styles/styles';
+import styles from '../../../../styles/styles';
 import CarouselActions from '../CarouselActions';
 import AttachmentView from '../../AttachmentView';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../../../withWindowDimensions';
 import CarouselButtons from '../CarouselButtons';
-import {propTypes as attachmentCarouselViewPropTypes} from './propTypes';
+import attachmentCarouselViewPropTypes from './propTypes';
 
 const VIEWABILITY_CONFIG = {
     // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -117,10 +117,8 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
+                item={item}
                 isFocused={activeSource === item.source}
-                source={item.source}
-                file={item.file}
-                isAuthTokenRequired={item.isAuthTokenRequired}
             />
         ),
         [activeSource],
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
similarity index 78%
rename from src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
index bfc10feaea8e..df835b300784 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,14 +1,15 @@
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {View, Keyboard} from 'react-native';
 import _ from 'underscore';
-import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
-import Pager from '../Lightbox';
-import styles from '../../../styles/styles';
+import addEncryptedAuthTokenToURL from '../../../../libs/addEncryptedAuthTokenToURL';
+import AttachmentCarouselPager from '../../AttachmentCarouselPager';
+import styles from '../../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
-import {propTypes as attachmentCarouselViewPropTypes} from './propTypes';
-import CONST from '../../../CONST';
+import AttachmentView from '../../AttachmentView';
+import propTypes from './propTypes';
+import CONST from '../../../..CONST';
 
-function AttachmentCarouselView({attachments, initialPage, containerDimensions, onClose, onNavigate}) {
+function AttachmentCarouselView({attachments, initialPage, initialActiveSource, containerDimensions, onClose, onNavigate}) {
     const pagerRef = useRef(null);
 
     // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
@@ -20,6 +21,7 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
 
     const [page, setPage] = useState(reversePage(initialPage));
+    const [activeSource, setActiveSource] = useState(initialActiveSource);
     const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
     const [shouldShowArrows, setShouldShowArrows] = useState(true);
 
@@ -53,12 +55,13 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     const updatePage = useCallback(
         (newPageIndex) => {
             Keyboard.dismiss();
-
-            setPage(newPageIndex);
-
             showArrows();
 
             const item = reversedAttachments[newPageIndex];
+
+            setPage(newPageIndex);
+            setActiveSource(item.source);
+
             onNavigate(item);
         },
         [onNavigate, reversedAttachments, showArrows],
@@ -79,6 +82,21 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
         [autoHideArrow, page, updatePage],
     );
 
+    /**
+     * Defines how a single attachment should be rendered
+     * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
+     * @returns {JSX.Element}
+     */
+    const renderItem = useCallback(
+        ({item}) => (
+            <AttachmentView
+                item={item}
+                isFocused={activeSource === item.source}
+            />
+        ),
+        [activeSource],
+    );
+
     useEffect(() => {
         autoHideArrow();
         // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -97,8 +115,9 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
             />
 
             {containerDimensions.width > 0 && containerDimensions.height > 0 && (
-                <Pager
+                <AttachmentCarouselPager
                     items={processedItems}
+                    renderItem={renderItem}
                     initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
                     onTap={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())}
@@ -116,6 +135,6 @@ function AttachmentCarouselView({attachments, initialPage, containerDimensions,
     );
 }
 
-AttachmentCarouselView.propTypes = attachmentCarouselViewPropTypes;
+AttachmentCarouselView.propTypes = propTypes;
 
 export default AttachmentCarouselView;
diff --git a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
similarity index 58%
rename from src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
index 19874ada5e21..28a5d9610068 100644
--- a/src/components/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
@@ -1,13 +1,5 @@
 import PropTypes from 'prop-types';
-
-const attachmentsPropType = PropTypes.arrayOf(
-    PropTypes.shape({
-        // eslint-disable-next-line react/forbid-prop-types
-        file: PropTypes.object.isRequired,
-        isAuthTokenRequired: PropTypes.bool.isRequired,
-        source: PropTypes.string.isRequired,
-    }),
-);
+import * as AttachmentsPropTypes from '../../propTypes';
 
 const propTypes = {
     /**
@@ -18,7 +10,7 @@ const propTypes = {
     /**
      * The attachments of the carousel
      */
-    attachments: attachmentsPropType.isRequired,
+    attachments: AttachmentsPropTypes.attachmentsPropType.isRequired,
 
     /**
      * The initial active ource of the carousel
@@ -34,4 +26,4 @@ const propTypes = {
     }).isRequired,
 };
 
-export {propTypes, attachmentsPropType};
+export default propTypes;
diff --git a/src/components/AttachmentCarousel/CarouselActions.js b/src/components/Attachments/AttachmentCarousel/CarouselActions.js
similarity index 94%
rename from src/components/AttachmentCarousel/CarouselActions.js
rename to src/components/Attachments/AttachmentCarousel/CarouselActions.js
index f74aa5e5fcbf..8c55c70c0899 100644
--- a/src/components/AttachmentCarousel/CarouselActions.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselActions.js
@@ -1,8 +1,8 @@
 import {useEffect} from 'react';
 import lodashGet from 'lodash/get';
 import PropTypes from 'prop-types';
-import KeyboardShortcut from '../../libs/KeyboardShortcut';
-import CONST from '../../CONST';
+import KeyboardShortcut from '../../../libs/KeyboardShortcut';
+import CONST from '../../../CONST';
 
 const propTypes = {
     /** Callback to cycle through attachments */
diff --git a/src/components/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
similarity index 90%
rename from src/components/AttachmentCarousel/CarouselButtons.js
rename to src/components/Attachments/AttachmentCarousel/CarouselButtons.js
index 59b4037f41c5..9f5814a60a28 100644
--- a/src/components/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
@@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
 import React from 'react';
 import _ from 'underscore';
 import {View} from 'react-native';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-import * as Expensicons from '../Icon/Expensicons';
-import Tooltip from '../Tooltip';
-import Button from '../Button';
-import styles from '../../styles/styles';
-import themeColors from '../../styles/themes/default';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import * as Expensicons from '../../Icon/Expensicons';
+import Tooltip from '../../Tooltip';
+import Button from '../../Button';
+import styles from '../../../styles/styles';
+import themeColors from '../../../styles/themes/default';
 import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes';
 
 const propTypes = {
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
similarity index 100%
rename from src/components/AttachmentCarousel/index.js
rename to src/components/Attachments/AttachmentCarousel/index.js
diff --git a/src/components/AttachmentCarousel/Lightbox.js b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
similarity index 82%
rename from src/components/AttachmentCarousel/Lightbox.js
rename to src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
index 5e24d4c54d5d..daa198e63f02 100644
--- a/src/components/AttachmentCarousel/Lightbox.js
+++ b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
@@ -1,27 +1,23 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {createContext, useContext, useEffect, useRef, useState, useImperativeHandle, useMemo} from 'react';
+import React, {useContext, useEffect, useRef, useState, useMemo} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
-import {Gesture, GestureDetector, GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
 import Animated, {
     cancelAnimation,
     runOnJS,
     runOnUI,
-    useAnimatedProps,
     useAnimatedReaction,
     useAnimatedStyle,
     useDerivedValue,
-    useEvent,
-    useHandler,
     useSharedValue,
     useWorkletCallback,
     withDecay,
     withSpring,
 } from 'react-native-reanimated';
-import PagerView from 'react-native-pager-view';
-import _ from 'underscore';
 import Image from '../Image';
 import styles from '../../styles/styles';
+import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 
 const DOUBLE_TAP_SCALE = 3;
 const MAX_SCALE = 20;
@@ -33,8 +29,6 @@ const SPRING_CONFIG = {
     damping: 500,
 };
 
-const Context = createContext(null);
-
 function clamp(value, lowerBound, upperBound) {
     'worklet';
 
@@ -76,7 +70,7 @@ const imageTransformerDefaultProps = {
 };
 
 function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
-    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(Context);
+    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
@@ -601,8 +595,8 @@ const pagePropTypes = {
     }).isRequired,
 };
 
-function Page({isActive: isActiveProp, item}) {
-    const {canvasWidth, canvasHeight} = useContext(Context);
+function AttachmentCarouselPage({isActive: isActiveProp, item}) {
+    const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
     const dimensions = cachedDimensions.get(item.url);
 
@@ -708,158 +702,6 @@ function Page({isActive: isActiveProp, item}) {
         </>
     );
 }
-Page.propTypes = pagePropTypes;
-
-const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
-
-function usePageScrollHandler(handlers, dependencies) {
-    const {context, doDependenciesDiffer} = useHandler(handlers, dependencies);
-    const subscribeForEvents = ['onPageScroll'];
-
-    return useEvent(
-        (event) => {
-            'worklet';
-
-            const {onPageScroll} = handlers;
-            if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
-                onPageScroll(event, context);
-            }
-        },
-        subscribeForEvents,
-        doDependenciesDiffer,
-    );
-}
-
-const noopWorklet = () => {
-    'worklet';
-
-    // noop
-};
-
-const pagerPropTypes = {
-    items: PropTypes.arrayOf(
-        PropTypes.shape({
-            key: PropTypes.string,
-            url: PropTypes.string,
-        }),
-    ).isRequired,
-    initialIndex: PropTypes.number,
-    onPageSelected: PropTypes.func,
-    onTap: PropTypes.func,
-    onSwipe: PropTypes.func,
-    onSwipeSuccess: PropTypes.func,
-    onSwipeDown: PropTypes.func,
-    onPinchGestureChange: PropTypes.func,
-    forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
-    containerWidth: PropTypes.number.isRequired,
-    containerHeight: PropTypes.number.isRequired,
-};
-
-const pagerDefaultProps = {
-    initialIndex: 0,
-    onPageSelected: () => {},
-    onTap: () => {},
-    onSwipe: noopWorklet,
-    onSwipeSuccess: () => {},
-    onSwipeDown: () => {},
-    onPinchGestureChange: () => {},
-    forwardedRef: null,
-};
-
-function Pager({items, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef, containerWidth, containerHeight}) {
-    const shouldPagerScroll = useSharedValue(true);
-    const pagerRef = useRef(null);
-
-    const isScrolling = useSharedValue(false);
-    const activeIndex = useSharedValue(initialIndex);
+AttachmentCarouselPage.propTypes = pagePropTypes;
 
-    const pageScrollHandler = usePageScrollHandler(
-        {
-            onPageScroll: (e) => {
-                'worklet';
-
-                activeIndex.value = e.position;
-                isScrolling.value = e.offset !== 0;
-            },
-        },
-        [],
-    );
-
-    const [activePage, setActivePage] = useState(initialIndex);
-
-    // we use reanimated for this since onPageSelected is called
-    // in the middle of the pager animation
-    useAnimatedReaction(
-        () => isScrolling.value,
-        (stillScrolling) => {
-            if (stillScrolling) {
-                return;
-            }
-
-            runOnJS(setActivePage)(activeIndex.value);
-        },
-    );
-
-    useImperativeHandle(
-        forwardedRef,
-        () => ({
-            setPage: (...props) => pagerRef.current.setPage(...props),
-        }),
-        [],
-    );
-
-    const animatedProps = useAnimatedProps(() => ({
-        scrollEnabled: shouldPagerScroll.value,
-    }));
-
-    return (
-        <GestureHandlerRootView style={styles.flex1}>
-            <Context.Provider
-                value={{
-                    canvasWidth: containerWidth,
-                    canvasHeight: containerHeight,
-                    isScrolling,
-                    pagerRef,
-                    shouldPagerScroll,
-                    onPinchGestureChange,
-                    onTap,
-                    onSwipe,
-                    onSwipeSuccess,
-                    onSwipeDown,
-                }}
-            >
-                <AnimatedPagerView
-                    pageMargin={40}
-                    onPageScroll={pageScrollHandler}
-                    animatedProps={animatedProps}
-                    onPageSelected={onPageSelected}
-                    ref={pagerRef}
-                    style={styles.flex1}
-                    initialPage={initialIndex}
-                >
-                    {_.map(items, (item, index) => (
-                        <View
-                            key={item.key}
-                            style={styles.flex1}
-                        >
-                            <Page
-                                isActive={index === activePage}
-                                item={item}
-                            />
-                        </View>
-                    ))}
-                </AnimatedPagerView>
-            </Context.Provider>
-        </GestureHandlerRootView>
-    );
-}
-Pager.propTypes = pagerPropTypes;
-Pager.defaultProps = pagerDefaultProps;
-
-export default React.forwardRef((props, ref) => (
-    <Pager
-        // eslint-disable-next-line react/jsx-props-no-spreading
-        {...props}
-        forwardedRef={ref}
-    />
-));
+export default AttachmentCarouselPage;
diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js
new file mode 100644
index 000000000000..39535288e22d
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js
@@ -0,0 +1,5 @@
+import {createContext} from 'react';
+
+const AttachmentCarouselContextPager = createContext(null);
+
+export default AttachmentCarouselContextPager;
diff --git a/src/components/Attachments/AttachmentCarouselPager/index.js b/src/components/Attachments/AttachmentCarouselPager/index.js
new file mode 100644
index 000000000000..8a31e85bc3ee
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarouselPager/index.js
@@ -0,0 +1,175 @@
+/* eslint-disable es/no-optional-chaining */
+import React, {useRef, useState, useImperativeHandle} from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import {GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-handler';
+import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated';
+import PagerView from 'react-native-pager-view';
+import _ from 'underscore';
+import styles from '../../styles/styles';
+import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
+
+const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
+
+function usePageScrollHandler(handlers, dependencies) {
+    const {context, doDependenciesDiffer} = useHandler(handlers, dependencies);
+    const subscribeForEvents = ['onPageScroll'];
+
+    return useEvent(
+        (event) => {
+            'worklet';
+
+            const {onPageScroll} = handlers;
+            if (onPageScroll && event.eventName.endsWith('onPageScroll')) {
+                onPageScroll(event, context);
+            }
+        },
+        subscribeForEvents,
+        doDependenciesDiffer,
+    );
+}
+
+const noopWorklet = () => {
+    'worklet';
+
+    // noop
+};
+
+const pagerPropTypes = {
+    items: PropTypes.arrayOf(
+        PropTypes.shape({
+            key: PropTypes.string,
+            url: PropTypes.string,
+        }),
+    ).isRequired,
+    renderItem: PropTypes.func.isRequired,
+    initialIndex: PropTypes.number,
+    onPageSelected: PropTypes.func,
+    onTap: PropTypes.func,
+    onSwipe: PropTypes.func,
+    onSwipeSuccess: PropTypes.func,
+    onSwipeDown: PropTypes.func,
+    onPinchGestureChange: PropTypes.func,
+    forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+    containerWidth: PropTypes.number.isRequired,
+    containerHeight: PropTypes.number.isRequired,
+};
+
+const pagerDefaultProps = {
+    initialIndex: 0,
+    onPageSelected: () => {},
+    onTap: () => {},
+    onSwipe: noopWorklet,
+    onSwipeSuccess: () => {},
+    onSwipeDown: () => {},
+    onPinchGestureChange: () => {},
+    forwardedRef: null,
+};
+
+function AttachmentCarouselPager({
+    items,
+    renderItem,
+    initialIndex,
+    onPageSelected,
+    onTap,
+    onSwipe = noopWorklet,
+    onSwipeSuccess,
+    onSwipeDown,
+    onPinchGestureChange,
+    forwardedRef,
+    containerWidth,
+    containerHeight,
+}) {
+    const shouldPagerScroll = useSharedValue(true);
+    const pagerRef = useRef(null);
+
+    const isScrolling = useSharedValue(false);
+    const activeIndex = useSharedValue(initialIndex);
+
+    const pageScrollHandler = usePageScrollHandler(
+        {
+            onPageScroll: (e) => {
+                'worklet';
+
+                activeIndex.value = e.position;
+                isScrolling.value = e.offset !== 0;
+            },
+        },
+        [],
+    );
+
+    const [activePage, setActivePage] = useState(initialIndex);
+
+    // we use reanimated for this since onPageSelected is called
+    // in the middle of the pager animation
+    useAnimatedReaction(
+        () => isScrolling.value,
+        (stillScrolling) => {
+            if (stillScrolling) {
+                return;
+            }
+
+            runOnJS(setActivePage)(activeIndex.value);
+        },
+    );
+
+    useImperativeHandle(
+        forwardedRef,
+        () => ({
+            setPage: (...props) => pagerRef.current.setPage(...props),
+        }),
+        [],
+    );
+
+    const animatedProps = useAnimatedProps(() => ({
+        scrollEnabled: shouldPagerScroll.value,
+    }));
+
+    return (
+        <GestureHandlerRootView style={styles.flex1}>
+            <AttachmentCarouselPagerContext.Provider
+                value={{
+                    canvasWidth: containerWidth,
+                    canvasHeight: containerHeight,
+                    isScrolling,
+                    pagerRef,
+                    shouldPagerScroll,
+                    onPinchGestureChange,
+                    onTap,
+                    onSwipe,
+                    onSwipeSuccess,
+                    onSwipeDown,
+                }}
+            >
+                <AnimatedPagerView
+                    pageMargin={40}
+                    onPageScroll={pageScrollHandler}
+                    animatedProps={animatedProps}
+                    onPageSelected={onPageSelected}
+                    ref={pagerRef}
+                    style={styles.flex1}
+                    initialPage={initialIndex}
+                >
+                    {_.map(items, (item, index) => (
+                        <View
+                            key={item.key}
+                            style={styles.flex1}
+                        >
+                            {renderItem({item, index, isActive: index === activePage})}
+                        </View>
+                    ))}
+                </AnimatedPagerView>
+            </AttachmentCarouselPagerContext.Provider>
+        </GestureHandlerRootView>
+    );
+}
+AttachmentCarouselPager.propTypes = pagerPropTypes;
+AttachmentCarouselPager.defaultProps = pagerDefaultProps;
+
+export default React.forwardRef((props, ref) => (
+    <AttachmentCarouselPager
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        {...props}
+        forwardedRef={ref}
+    />
+));
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
new file mode 100755
index 000000000000..87903f7da156
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
@@ -0,0 +1,61 @@
+import React, {memo} from 'react';
+import PropTypes from 'prop-types';
+import styles from '../../../../styles/styles';
+import ImageView from '../../../ImageView';
+import withLocalize, {withLocalizePropTypes} from '../../../withLocalize';
+import compose from '../../../../libs/compose';
+import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
+import CONST from '../../../../CONST';
+import * as AttachmentsPropTypes from '../../propTypes';
+
+const propTypes = {
+    item: AttachmentsPropTypes.attachmentPropType,
+
+    /** Function for handle on press */
+    onPress: PropTypes.func,
+
+    /** Handles scale changed event */
+    onScaleChanged: PropTypes.func,
+
+    ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+    item: {
+        isAuthTokenRequired: false,
+        file: {
+            name: '',
+        },
+    },
+    onPress: undefined,
+    onScaleChanged: () => {},
+};
+
+function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChanged, translate}) {
+    const children = (
+        <ImageView
+            onScaleChanged={onScaleChanged}
+            url={item.source}
+            fileName={item.file.name}
+            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+        />
+    );
+    return onPress ? (
+        <PressableWithoutFeedback
+            onPress={onPress}
+            disabled={loadComplete}
+            style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
+            accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+            accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
+        >
+            {children}
+        </PressableWithoutFeedback>
+    ) : (
+        children
+    );
+}
+
+AttachmentViewImage.propTypes = propTypes;
+AttachmentViewImage.defaultProps = defaultProps;
+
+export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
new file mode 100755
index 000000000000..a8bfa19cf3d9
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -0,0 +1,64 @@
+import React, {memo} from 'react';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+import ImageView from '../../ImageView';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import compose from '../../../libs/compose';
+import PressableWithoutFeedback from '../../Pressable/PressableWithoutFeedback';
+import CONST from '../../../CONST';
+import AttachmentCarouselPage from '../../AttachmentCarouselPager/AttachmentCarouselPage';
+import * as AttachmentsPropTypes from '../../Attachments/propTypes';
+
+const propTypes = {
+    item: AttachmentsPropTypes.attachmentPropType,
+
+    /** Function for handle on press */
+    onPress: PropTypes.func,
+
+    /** Handles scale changed event */
+    onScaleChanged: PropTypes.func,
+
+    ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+    item: {
+        isAuthTokenRequired: false,
+        file: {
+            name: '',
+        },
+    },
+    onPress: undefined,
+    onScaleChanged: () => {},
+};
+
+function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChanged, translate}) {
+    const children = (
+        <AttachmentCarouselPage>
+            <ImageView
+                onScaleChanged={onScaleChanged}
+                url={item.source}
+                fileName={item.file.name}
+                isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+            />
+        </AttachmentCarouselPage>
+    );
+    return onPress ? (
+        <PressableWithoutFeedback
+            onPress={onPress}
+            disabled={loadComplete}
+            style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
+            accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+            accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
+        >
+            {children}
+        </PressableWithoutFeedback>
+    ) : (
+        children
+    );
+}
+
+AttachmentViewImage.propTypes = propTypes;
+AttachmentViewImage.defaultProps = defaultProps;
+
+export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
new file mode 100755
index 000000000000..527413360ed0
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -0,0 +1,146 @@
+import React, {memo, useState} from 'react';
+import {View, ActivityIndicator} from 'react-native';
+import _ from 'underscore';
+import PropTypes from 'prop-types';
+import Str from 'expensify-common/lib/str';
+import styles from '../../../styles/styles';
+import PDFView from '../../PDFView';
+import Icon from '../../Icon';
+import * as Expensicons from '../../Icon/Expensicons';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import compose from '../../../libs/compose';
+import Text from '../../Text';
+import Tooltip from '../../Tooltip';
+import themeColors from '../../../styles/themes/default';
+import variables from '../../../styles/variables';
+import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
+import AttachmentViewImage from './AttachmentViewImage';
+import * as AttachmentsPropTypes from '../propTypes';
+
+const propTypes = {
+    /** The attachment to display */
+    item: AttachmentsPropTypes.attachmentPropType,
+
+    /** Flag to show/hide download icon */
+    shouldShowDownloadIcon: PropTypes.bool,
+
+    /** Flag to show the loading indicator */
+    shouldShowLoadingSpinnerIcon: PropTypes.bool,
+
+    /** Whether this view is the active screen  */
+    isFocused: PropTypes.bool,
+
+    /** Function for handle on press */
+    onPress: PropTypes.func,
+
+    /** Handles scale changed event */
+    onScaleChanged: PropTypes.func,
+
+    /** Notify parent that the UI should be modified to accommodate keyboard */
+    onToggleKeyboard: PropTypes.func,
+
+    /** Extra styles to pass to View wrapper */
+    // eslint-disable-next-line react/forbid-prop-types
+    containerStyles: PropTypes.arrayOf(PropTypes.object),
+
+    ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+    item: {
+        isAuthTokenRequired: false,
+        file: {
+            name: '',
+        },
+    },
+    shouldShowDownloadIcon: false,
+    shouldShowLoadingSpinnerIcon: false,
+    onPress: undefined,
+    onScaleChanged: () => {},
+    onToggleKeyboard: () => {},
+    containerStyles: [],
+    isFocused: false,
+};
+
+function AttachmentView({item, onPress, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, onScaleChanged, onToggleKeyboard, translate, isFocused}) {
+    const [loadComplete, setLoadComplete] = useState(false);
+
+    // Handles case where source is a component (ex: SVG)
+    if (_.isFunction(item.source)) {
+        return (
+            <Icon
+                src={item.source}
+                height={variables.defaultAvatarPreviewSize}
+                width={variables.defaultAvatarPreviewSize}
+            />
+        );
+    }
+
+    // Check both source and file.name since PDFs dragged into the the text field
+    // will appear with a source that is a blob
+    if (Str.isPDF(item.source) || (item.file && Str.isPDF(item.file.name || translate('attachmentView.unknownFilename')))) {
+        const sourceURL = item.isAuthTokenRequired ? addEncryptedAuthTokenToURL(item.source) : item.source;
+        return (
+            <PDFView
+                onPress={onPress}
+                isFocused={isFocused}
+                sourceURL={sourceURL}
+                fileName={item.file.name}
+                style={styles.imageModalPDF}
+                onToggleKeyboard={onToggleKeyboard}
+                onScaleChanged={onScaleChanged}
+                onLoadComplete={() => !loadComplete && setLoadComplete(true)}
+            />
+        );
+    }
+
+    // For this check we use both source and file.name since temporary file source is a blob
+    // both PDFs and images will appear as images when pasted into the the text field
+    const isImage = Str.isImage(item.source);
+    if (isImage || (item.file && Str.isImage(item.file.name))) {
+        return (
+            <AttachmentViewImage
+                loadComplete={loadComplete}
+                onPress={onPress}
+                isImage={isImage}
+                file={item.file}
+                source={item.source}
+                isAuthTokenRequired={item.isAuthTokenRequired}
+                onScaleChanged={onScaleChanged}
+            />
+        );
+    }
+
+    return (
+        <View style={[styles.defaultAttachmentView, ...containerStyles]}>
+            <View style={styles.mr2}>
+                <Icon src={Expensicons.Paperclip} />
+            </View>
+
+            <Text style={[styles.textStrong, styles.flexShrink1, styles.breakAll, styles.flexWrap, styles.mw100]}>{item.file && item.file.name}</Text>
+            {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
+                <Tooltip text={translate('common.download')}>
+                    <View style={styles.ml2}>
+                        <Icon src={Expensicons.Download} />
+                    </View>
+                </Tooltip>
+            )}
+            {shouldShowLoadingSpinnerIcon && (
+                <View style={styles.ml2}>
+                    <Tooltip text={translate('common.downloading')}>
+                        <ActivityIndicator
+                            size="small"
+                            color={themeColors.textSupporting}
+                        />
+                    </Tooltip>
+                </View>
+            )}
+        </View>
+    );
+}
+
+AttachmentView.propTypes = propTypes;
+AttachmentView.defaultProps = defaultProps;
+AttachmentView.displayName = 'AttachmentView';
+
+export default compose(memo, withLocalize)(AttachmentView);
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
new file mode 100644
index 000000000000..0abbaa1254e6
--- /dev/null
+++ b/src/components/Attachments/propTypes.js
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types';
+
+const attachmentPropType = PropTypes.shape({
+    /** Whether source url requires authentication */
+    isAuthTokenRequired: PropTypes.bool,
+
+    /** URL to full-sized attachment or SVG function */
+    source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
+
+    /** File object maybe be instance of File or Object */
+    file: PropTypes.shape({
+        name: PropTypes.string,
+    }),
+});
+
+const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
+
+export {attachmentPropType, attachmentsPropType};

From cad059e51b9f818dcd2df10bf26c8137349893d3 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:25:31 +0200
Subject: [PATCH 062/133] fix: imports

---
 .../AttachmentViewImage/index.native.js            | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
index a8bfa19cf3d9..ccad68d148fc 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -1,13 +1,13 @@
 import React, {memo} from 'react';
 import PropTypes from 'prop-types';
-import styles from '../../../styles/styles';
-import ImageView from '../../ImageView';
-import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
-import compose from '../../../libs/compose';
-import PressableWithoutFeedback from '../../Pressable/PressableWithoutFeedback';
-import CONST from '../../../CONST';
+import styles from '../../../../styles/styles';
+import ImageView from '../../../ImageView';
+import withLocalize, {withLocalizePropTypes} from '../../../withLocalize';
+import compose from '../../../../libs/compose';
+import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
+import CONST from '../../../../CONST';
 import AttachmentCarouselPage from '../../AttachmentCarouselPager/AttachmentCarouselPage';
-import * as AttachmentsPropTypes from '../../Attachments/propTypes';
+import * as AttachmentsPropTypes from '../../propTypes';
 
 const propTypes = {
     item: AttachmentsPropTypes.attachmentPropType,

From 38b22809d6291c836f4042401bc70752f8a5905c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:43:21 +0200
Subject: [PATCH 063/133] extract more propTypes

---
 .../AttachmentViewImage/index.js              | 25 ++-------
 .../AttachmentViewImage/index.native.js       | 51 +++++++------------
 .../Attachments/AttachmentView/index.js       | 24 ++-------
 .../Attachments/AttachmentView/propTypes.js   | 30 +++++++++++
 4 files changed, 56 insertions(+), 74 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentView/propTypes.js

diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
index 87903f7da156..972c98a3a43d 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
@@ -1,36 +1,17 @@
 import React, {memo} from 'react';
-import PropTypes from 'prop-types';
 import styles from '../../../../styles/styles';
 import ImageView from '../../../ImageView';
 import withLocalize, {withLocalizePropTypes} from '../../../withLocalize';
 import compose from '../../../../libs/compose';
 import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
 import CONST from '../../../../CONST';
-import * as AttachmentsPropTypes from '../../propTypes';
+import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
 
 const propTypes = {
-    item: AttachmentsPropTypes.attachmentPropType,
-
-    /** Function for handle on press */
-    onPress: PropTypes.func,
-
-    /** Handles scale changed event */
-    onScaleChanged: PropTypes.func,
-
+    ...attachmentViewPropTypes,
     ...withLocalizePropTypes,
 };
 
-const defaultProps = {
-    item: {
-        isAuthTokenRequired: false,
-        file: {
-            name: '',
-        },
-    },
-    onPress: undefined,
-    onScaleChanged: () => {},
-};
-
 function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChanged, translate}) {
     const children = (
         <ImageView
@@ -56,6 +37,6 @@ function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChang
 }
 
 AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = defaultProps;
+AttachmentViewImage.defaultProps = attachmentViewDefaultProps;
 
 export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
index ccad68d148fc..40443bf5ba81 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -1,48 +1,33 @@
 import React, {memo} from 'react';
-import PropTypes from 'prop-types';
 import styles from '../../../../styles/styles';
-import ImageView from '../../../ImageView';
 import withLocalize, {withLocalizePropTypes} from '../../../withLocalize';
+import ImageView from '../../../ImageView';
 import compose from '../../../../libs/compose';
 import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
 import CONST from '../../../../CONST';
 import AttachmentCarouselPage from '../../AttachmentCarouselPager/AttachmentCarouselPage';
-import * as AttachmentsPropTypes from '../../propTypes';
+import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
 
 const propTypes = {
-    item: AttachmentsPropTypes.attachmentPropType,
-
-    /** Function for handle on press */
-    onPress: PropTypes.func,
-
-    /** Handles scale changed event */
-    onScaleChanged: PropTypes.func,
-
+    ...attachmentViewPropTypes,
     ...withLocalizePropTypes,
 };
 
-const defaultProps = {
-    item: {
-        isAuthTokenRequired: false,
-        file: {
-            name: '',
-        },
-    },
-    onPress: undefined,
-    onScaleChanged: () => {},
-};
-
-function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChanged, translate}) {
-    const children = (
-        <AttachmentCarouselPage>
-            <ImageView
-                onScaleChanged={onScaleChanged}
-                url={item.source}
-                fileName={item.file.name}
-                isAuthTokenRequired={isImage && item.isAuthTokenRequired}
-            />
-        </AttachmentCarouselPage>
+function AttachmentViewImage({item, loadComplete, onPress, isImage, isUsedInCarousel, onScaleChanged, translate}) {
+    const children = isUsedInCarousel ? (
+        <AttachmentCarouselPage
+            item={item}
+            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+        />
+    ) : (
+        <ImageView
+            onScaleChanged={onScaleChanged}
+            url={item.source}
+            fileName={item.file.name}
+            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+        />
     );
+
     return onPress ? (
         <PressableWithoutFeedback
             onPress={onPress}
@@ -59,6 +44,6 @@ function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChang
 }
 
 AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = defaultProps;
+AttachmentViewImage.defaultProps = attachmentViewDefaultProps;
 
 export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index 527413360ed0..10fb10b61907 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -15,11 +15,12 @@ import themeColors from '../../../styles/themes/default';
 import variables from '../../../styles/variables';
 import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import AttachmentViewImage from './AttachmentViewImage';
-import * as AttachmentsPropTypes from '../propTypes';
+
+import {attachmentViewPropTypes, attachmentViewDefaultProps} from './propTypes';
 
 const propTypes = {
-    /** The attachment to display */
-    item: AttachmentsPropTypes.attachmentPropType,
+    ...attachmentViewPropTypes,
+    ...withLocalizePropTypes,
 
     /** Flag to show/hide download icon */
     shouldShowDownloadIcon: PropTypes.bool,
@@ -30,33 +31,18 @@ const propTypes = {
     /** Whether this view is the active screen  */
     isFocused: PropTypes.bool,
 
-    /** Function for handle on press */
-    onPress: PropTypes.func,
-
-    /** Handles scale changed event */
-    onScaleChanged: PropTypes.func,
-
     /** Notify parent that the UI should be modified to accommodate keyboard */
     onToggleKeyboard: PropTypes.func,
 
     /** Extra styles to pass to View wrapper */
     // eslint-disable-next-line react/forbid-prop-types
     containerStyles: PropTypes.arrayOf(PropTypes.object),
-
-    ...withLocalizePropTypes,
 };
 
 const defaultProps = {
-    item: {
-        isAuthTokenRequired: false,
-        file: {
-            name: '',
-        },
-    },
+    ...attachmentViewDefaultProps,
     shouldShowDownloadIcon: false,
     shouldShowLoadingSpinnerIcon: false,
-    onPress: undefined,
-    onScaleChanged: () => {},
     onToggleKeyboard: () => {},
     containerStyles: [],
     isFocused: false,
diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js
new file mode 100644
index 000000000000..4e21d10fc5cd
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/propTypes.js
@@ -0,0 +1,30 @@
+import PropTypes from 'prop-types';
+import * as AttachmentsPropTypes from '../propTypes';
+
+const attachmentViewPropTypes = {
+    /** Attachment to display */
+    item: AttachmentsPropTypes.attachmentPropType,
+
+    /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
+    isUsedInCarousel: PropTypes.bool,
+
+    /** Function for handle on press */
+    onPress: PropTypes.func,
+
+    /** Handles scale changed event */
+    onScaleChanged: PropTypes.func,
+};
+
+const attachmentViewDefaultProps = {
+    item: {
+        isAuthTokenRequired: false,
+        file: {
+            name: '',
+        },
+    },
+    isUsedInCarousel: false,
+    onPress: undefined,
+    onScaleChanged: () => {},
+};
+
+export {attachmentViewPropTypes, attachmentViewDefaultProps};

From d58e3c9ee4a4c8c6efab0326ee249927097f945e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:43:33 +0200
Subject: [PATCH 064/133] add "isUsedInCarousel" prop

---
 .../AttachmentCarousel/AttachmentCarouselView/index.js           | 1 +
 .../AttachmentCarousel/AttachmentCarouselView/index.native.js    | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
index 37ccd2f3b71c..3d1093c492fb 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -119,6 +119,7 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
             <AttachmentView
                 item={item}
                 isFocused={activeSource === item.source}
+                isUsedInCarousel
             />
         ),
         [activeSource],
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
index df835b300784..3b0c325d67c3 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -92,6 +92,7 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
             <AttachmentView
                 item={item}
                 isFocused={activeSource === item.source}
+                isUsedInCarousel
             />
         ),
         [activeSource],

From 003268be15d9076febcdf24d01857f1d153ca02e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:43:46 +0200
Subject: [PATCH 065/133] split up AttachmentCarouselPage logic

---
 .../AttachmentCarouselPage.js                 | 580 +-----------------
 .../ImageTransformer.js                       | 566 +++++++++++++++++
 .../AttachmentCarouselPager/ImageWrapper.js   |  24 +
 .../AttachmentCarouselPager/index.js          |   2 +-
 4 files changed, 595 insertions(+), 577 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js
 create mode 100644 src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js

diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
index daa198e63f02..3839d96304b2 100644
--- a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
@@ -1,583 +1,11 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {useContext, useEffect, useRef, useState, useMemo} from 'react';
+import React, {useContext, useEffect, useState, useMemo} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
-import {Gesture, GestureDetector} from 'react-native-gesture-handler';
-import Animated, {
-    cancelAnimation,
-    runOnJS,
-    runOnUI,
-    useAnimatedReaction,
-    useAnimatedStyle,
-    useDerivedValue,
-    useSharedValue,
-    useWorkletCallback,
-    withDecay,
-    withSpring,
-} from 'react-native-reanimated';
-import Image from '../Image';
-import styles from '../../styles/styles';
+import Image from '../../Image';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
-
-const DOUBLE_TAP_SCALE = 3;
-const MAX_SCALE = 20;
-const MIN_SCALE = 0.7;
-
-const SPRING_CONFIG = {
-    mass: 3,
-    stiffness: 1000,
-    damping: 500,
-};
-
-function clamp(value, lowerBound, upperBound) {
-    'worklet';
-
-    return Math.min(Math.max(lowerBound, value), upperBound);
-}
-
-const imageWrapperPropTypes = {
-    children: PropTypes.node.isRequired,
-};
-
-function ImageWrapper({children}) {
-    return (
-        <Animated.View
-            collapsable={false}
-            style={[StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter]}
-        >
-            {children}
-        </Animated.View>
-    );
-}
-ImageWrapper.propTypes = imageWrapperPropTypes;
-
-const imageTransformerPropTypes = {
-    imageWidth: PropTypes.number,
-    imageHeight: PropTypes.number,
-    imageScale: PropTypes.number,
-    scaledImageWidth: PropTypes.number,
-    scaledImageHeight: PropTypes.number,
-    isActive: PropTypes.bool.isRequired,
-    children: PropTypes.node.isRequired,
-};
-
-const imageTransformerDefaultProps = {
-    imageWidth: 0,
-    imageHeight: 0,
-    imageScale: 1,
-    scaledImageWidth: 0,
-    scaledImageHeight: 0,
-};
-
-function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
-    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
-
-    const zoomScale = useSharedValue(1);
-    // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
-    // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + imageScale - 1, 0), [imageScale]);
-
-    const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
-    const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
-
-    // used for pan gesture
-    const translateY = useSharedValue(0);
-    const translateX = useSharedValue(0);
-    const offsetX = useSharedValue(0);
-    const offsetY = useSharedValue(0);
-    const isSwiping = useSharedValue(false);
-
-    // used for moving fingers when pinching
-    const scaleTranslateX = useSharedValue(0);
-    const scaleTranslateY = useSharedValue(0);
-
-    // storage for the the origin of the gesture
-    const origin = {
-        x: useSharedValue(0),
-        y: useSharedValue(0),
-    };
-
-    // storage for the pan velocity to calculate the decay
-    const panVelocityX = useSharedValue(0);
-    const panVelocityY = useSharedValue(0);
-
-    // store scale in between gestures
-    const scaleOffset = useSharedValue(1);
-
-    // disable pan vertically when image is smaller than screen
-    const canPanVertically = useDerivedValue(() => canvasHeight < zoomScaledImageHeight.value, [canvasHeight]);
-
-    // calculates bounds of the scaled image
-    // can we pan left/right/up/down
-    // can be used to limit gesture or implementing tension effect
-    const getBounds = useWorkletCallback(() => {
-        const rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2;
-
-        let topBoundary = 0;
-
-        if (canvasHeight < zoomScaledImageHeight.value) {
-            topBoundary = Math.abs(zoomScaledImageHeight.value - canvasHeight) / 2;
-        }
-
-        const maxVector = {x: rightBoundary, y: topBoundary};
-        const minVector = {x: -rightBoundary, y: -topBoundary};
-
-        const target = {
-            x: clamp(offsetX.value, minVector.x, maxVector.x),
-            y: clamp(offsetY.value, minVector.y, maxVector.y),
-        };
-
-        const isInBoundaryX = target.x === offsetX.value;
-        const isInBoundaryY = target.y === offsetY.value;
-
-        return {
-            target,
-            isInBoundaryX,
-            isInBoundaryY,
-            minVector,
-            maxVector,
-            canPanLeft: target.x < maxVector.x,
-            canPanRight: target.x > minVector.x,
-        };
-    }, [canvasWidth, canvasHeight]);
-
-    const afterGesture = useWorkletCallback(() => {
-        const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
-
-        if (!canPanVertically.value) {
-            offsetY.value = withSpring(target.y, SPRING_CONFIG);
-        }
-
-        if (
-            zoomScale.value === 1 &&
-            offsetX.value === 0 &&
-            offsetY.value === 0 &&
-            translateX.value === 0 &&
-            translateY.value === 0 &&
-            scaleTranslateX.value === 0 &&
-            scaleTranslateY.value === 0
-        ) {
-            // we don't need to run any animations
-            return;
-        }
-
-        if (zoomScale.value <= 1) {
-            // just center it
-            // reset(true);
-            offsetX.value = withSpring(0, SPRING_CONFIG);
-            offsetY.value = withSpring(0, SPRING_CONFIG);
-            return;
-        }
-
-        const deceleration = 0.9915;
-
-        if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_SCALE) {
-                offsetX.value = withDecay({
-                    velocity: panVelocityX.value,
-                    clamp: [minVector.x, maxVector.x],
-                    deceleration,
-                    rubberBandEffect: false,
-                });
-            }
-        } else {
-            offsetX.value = withSpring(target.x, SPRING_CONFIG);
-        }
-
-        if (isInBoundaryY) {
-            if (
-                Math.abs(panVelocityY.value) > 0 &&
-                zoomScale.value <= MAX_SCALE &&
-                // limit vertical pan only when image is smaller than screen
-                offsetY.value !== minVector.y &&
-                offsetY.value !== maxVector.y
-            ) {
-                offsetY.value = withDecay({
-                    velocity: panVelocityY.value,
-                    clamp: [minVector.y, maxVector.y],
-                    deceleration,
-                });
-            }
-        } else {
-            offsetY.value = withSpring(target.y, SPRING_CONFIG, () => {
-                isSwiping.value = false;
-            });
-        }
-    });
-
-    const stopAnimation = useWorkletCallback(() => {
-        cancelAnimation(offsetX);
-        cancelAnimation(offsetY);
-    });
-
-    const zoomToCoordinates = useWorkletCallback(
-        (x, y) => {
-            'worklet';
-
-            stopAnimation();
-
-            const usableImage = {
-                x: scaledImageWidth,
-                y: scaledImageHeight,
-            };
-
-            const targetImageSize = {
-                x: usableImage.x * DOUBLE_TAP_SCALE,
-                y: usableImage.y * DOUBLE_TAP_SCALE,
-            };
-
-            const CENTER = {
-                x: canvasWidth / 2,
-                y: canvasHeight / 2,
-            };
-
-            const imageCenter = {
-                x: usableImage.x / 2,
-                y: usableImage.y / 2,
-            };
-
-            const focal = {x, y};
-
-            const currentOrigin = {
-                x: (targetImageSize.x / 2 - CENTER.x) * -1,
-                y: (targetImageSize.y / 2 - CENTER.y) * -1,
-            };
-
-            const koef = {
-                x: (1 / imageCenter.x) * focal.x - 1,
-                y: (1 / imageCenter.y) * focal.y - 1,
-            };
-
-            const target = {
-                x: currentOrigin.x * koef.x,
-                y: currentOrigin.y * koef.y,
-            };
-
-            if (targetImageSize.y < canvasHeight) {
-                target.y = 0;
-            }
-
-            offsetX.value = withSpring(target.x, SPRING_CONFIG);
-            offsetY.value = withSpring(target.y, SPRING_CONFIG);
-            zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
-            scaleOffset.value = DOUBLE_TAP_SCALE;
-        },
-        [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight],
-    );
-
-    const reset = useWorkletCallback((animated) => {
-        scaleOffset.value = 1;
-
-        stopAnimation();
-
-        if (animated) {
-            offsetX.value = withSpring(0, SPRING_CONFIG);
-            offsetY.value = withSpring(0, SPRING_CONFIG);
-            zoomScale.value = withSpring(1, SPRING_CONFIG);
-        } else {
-            zoomScale.value = 1;
-            translateX.value = 0;
-            translateY.value = 0;
-            offsetX.value = 0;
-            offsetY.value = 0;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
-        }
-    });
-
-    const doubleTap = Gesture.Tap()
-        .numberOfTaps(2)
-        .maxDelay(150)
-        .maxDistance(20)
-        .onEnd((evt) => {
-            if (zoomScale.value > 1) {
-                reset(true);
-            } else {
-                zoomToCoordinates(evt.x, evt.y);
-            }
-        });
-
-    const panGestureRef = useRef(Gesture.Pan());
-
-    const singleTap = Gesture.Tap()
-        .numberOfTaps(1)
-        .maxDuration(50)
-        .requireExternalGestureToFail(doubleTap, panGestureRef)
-        .onBegin(() => {
-            stopAnimation();
-        })
-        .onFinalize((evt, success) => {
-            if (!success || !onTap) return;
-
-            runOnJS(onTap)();
-        });
-
-    const previousTouch = useSharedValue(null);
-
-    const panGesture = Gesture.Pan()
-        .manualActivation(true)
-        .averageTouches(true)
-        .onTouchesMove((evt, state) => {
-            if (zoomScale.value > 1) {
-                state.activate();
-            }
-
-            // TODO: Swipe down to close carousel gesture
-            // this needs fine tuning to work properly
-            // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
-            //     const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x);
-            //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
-
-            //     // TODO: this needs tuning
-            //     if (Math.abs(velocityY) > velocityX && velocityY > 20) {
-            //         state.activate();
-
-            //         isSwiping.value = true;
-            //         previousTouch.value = null;
-
-            //         runOnJS(onSwipeDown)();
-            //         return;
-            //     }
-            // }
-
-            if (previousTouch.value == null) {
-                previousTouch.value = {
-                    x: evt.allTouches[0].x,
-                    y: evt.allTouches[0].y,
-                };
-            }
-        })
-        .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
-        .onBegin(() => {
-            stopAnimation();
-        })
-        .onChange((evt) => {
-            // since we running both pinch and pan gesture handlers simultaneously
-            // we need to make sure that we don't pan when we pinch and move fingers
-            // since we track it as pinch focal gesture
-            if (evt.numberOfPointers > 1 || isScrolling.value) {
-                return;
-            }
-
-            panVelocityX.value = evt.velocityX;
-
-            panVelocityY.value = evt.velocityY;
-
-            if (!isSwiping.value) {
-                translateX.value += evt.changeX;
-            }
-
-            if (canPanVertically.value || isSwiping.value) {
-                translateY.value += evt.changeY;
-            }
-        })
-        .onEnd((evt) => {
-            previousTouch.value = null;
-
-            if (isScrolling.value) {
-                return;
-            }
-
-            offsetX.value += translateX.value;
-            offsetY.value += translateY.value;
-            translateX.value = 0;
-            translateY.value = 0;
-
-            if (isSwiping.value) {
-                const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
-                const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
-
-                if (enoughVelocity && rightDirection) {
-                    const maybeInvert = (v) => {
-                        const invert = evt.velocityY < 0;
-                        return invert ? -v : v;
-                    };
-
-                    offsetY.value = withSpring(
-                        maybeInvert(imageHeight * 2),
-                        {
-                            stiffness: 50,
-                            damping: 30,
-                            mass: 1,
-                            overshootClamping: true,
-                            restDisplacementThreshold: 300,
-                            restSpeedThreshold: 300,
-                            velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
-                        },
-                        () => {
-                            runOnJS(onSwipeSuccess)();
-                        },
-                    );
-                    return;
-                }
-            }
-
-            afterGesture();
-
-            panVelocityX.value = 0;
-            panVelocityY.value = 0;
-        })
-        .withRef(panGestureRef);
-
-    const getAdjustedFocal = useWorkletCallback(
-        (focalX, focalY) => ({
-            x: focalX - (canvasWidth / 2 + offsetX.value),
-            y: focalY - (canvasHeight / 2 + offsetY.value),
-        }),
-        [canvasWidth, canvasHeight],
-    );
-
-    // used to store event scale value when we limit scale
-    const gestureScale = useSharedValue(1);
-
-    const pinchGestureRunning = useSharedValue(false);
-    const pinchGesture = Gesture.Pinch()
-        .onTouchesDown((evt, state) => {
-            // we don't want to activate pinch gesture when we are scrolling pager
-            if (!isScrolling.value) return;
-
-            state.fail();
-        })
-        .simultaneousWithExternalGesture(panGesture, doubleTap)
-        .onStart((evt) => {
-            pinchGestureRunning.value = true;
-
-            stopAnimation();
-
-            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-            origin.x.value = adjustFocal.x;
-            origin.y.value = adjustFocal.y;
-        })
-        .onChange((evt) => {
-            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
-
-            if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
-                gestureScale.value = evt.scale;
-            }
-
-            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
-
-            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
-            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
-        })
-        .onEnd(() => {
-            offsetX.value += scaleTranslateX.value;
-            offsetY.value += scaleTranslateY.value;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
-            scaleOffset.value = zoomScale.value;
-            gestureScale.value = 1;
-
-            if (scaleOffset.value < 1) {
-                // make sure we don't add stuff below the 1
-                scaleOffset.value = 1;
-
-                // this runs the timing animation
-                zoomScale.value = withSpring(1, SPRING_CONFIG);
-            } else if (scaleOffset.value > MAX_SCALE) {
-                scaleOffset.value = MAX_SCALE;
-                zoomScale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
-            }
-
-            afterGesture();
-
-            pinchGestureRunning.value = false;
-        });
-
-    const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false);
-    useAnimatedReaction(
-        () => [zoomScale.value, pinchGestureRunning.value],
-        ([zoom, running]) => {
-            const newIsPinchGestureInUse = zoom !== 1 || running;
-            if (isPinchGestureInUse !== newIsPinchGestureInUse) {
-                runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse);
-            }
-        },
-    );
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]);
-
-    const animatedStyles = useAnimatedStyle(() => {
-        const x = scaleTranslateX.value + translateX.value + offsetX.value;
-        const y = scaleTranslateY.value + translateY.value + offsetY.value;
-
-        if (isSwiping.value) {
-            onSwipe(y);
-        }
-
-        return {
-            transform: [
-                {
-                    translateX: x,
-                },
-                {
-                    translateY: y,
-                },
-                {scale: totalScale.value},
-            ],
-        };
-    }, []);
-
-    // reacts to scale change and enables/disables pager scroll
-    useAnimatedReaction(
-        () => zoomScale.value,
-        () => {
-            shouldPagerScroll.value = zoomScale.value === 1;
-        },
-    );
-
-    const mounted = useRef(false);
-    useEffect(() => {
-        if (!mounted.current) {
-            mounted.current = true;
-            return;
-        }
-
-        if (!isActive) {
-            runOnUI(reset)(false);
-        }
-    }, [isActive, mounted, reset]);
-
-    return (
-        <View
-            collapsable={false}
-            style={[
-                styles.flex1,
-                {
-                    width: canvasWidth,
-                },
-            ]}
-        >
-            <GestureDetector gesture={pinchGesture}>
-                <Animated.View
-                    collapsable
-                    style={StyleSheet.absoluteFill}
-                >
-                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
-                        <Animated.View
-                            collapsable
-                            style={StyleSheet.absoluteFill}
-                        >
-                            <ImageWrapper>
-                                <GestureDetector gesture={doubleTap}>
-                                    <Animated.View
-                                        collapsable={false}
-                                        style={[animatedStyles]}
-                                    >
-                                        {children}
-                                    </Animated.View>
-                                </GestureDetector>
-                            </ImageWrapper>
-                        </Animated.View>
-                    </GestureDetector>
-                </Animated.View>
-            </GestureDetector>
-        </View>
-    );
-}
-ImageTransformer.propTypes = imageTransformerPropTypes;
-ImageTransformer.defaultProps = imageTransformerDefaultProps;
+import ImageTransformer from './ImageTransformer';
+import ImageWrapper from './ImageWrapper';
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = canvasWidth / imageWidth;
diff --git a/src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js b/src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js
new file mode 100644
index 000000000000..e6ba9d28cd43
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js
@@ -0,0 +1,566 @@
+/* eslint-disable es/no-optional-chaining */
+import React, {useContext, useEffect, useRef, useState} from 'react';
+import {StyleSheet, View} from 'react-native';
+import PropTypes from 'prop-types';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import Animated, {
+    cancelAnimation,
+    runOnJS,
+    runOnUI,
+    useAnimatedReaction,
+    useAnimatedStyle,
+    useDerivedValue,
+    useSharedValue,
+    useWorkletCallback,
+    withDecay,
+    withSpring,
+} from 'react-native-reanimated';
+import styles from '../../../styles/styles';
+import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
+import ImageWrapper from './ImageWrapper';
+
+const DOUBLE_TAP_SCALE = 3;
+const MAX_SCALE = 20;
+const MIN_SCALE = 0.7;
+
+const SPRING_CONFIG = {
+    mass: 3,
+    stiffness: 1000,
+    damping: 500,
+};
+
+function clamp(value, lowerBound, upperBound) {
+    'worklet';
+
+    return Math.min(Math.max(lowerBound, value), upperBound);
+}
+
+const imageTransformerPropTypes = {
+    imageWidth: PropTypes.number,
+    imageHeight: PropTypes.number,
+    imageScale: PropTypes.number,
+    scaledImageWidth: PropTypes.number,
+    scaledImageHeight: PropTypes.number,
+    isActive: PropTypes.bool.isRequired,
+    children: PropTypes.node.isRequired,
+};
+
+const imageTransformerDefaultProps = {
+    imageWidth: 0,
+    imageHeight: 0,
+    imageScale: 1,
+    scaledImageWidth: 0,
+    scaledImageHeight: 0,
+};
+
+function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
+    const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
+
+    const zoomScale = useSharedValue(1);
+    // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
+    // Substracting 1, because both scales have the initial image as the base reference
+    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + imageScale - 1, 0), [imageScale]);
+
+    const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
+    const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
+
+    // used for pan gesture
+    const translateY = useSharedValue(0);
+    const translateX = useSharedValue(0);
+    const offsetX = useSharedValue(0);
+    const offsetY = useSharedValue(0);
+    const isSwiping = useSharedValue(false);
+
+    // used for moving fingers when pinching
+    const scaleTranslateX = useSharedValue(0);
+    const scaleTranslateY = useSharedValue(0);
+
+    // storage for the the origin of the gesture
+    const origin = {
+        x: useSharedValue(0),
+        y: useSharedValue(0),
+    };
+
+    // storage for the pan velocity to calculate the decay
+    const panVelocityX = useSharedValue(0);
+    const panVelocityY = useSharedValue(0);
+
+    // store scale in between gestures
+    const scaleOffset = useSharedValue(1);
+
+    // disable pan vertically when image is smaller than screen
+    const canPanVertically = useDerivedValue(() => canvasHeight < zoomScaledImageHeight.value, [canvasHeight]);
+
+    // calculates bounds of the scaled image
+    // can we pan left/right/up/down
+    // can be used to limit gesture or implementing tension effect
+    const getBounds = useWorkletCallback(() => {
+        const rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2;
+
+        let topBoundary = 0;
+
+        if (canvasHeight < zoomScaledImageHeight.value) {
+            topBoundary = Math.abs(zoomScaledImageHeight.value - canvasHeight) / 2;
+        }
+
+        const maxVector = {x: rightBoundary, y: topBoundary};
+        const minVector = {x: -rightBoundary, y: -topBoundary};
+
+        const target = {
+            x: clamp(offsetX.value, minVector.x, maxVector.x),
+            y: clamp(offsetY.value, minVector.y, maxVector.y),
+        };
+
+        const isInBoundaryX = target.x === offsetX.value;
+        const isInBoundaryY = target.y === offsetY.value;
+
+        return {
+            target,
+            isInBoundaryX,
+            isInBoundaryY,
+            minVector,
+            maxVector,
+            canPanLeft: target.x < maxVector.x,
+            canPanRight: target.x > minVector.x,
+        };
+    }, [canvasWidth, canvasHeight]);
+
+    const afterGesture = useWorkletCallback(() => {
+        const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
+
+        if (!canPanVertically.value) {
+            offsetY.value = withSpring(target.y, SPRING_CONFIG);
+        }
+
+        if (
+            zoomScale.value === 1 &&
+            offsetX.value === 0 &&
+            offsetY.value === 0 &&
+            translateX.value === 0 &&
+            translateY.value === 0 &&
+            scaleTranslateX.value === 0 &&
+            scaleTranslateY.value === 0
+        ) {
+            // we don't need to run any animations
+            return;
+        }
+
+        if (zoomScale.value <= 1) {
+            // just center it
+            // reset(true);
+            offsetX.value = withSpring(0, SPRING_CONFIG);
+            offsetY.value = withSpring(0, SPRING_CONFIG);
+            return;
+        }
+
+        const deceleration = 0.9915;
+
+        if (isInBoundaryX) {
+            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_SCALE) {
+                offsetX.value = withDecay({
+                    velocity: panVelocityX.value,
+                    clamp: [minVector.x, maxVector.x],
+                    deceleration,
+                    rubberBandEffect: false,
+                });
+            }
+        } else {
+            offsetX.value = withSpring(target.x, SPRING_CONFIG);
+        }
+
+        if (isInBoundaryY) {
+            if (
+                Math.abs(panVelocityY.value) > 0 &&
+                zoomScale.value <= MAX_SCALE &&
+                // limit vertical pan only when image is smaller than screen
+                offsetY.value !== minVector.y &&
+                offsetY.value !== maxVector.y
+            ) {
+                offsetY.value = withDecay({
+                    velocity: panVelocityY.value,
+                    clamp: [minVector.y, maxVector.y],
+                    deceleration,
+                });
+            }
+        } else {
+            offsetY.value = withSpring(target.y, SPRING_CONFIG, () => {
+                isSwiping.value = false;
+            });
+        }
+    });
+
+    const stopAnimation = useWorkletCallback(() => {
+        cancelAnimation(offsetX);
+        cancelAnimation(offsetY);
+    });
+
+    const zoomToCoordinates = useWorkletCallback(
+        (x, y) => {
+            'worklet';
+
+            stopAnimation();
+
+            const usableImage = {
+                x: scaledImageWidth,
+                y: scaledImageHeight,
+            };
+
+            const targetImageSize = {
+                x: usableImage.x * DOUBLE_TAP_SCALE,
+                y: usableImage.y * DOUBLE_TAP_SCALE,
+            };
+
+            const CENTER = {
+                x: canvasWidth / 2,
+                y: canvasHeight / 2,
+            };
+
+            const imageCenter = {
+                x: usableImage.x / 2,
+                y: usableImage.y / 2,
+            };
+
+            const focal = {x, y};
+
+            const currentOrigin = {
+                x: (targetImageSize.x / 2 - CENTER.x) * -1,
+                y: (targetImageSize.y / 2 - CENTER.y) * -1,
+            };
+
+            const koef = {
+                x: (1 / imageCenter.x) * focal.x - 1,
+                y: (1 / imageCenter.y) * focal.y - 1,
+            };
+
+            const target = {
+                x: currentOrigin.x * koef.x,
+                y: currentOrigin.y * koef.y,
+            };
+
+            if (targetImageSize.y < canvasHeight) {
+                target.y = 0;
+            }
+
+            offsetX.value = withSpring(target.x, SPRING_CONFIG);
+            offsetY.value = withSpring(target.y, SPRING_CONFIG);
+            zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
+            scaleOffset.value = DOUBLE_TAP_SCALE;
+        },
+        [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight],
+    );
+
+    const reset = useWorkletCallback((animated) => {
+        scaleOffset.value = 1;
+
+        stopAnimation();
+
+        if (animated) {
+            offsetX.value = withSpring(0, SPRING_CONFIG);
+            offsetY.value = withSpring(0, SPRING_CONFIG);
+            zoomScale.value = withSpring(1, SPRING_CONFIG);
+        } else {
+            zoomScale.value = 1;
+            translateX.value = 0;
+            translateY.value = 0;
+            offsetX.value = 0;
+            offsetY.value = 0;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+        }
+    });
+
+    const doubleTap = Gesture.Tap()
+        .numberOfTaps(2)
+        .maxDelay(150)
+        .maxDistance(20)
+        .onEnd((evt) => {
+            if (zoomScale.value > 1) {
+                reset(true);
+            } else {
+                zoomToCoordinates(evt.x, evt.y);
+            }
+        });
+
+    const panGestureRef = useRef(Gesture.Pan());
+
+    const singleTap = Gesture.Tap()
+        .numberOfTaps(1)
+        .maxDuration(50)
+        .requireExternalGestureToFail(doubleTap, panGestureRef)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onFinalize((evt, success) => {
+            if (!success || !onTap) return;
+
+            runOnJS(onTap)();
+        });
+
+    const previousTouch = useSharedValue(null);
+
+    const panGesture = Gesture.Pan()
+        .manualActivation(true)
+        .averageTouches(true)
+        .onTouchesMove((evt, state) => {
+            if (zoomScale.value > 1) {
+                state.activate();
+            }
+
+            // TODO: Swipe down to close carousel gesture
+            // this needs fine tuning to work properly
+            // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) {
+            //     const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x);
+            //     const velocityY = evt.allTouches[0].y - previousTouch.value.y;
+
+            //     // TODO: this needs tuning
+            //     if (Math.abs(velocityY) > velocityX && velocityY > 20) {
+            //         state.activate();
+
+            //         isSwiping.value = true;
+            //         previousTouch.value = null;
+
+            //         runOnJS(onSwipeDown)();
+            //         return;
+            //     }
+            // }
+
+            if (previousTouch.value == null) {
+                previousTouch.value = {
+                    x: evt.allTouches[0].x,
+                    y: evt.allTouches[0].y,
+                };
+            }
+        })
+        .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap)
+        .onBegin(() => {
+            stopAnimation();
+        })
+        .onChange((evt) => {
+            // since we running both pinch and pan gesture handlers simultaneously
+            // we need to make sure that we don't pan when we pinch and move fingers
+            // since we track it as pinch focal gesture
+            if (evt.numberOfPointers > 1 || isScrolling.value) {
+                return;
+            }
+
+            panVelocityX.value = evt.velocityX;
+
+            panVelocityY.value = evt.velocityY;
+
+            if (!isSwiping.value) {
+                translateX.value += evt.changeX;
+            }
+
+            if (canPanVertically.value || isSwiping.value) {
+                translateY.value += evt.changeY;
+            }
+        })
+        .onEnd((evt) => {
+            previousTouch.value = null;
+
+            if (isScrolling.value) {
+                return;
+            }
+
+            offsetX.value += translateX.value;
+            offsetY.value += translateY.value;
+            translateX.value = 0;
+            translateY.value = 0;
+
+            if (isSwiping.value) {
+                const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY);
+                const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0);
+
+                if (enoughVelocity && rightDirection) {
+                    const maybeInvert = (v) => {
+                        const invert = evt.velocityY < 0;
+                        return invert ? -v : v;
+                    };
+
+                    offsetY.value = withSpring(
+                        maybeInvert(imageHeight * 2),
+                        {
+                            stiffness: 50,
+                            damping: 30,
+                            mass: 1,
+                            overshootClamping: true,
+                            restDisplacementThreshold: 300,
+                            restSpeedThreshold: 300,
+                            velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY,
+                        },
+                        () => {
+                            runOnJS(onSwipeSuccess)();
+                        },
+                    );
+                    return;
+                }
+            }
+
+            afterGesture();
+
+            panVelocityX.value = 0;
+            panVelocityY.value = 0;
+        })
+        .withRef(panGestureRef);
+
+    const getAdjustedFocal = useWorkletCallback(
+        (focalX, focalY) => ({
+            x: focalX - (canvasWidth / 2 + offsetX.value),
+            y: focalY - (canvasHeight / 2 + offsetY.value),
+        }),
+        [canvasWidth, canvasHeight],
+    );
+
+    // used to store event scale value when we limit scale
+    const gestureScale = useSharedValue(1);
+
+    const pinchGestureRunning = useSharedValue(false);
+    const pinchGesture = Gesture.Pinch()
+        .onTouchesDown((evt, state) => {
+            // we don't want to activate pinch gesture when we are scrolling pager
+            if (!isScrolling.value) return;
+
+            state.fail();
+        })
+        .simultaneousWithExternalGesture(panGesture, doubleTap)
+        .onStart((evt) => {
+            pinchGestureRunning.value = true;
+
+            stopAnimation();
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            origin.x.value = adjustFocal.x;
+            origin.y.value = adjustFocal.y;
+        })
+        .onChange((evt) => {
+            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+
+            if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
+                gestureScale.value = evt.scale;
+            }
+
+            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+
+            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
+            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
+        })
+        .onEnd(() => {
+            offsetX.value += scaleTranslateX.value;
+            offsetY.value += scaleTranslateY.value;
+            scaleTranslateX.value = 0;
+            scaleTranslateY.value = 0;
+            scaleOffset.value = zoomScale.value;
+            gestureScale.value = 1;
+
+            if (scaleOffset.value < 1) {
+                // make sure we don't add stuff below the 1
+                scaleOffset.value = 1;
+
+                // this runs the timing animation
+                zoomScale.value = withSpring(1, SPRING_CONFIG);
+            } else if (scaleOffset.value > MAX_SCALE) {
+                scaleOffset.value = MAX_SCALE;
+                zoomScale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
+            }
+
+            afterGesture();
+
+            pinchGestureRunning.value = false;
+        });
+
+    const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false);
+    useAnimatedReaction(
+        () => [zoomScale.value, pinchGestureRunning.value],
+        ([zoom, running]) => {
+            const newIsPinchGestureInUse = zoom !== 1 || running;
+            if (isPinchGestureInUse !== newIsPinchGestureInUse) {
+                runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse);
+            }
+        },
+    );
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]);
+
+    const animatedStyles = useAnimatedStyle(() => {
+        const x = scaleTranslateX.value + translateX.value + offsetX.value;
+        const y = scaleTranslateY.value + translateY.value + offsetY.value;
+
+        if (isSwiping.value) {
+            onSwipe(y);
+        }
+
+        return {
+            transform: [
+                {
+                    translateX: x,
+                },
+                {
+                    translateY: y,
+                },
+                {scale: totalScale.value},
+            ],
+        };
+    }, []);
+
+    // reacts to scale change and enables/disables pager scroll
+    useAnimatedReaction(
+        () => zoomScale.value,
+        () => {
+            shouldPagerScroll.value = zoomScale.value === 1;
+        },
+    );
+
+    const mounted = useRef(false);
+    useEffect(() => {
+        if (!mounted.current) {
+            mounted.current = true;
+            return;
+        }
+
+        if (!isActive) {
+            runOnUI(reset)(false);
+        }
+    }, [isActive, mounted, reset]);
+
+    return (
+        <View
+            collapsable={false}
+            style={[
+                styles.flex1,
+                {
+                    width: canvasWidth,
+                },
+            ]}
+        >
+            <GestureDetector gesture={pinchGesture}>
+                <Animated.View
+                    collapsable
+                    style={StyleSheet.absoluteFill}
+                >
+                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
+                        <Animated.View
+                            collapsable
+                            style={StyleSheet.absoluteFill}
+                        >
+                            <ImageWrapper>
+                                <GestureDetector gesture={doubleTap}>
+                                    <Animated.View
+                                        collapsable={false}
+                                        style={[animatedStyles]}
+                                    >
+                                        {children}
+                                    </Animated.View>
+                                </GestureDetector>
+                            </ImageWrapper>
+                        </Animated.View>
+                    </GestureDetector>
+                </Animated.View>
+            </GestureDetector>
+        </View>
+    );
+}
+ImageTransformer.propTypes = imageTransformerPropTypes;
+ImageTransformer.defaultProps = imageTransformerDefaultProps;
+
+export default ImageTransformer;
diff --git a/src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js b/src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js
new file mode 100644
index 000000000000..740f0d3367e4
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js
@@ -0,0 +1,24 @@
+/* eslint-disable es/no-optional-chaining */
+import React from 'react';
+import {StyleSheet} from 'react-native';
+import PropTypes from 'prop-types';
+import Animated from 'react-native-reanimated';
+import styles from '../../../styles/styles';
+
+const imageWrapperPropTypes = {
+    children: PropTypes.node.isRequired,
+};
+
+function ImageWrapper({children}) {
+    return (
+        <Animated.View
+            collapsable={false}
+            style={[StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter]}
+        >
+            {children}
+        </Animated.View>
+    );
+}
+ImageWrapper.propTypes = imageWrapperPropTypes;
+
+export default ImageWrapper;
diff --git a/src/components/Attachments/AttachmentCarouselPager/index.js b/src/components/Attachments/AttachmentCarouselPager/index.js
index 8a31e85bc3ee..7aaa53f247bc 100644
--- a/src/components/Attachments/AttachmentCarouselPager/index.js
+++ b/src/components/Attachments/AttachmentCarouselPager/index.js
@@ -6,7 +6,7 @@ import {GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-
 import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
-import styles from '../../styles/styles';
+import styles from '../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 
 const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));

From 64046b9e5d2edc914be87eaf0aaafa1df7e08ede Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:45:29 +0200
Subject: [PATCH 066/133] add isAuthTokenRequired prop to carousel page

---
 .../AttachmentCarouselPager/AttachmentCarouselPage.js        | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
index 3839d96304b2..f5f6d8051986 100644
--- a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
@@ -21,9 +21,10 @@ const pagePropTypes = {
     item: PropTypes.shape({
         url: PropTypes.string,
     }).isRequired,
+    isAuthTokenRequired: PropTypes.bool.isRequired,
 };
 
-function AttachmentCarouselPage({isActive: isActiveProp, item}) {
+function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequired}) {
     const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
     const dimensions = cachedDimensions.get(item.url);
@@ -68,6 +69,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item}) {
                         <Image
                             source={{uri: item.url}}
                             style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
+                            isAuthTokenRequired={isAuthTokenRequired}
                             onLoad={(evt) => {
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
                                 const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
@@ -97,6 +99,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item}) {
                 <ImageWrapper>
                     <Image
                         source={{uri: item.url}}
+                        isAuthTokenRequired={isAuthTokenRequired}
                         onLoad={(evt) => {
                             const imageWidth = evt.nativeEvent.width;
                             const imageHeight = evt.nativeEvent.height;

From dd6e0d52a007f5cb8ccc46ef3ee51f9ddf099e81 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 11:47:32 +0200
Subject: [PATCH 067/133] fix: imports

---
 .../AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js  | 2 +-
 src/components/AttachmentModal.js                             | 4 ++--
 src/components/Attachments/AttachmentCarousel/index.js        | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
index 8507713c887e..46576bc62e7a 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import {propTypes as anchorForAttachmentsOnlyPropTypes, defaultProps as anchorForAttachmentsOnlyDefaultProps} from './anchorForAttachmentsOnlyPropTypes';
 import CONST from '../../CONST';
 import ONYXKEYS from '../../ONYXKEYS';
-import AttachmentView from '../AttachmentView';
+import AttachmentView from '../Attachments/AttachmentView';
 import * as Download from '../../libs/actions/Download';
 import fileDownload from '../../libs/fileDownload';
 import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 4e49d89ab9de..df0e2104f49b 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -7,8 +7,8 @@ import lodashExtend from 'lodash/extend';
 import _ from 'underscore';
 import CONST from '../CONST';
 import Modal from './Modal';
-import AttachmentView from './AttachmentView/AttachmentView';
-import AttachmentCarousel from './AttachmentCarousel';
+import AttachmentView from './Attachments/AttachmentView';
+import AttachmentCarousel from './Attachments/AttachmentCarousel';
 import styles from '../styles/styles';
 import * as StyleUtils from '../styles/StyleUtils';
 import * as FileUtils from '../libs/fileDownload/FileUtils';
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 08f3fd12ea9c..903f58e3afc3 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
 import _ from 'underscore';
 import {Parser as HtmlParser} from 'htmlparser2';
-import styles from '../../styles/styles';
+import styles from '../../../styles/styles';
 import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
 import CONST from '../../CONST';
 import ONYXKEYS from '../../ONYXKEYS';

From e70bfa092e6a20ab3b488c8e96e9b38575c25955 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 12:12:01 +0200
Subject: [PATCH 068/133] fix: imports

---
 .../AttachmentCarouselView/index.native.js     |  2 +-
 .../Attachments/AttachmentCarousel/index.js    | 18 +++++++++---------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 3b0c325d67c3..944f116ccd58 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -7,7 +7,7 @@ import styles from '../../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
 import AttachmentView from '../../AttachmentView';
 import propTypes from './propTypes';
-import CONST from '../../../..CONST';
+import CONST from '../../../../CONST';
 
 function AttachmentCarouselView({attachments, initialPage, initialActiveSource, containerDimensions, onClose, onNavigate}) {
     const pagerRef = useRef(null);
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 903f58e3afc3..7db9e86c5a3d 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -5,15 +5,15 @@ import {withOnyx} from 'react-native-onyx';
 import _ from 'underscore';
 import {Parser as HtmlParser} from 'htmlparser2';
 import styles from '../../../styles/styles';
-import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
-import CONST from '../../CONST';
-import ONYXKEYS from '../../ONYXKEYS';
-import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
-import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-import compose from '../../libs/compose';
-import withWindowDimensions from '../withWindowDimensions';
-import reportPropTypes from '../../pages/reportPropTypes';
+import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
+import CONST from '../../../CONST';
+import ONYXKEYS from '../../../ONYXKEYS';
+import reportActionPropTypes from '../../../pages/home/report/reportActionPropTypes';
+import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import compose from '../../../libs/compose';
+import withWindowDimensions from '../../withWindowDimensions';
+import reportPropTypes from '../../../pages/reportPropTypes';
 import AttachmentCarouselView from './AttachmentCarouselView';
 
 const propTypes = {

From c6d69eb150e02de99547c973031db9383b9a4917 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 12:16:34 +0200
Subject: [PATCH 069/133] fix: invalid import

---
 .../Attachments/AttachmentCarousel/CarouselButtons.js           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
index 9f5814a60a28..e0629a488668 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
@@ -8,7 +8,7 @@ import Tooltip from '../../Tooltip';
 import Button from '../../Button';
 import styles from '../../../styles/styles';
 import themeColors from '../../../styles/themes/default';
-import * as AttachmentCarouselViewPropTypes from './AttachmentCarouselView/propTypes';
+import * as AttachmentCarouselViewPropTypes from '../propTypes';
 
 const propTypes = {
     /** Where the arrows should be visible */

From 8ef110799b13dc11aeef0d4a39742221628f34fc Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 12:36:49 +0200
Subject: [PATCH 070/133] simplify propTypes and fix pdfs

---
 .../AttachmentCarouselView/index.native.js    | 10 +++-------
 .../AttachmentCarouselPage.js                 | 15 +++++++--------
 .../AttachmentViewImage/index.js              |  6 +++---
 .../AttachmentViewImage/index.native.js       |  9 +++++----
 .../AttachmentViewImage/propTypes.js          | 19 +++++++++++++++++++
 .../Attachments/AttachmentView/index.js       | 14 +++++---------
 .../Attachments/AttachmentView/propTypes.js   |  4 ++++
 7 files changed, 46 insertions(+), 31 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 944f116ccd58..5c955c6f6cf2 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,7 +1,5 @@
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {View, Keyboard} from 'react-native';
-import _ from 'underscore';
-import addEncryptedAuthTokenToURL from '../../../../libs/addEncryptedAuthTokenToURL';
 import AttachmentCarouselPager from '../../AttachmentCarouselPager';
 import styles from '../../../../styles/styles';
 import CarouselButtons from '../CarouselButtons';
@@ -16,8 +14,6 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
     // promotes the natural feeling of swiping left/right to go to the next/previous image
     const reversedAttachments = useMemo(() => attachments.reverse(), [attachments]);
 
-    const processedItems = useMemo(() => _.map(reversedAttachments, (item) => ({key: item.source, url: addEncryptedAuthTokenToURL(item.source)})), [reversedAttachments]);
-
     const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
 
     const [page, setPage] = useState(reversePage(initialPage));
@@ -93,9 +89,10 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
                 item={item}
                 isFocused={activeSource === item.source}
                 isUsedInCarousel
+                onPress={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())}
             />
         ),
-        [activeSource],
+        [activeSource, shouldShowArrows, showArrows],
     );
 
     useEffect(() => {
@@ -117,11 +114,10 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
 
             {containerDimensions.width > 0 && containerDimensions.height > 0 && (
                 <AttachmentCarouselPager
-                    items={processedItems}
+                    items={reversedAttachments}
                     renderItem={renderItem}
                     initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                    onTap={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())}
                     onPinchGestureChange={(newIsPinchGestureRunning) => {
                         setIsPinchGestureRunning(newIsPinchGestureRunning);
                         if (!newIsPinchGestureRunning && !shouldShowArrows) showArrows();
diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
index f5f6d8051986..aa241f0a80ce 100644
--- a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
@@ -6,6 +6,7 @@ import Image from '../../Image';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageTransformer from './ImageTransformer';
 import ImageWrapper from './ImageWrapper';
+import * as AttachmentsPropTypes from '../propTypes';
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = canvasWidth / imageWidth;
@@ -18,16 +19,14 @@ const cachedDimensions = new Map();
 
 const pagePropTypes = {
     isActive: PropTypes.bool.isRequired,
-    item: PropTypes.shape({
-        url: PropTypes.string,
-    }).isRequired,
+    item: AttachmentsPropTypes.attachmentPropType.isRequired,
     isAuthTokenRequired: PropTypes.bool.isRequired,
 };
 
 function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequired}) {
     const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
-    const dimensions = cachedDimensions.get(item.url);
+    const dimensions = cachedDimensions.get(item.source);
 
     const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0;
 
@@ -67,7 +66,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
                         imageScale={dimensions?.imageScale}
                     >
                         <Image
-                            source={{uri: item.url}}
+                            source={{uri: item.source}}
                             style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
                             onLoad={(evt) => {
@@ -78,7 +77,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
 
                                 // Don't update the dimensions if they are already set
                                 if (dimensions?.imageWidth !== imageWidth || dimensions?.imageHeight !== imageHeight || dimensions?.imageScale !== imageScale) {
-                                    cachedDimensions.set(item.url, {
+                                    cachedDimensions.set(item.source, {
                                         ...dimensions,
                                         imageWidth,
                                         imageHeight,
@@ -98,7 +97,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
             {(!isActive || showFallback) && (
                 <ImageWrapper>
                     <Image
-                        source={{uri: item.url}}
+                        source={{uri: item.source}}
                         isAuthTokenRequired={isAuthTokenRequired}
                         onLoad={(evt) => {
                             const imageWidth = evt.nativeEvent.width;
@@ -112,7 +111,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
                             // Don't update the dimensions if they are already set
                             if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return;
 
-                            cachedDimensions.set(item.url, {
+                            cachedDimensions.set(item.source, {
                                 ...dimensions,
                                 scaledImageWidth,
                                 scaledImageHeight,
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
index 972c98a3a43d..4329fe24e7d6 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
@@ -5,10 +5,10 @@ import withLocalize, {withLocalizePropTypes} from '../../../withLocalize';
 import compose from '../../../../libs/compose';
 import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
 import CONST from '../../../../CONST';
-import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
+import {attachmentViewImagePropTypes, attachmentViewImageDefaultProps} from './propTypes';
 
 const propTypes = {
-    ...attachmentViewPropTypes,
+    ...attachmentViewImagePropTypes,
     ...withLocalizePropTypes,
 };
 
@@ -37,6 +37,6 @@ function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChang
 }
 
 AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = attachmentViewDefaultProps;
+AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps;
 
 export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
index 40443bf5ba81..70c66fa05dd8 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -6,17 +6,18 @@ import compose from '../../../../libs/compose';
 import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
 import CONST from '../../../../CONST';
 import AttachmentCarouselPage from '../../AttachmentCarouselPager/AttachmentCarouselPage';
-import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
+import {attachmentViewImagePropTypes, attachmentViewImageDefaultProps} from './propTypes';
 
 const propTypes = {
-    ...attachmentViewPropTypes,
+    ...attachmentViewImagePropTypes,
     ...withLocalizePropTypes,
 };
 
-function AttachmentViewImage({item, loadComplete, onPress, isImage, isUsedInCarousel, onScaleChanged, translate}) {
+function AttachmentViewImage({item, isFocused, isUsedInCarousel, loadComplete, onPress, isImage, onScaleChanged, translate}) {
     const children = isUsedInCarousel ? (
         <AttachmentCarouselPage
             item={item}
+            isActive={isFocused}
             isAuthTokenRequired={isImage && item.isAuthTokenRequired}
         />
     ) : (
@@ -44,6 +45,6 @@ function AttachmentViewImage({item, loadComplete, onPress, isImage, isUsedInCaro
 }
 
 AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = attachmentViewDefaultProps;
+AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps;
 
 export default compose(memo, withLocalize)(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
new file mode 100644
index 000000000000..661b940da207
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
@@ -0,0 +1,19 @@
+import PropTypes from 'prop-types';
+import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
+
+const attachmentViewImagePropTypes = {
+    ...attachmentViewPropTypes,
+
+    loadComplete: PropTypes.bool.isRequired,
+
+    isImage: PropTypes.bool.isRequired,
+};
+
+const attachmentViewImageDefaultProps = {
+    ...attachmentViewDefaultProps,
+
+    loadComplete: false,
+    isImage: false,
+};
+
+export {attachmentViewImagePropTypes, attachmentViewImageDefaultProps};
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index 10fb10b61907..06b841b4477b 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -28,9 +28,6 @@ const propTypes = {
     /** Flag to show the loading indicator */
     shouldShowLoadingSpinnerIcon: PropTypes.bool,
 
-    /** Whether this view is the active screen  */
-    isFocused: PropTypes.bool,
-
     /** Notify parent that the UI should be modified to accommodate keyboard */
     onToggleKeyboard: PropTypes.func,
 
@@ -45,10 +42,9 @@ const defaultProps = {
     shouldShowLoadingSpinnerIcon: false,
     onToggleKeyboard: () => {},
     containerStyles: [],
-    isFocused: false,
 };
 
-function AttachmentView({item, onPress, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, onScaleChanged, onToggleKeyboard, translate, isFocused}) {
+function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, onScaleChanged, onToggleKeyboard, translate, isFocused}) {
     const [loadComplete, setLoadComplete] = useState(false);
 
     // Handles case where source is a component (ex: SVG)
@@ -86,12 +82,12 @@ function AttachmentView({item, onPress, shouldShowLoadingSpinnerIcon, shouldShow
     if (isImage || (item.file && Str.isImage(item.file.name))) {
         return (
             <AttachmentViewImage
+                item={item}
+                isUsedInCarousel={isUsedInCarousel}
                 loadComplete={loadComplete}
-                onPress={onPress}
+                isFocused={isFocused}
                 isImage={isImage}
-                file={item.file}
-                source={item.source}
-                isAuthTokenRequired={item.isAuthTokenRequired}
+                onPress={onPress}
                 onScaleChanged={onScaleChanged}
             />
         );
diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js
index 4e21d10fc5cd..021715b944a1 100644
--- a/src/components/Attachments/AttachmentView/propTypes.js
+++ b/src/components/Attachments/AttachmentView/propTypes.js
@@ -5,6 +5,9 @@ const attachmentViewPropTypes = {
     /** Attachment to display */
     item: AttachmentsPropTypes.attachmentPropType,
 
+    /** Whether this view is the active screen  */
+    isFocused: PropTypes.bool,
+
     /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
     isUsedInCarousel: PropTypes.bool,
 
@@ -22,6 +25,7 @@ const attachmentViewDefaultProps = {
             name: '',
         },
     },
+    isFocused: false,
     isUsedInCarousel: false,
     onPress: undefined,
     onScaleChanged: () => {},

From 2b7ac8f00015d6134826aa913ae23590bb501c75 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 13:13:40 +0200
Subject: [PATCH 071/133] extract pdf logic

---
 .../AttachmentCarouselPager/index.js          |  2 +-
 .../AttachmentView/AttachmentViewPdf/index.js | 24 ++++++++++
 .../AttachmentViewPdf/index.native.js         | 44 +++++++++++++++++++
 .../AttachmentViewPdf/propTypes.js            | 14 ++++++
 .../Attachments/AttachmentView/index.js       | 19 ++++----
 5 files changed, 93 insertions(+), 10 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
 create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
 create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js

diff --git a/src/components/Attachments/AttachmentCarouselPager/index.js b/src/components/Attachments/AttachmentCarouselPager/index.js
index 7aaa53f247bc..96703b659a57 100644
--- a/src/components/Attachments/AttachmentCarouselPager/index.js
+++ b/src/components/Attachments/AttachmentCarouselPager/index.js
@@ -152,7 +152,7 @@ function AttachmentCarouselPager({
                 >
                     {_.map(items, (item, index) => (
                         <View
-                            key={item.key}
+                            key={item.source}
                             style={styles.flex1}
                         >
                             {renderItem({item, index, isActive: index === activePage})}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
new file mode 100644
index 000000000000..b5a80554883d
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
@@ -0,0 +1,24 @@
+import React, {memo} from 'react';
+import styles from '../../../../styles/styles';
+import {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps} from './propTypes';
+import PDFView from '../../../PDFView';
+
+function AttachmentViewPdf({item, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete}) {
+    return (
+        <PDFView
+            onPress={onPress}
+            isFocused={isFocused}
+            sourceURL={encryptedSourceUrl}
+            fileName={item.file.name}
+            style={styles.imageModalPDF}
+            onToggleKeyboard={onToggleKeyboard}
+            onScaleChanged={onScaleChanged}
+            onLoadComplete={onLoadComplete}
+        />
+    );
+}
+
+AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes;
+AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps;
+
+export default memo(AttachmentViewPdf);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
new file mode 100644
index 000000000000..47e26fd09f8e
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
@@ -0,0 +1,44 @@
+import React, {memo, useCallback, useContext} from 'react';
+import styles from '../../../../styles/styles';
+import {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps} from './propTypes';
+import PDFView from '../../../PDFView';
+
+import AttachmentCarouselPagerContext from '../../AttachmentCarouselPager/AttachmentCarouselPagerContext';
+
+function AttachmentViewPdf({item, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete}) {
+    const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
+
+    const onScaleChanged = useCallback(
+        (scale) => {
+            onScaleChangedProp();
+
+            // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in
+            if (isUsedInCarousel) {
+                const shouldPagerScroll = scale === 1;
+
+                if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) return;
+
+                attachmentCarouselPagerContext.shouldPagerScroll.value = shouldPagerScroll;
+            }
+        },
+        [attachmentCarouselPagerContext.shouldPagerScroll, isUsedInCarousel, onScaleChangedProp],
+    );
+
+    return (
+        <PDFView
+            onPress={onPress}
+            isFocused={isFocused}
+            sourceURL={encryptedSourceUrl}
+            fileName={item.file.name}
+            style={styles.imageModalPDF}
+            onToggleKeyboard={onToggleKeyboard}
+            onScaleChanged={onScaleChanged}
+            onLoadComplete={onLoadComplete}
+        />
+    );
+}
+
+AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes;
+AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps;
+
+export default memo(AttachmentViewPdf);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
new file mode 100644
index 000000000000..91d298979831
--- /dev/null
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
@@ -0,0 +1,14 @@
+import PropTypes from 'prop-types';
+import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
+
+const attachmentViewPdfPropTypes = {
+    ...attachmentViewPropTypes,
+
+    encryptedSourceUrl: PropTypes.string.isRequired,
+    onToggleKeyboard: PropTypes.func.isRequired,
+    onLoadComplete: PropTypes.func.isRequired,
+};
+
+const attachmentViewPdfDefaultProps = attachmentViewDefaultProps;
+
+export {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps};
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index 06b841b4477b..9bf277350328 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -4,7 +4,6 @@ import _ from 'underscore';
 import PropTypes from 'prop-types';
 import Str from 'expensify-common/lib/str';
 import styles from '../../../styles/styles';
-import PDFView from '../../PDFView';
 import Icon from '../../Icon';
 import * as Expensicons from '../../Icon/Expensicons';
 import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
@@ -13,8 +12,9 @@ import Text from '../../Text';
 import Tooltip from '../../Tooltip';
 import themeColors from '../../../styles/themes/default';
 import variables from '../../../styles/variables';
-import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 import AttachmentViewImage from './AttachmentViewImage';
+import AttachmentViewPdf from './AttachmentViewPdf';
+import addEncryptedAuthTokenToURL from '../../../libs/addEncryptedAuthTokenToURL';
 
 import {attachmentViewPropTypes, attachmentViewDefaultProps} from './propTypes';
 
@@ -61,16 +61,17 @@ function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinn
     // Check both source and file.name since PDFs dragged into the the text field
     // will appear with a source that is a blob
     if (Str.isPDF(item.source) || (item.file && Str.isPDF(item.file.name || translate('attachmentView.unknownFilename')))) {
-        const sourceURL = item.isAuthTokenRequired ? addEncryptedAuthTokenToURL(item.source) : item.source;
+        const encryptedSourceUrl = item.isAuthTokenRequired ? addEncryptedAuthTokenToURL(item.source) : item.source;
+
         return (
-            <PDFView
-                onPress={onPress}
+            <AttachmentViewPdf
+                item={item}
+                encryptedSourceUrl={encryptedSourceUrl}
+                isUsedInCarousel={isUsedInCarousel}
                 isFocused={isFocused}
-                sourceURL={sourceURL}
-                fileName={item.file.name}
-                style={styles.imageModalPDF}
-                onToggleKeyboard={onToggleKeyboard}
+                onPress={onPress}
                 onScaleChanged={onScaleChanged}
+                onToggleKeyboard={onToggleKeyboard}
                 onLoadComplete={() => !loadComplete && setLoadComplete(true)}
             />
         );

From bee4df32c1e8dbddf067ae50862a0909186bc67f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 20:59:27 +0200
Subject: [PATCH 072/133] simplify attachment initialization

---
 .../AttachmentCarouselView/index.native.js     | 18 ++++++------------
 .../AttachmentCarousel/CarouselButtons.js      |  4 ++--
 .../Attachments/AttachmentCarousel/index.js    |  9 ++++++---
 3 files changed, 14 insertions(+), 17 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
index 5c955c6f6cf2..da969885c87e 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
 import {View, Keyboard} from 'react-native';
 import AttachmentCarouselPager from '../../AttachmentCarouselPager';
 import styles from '../../../../styles/styles';
@@ -10,13 +10,7 @@ import CONST from '../../../../CONST';
 function AttachmentCarouselView({attachments, initialPage, initialActiveSource, containerDimensions, onClose, onNavigate}) {
     const pagerRef = useRef(null);
 
-    // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling
-    // promotes the natural feeling of swiping left/right to go to the next/previous image
-    const reversedAttachments = useMemo(() => attachments.reverse(), [attachments]);
-
-    const reversePage = useCallback((page) => Math.max(0, Math.min(attachments.length - page - 1, attachments.length)), [attachments.length]);
-
-    const [page, setPage] = useState(reversePage(initialPage));
+    const [page, setPage] = useState(initialPage);
     const [activeSource, setActiveSource] = useState(initialActiveSource);
     const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
     const [shouldShowArrows, setShouldShowArrows] = useState(true);
@@ -53,14 +47,14 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
             Keyboard.dismiss();
             showArrows();
 
-            const item = reversedAttachments[newPageIndex];
+            const item = attachments[newPageIndex];
 
             setPage(newPageIndex);
             setActiveSource(item.source);
 
             onNavigate(item);
         },
-        [onNavigate, reversedAttachments, showArrows],
+        [onNavigate, attachments, showArrows],
     );
 
     /**
@@ -104,7 +98,7 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
         <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
             <CarouselButtons
                 shouldShowArrows={shouldShowArrows && !isPinchGestureRunning}
-                page={reversePage(page)}
+                page={page}
                 attachments={attachments}
                 onBack={() => cycleThroughAttachments(-1)}
                 onForward={() => cycleThroughAttachments(1)}
@@ -114,7 +108,7 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
 
             {containerDimensions.width > 0 && containerDimensions.height > 0 && (
                 <AttachmentCarouselPager
-                    items={reversedAttachments}
+                    items={attachments}
                     renderItem={renderItem}
                     initialIndex={page}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
index e0629a488668..27976822bae9 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
@@ -37,8 +37,8 @@ const defaultProps = {
 };
 
 function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow, ...props}) {
-    const isForwardDisabled = page === 0;
-    const isBackDisabled = page === _.size(attachments) - 1;
+    const isBackDisabled = page === 0;
+    const isForwardDisabled = page === _.size(attachments) - 1;
 
     return shouldShowArrows ? (
         <>
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 7db9e86c5a3d..81804dbc5133 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -57,7 +57,7 @@ class AttachmentCarousel extends React.Component {
      */
     createInitialState() {
         const actions = [ReportActionsUtils.getParentReportAction(this.props.report), ...ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions))];
-        const attachments = [];
+        let attachments = [];
 
         const htmlParser = new HtmlParser({
             onopentag: (name, attribs) => {
@@ -85,20 +85,23 @@ class AttachmentCarousel extends React.Component {
         });
         htmlParser.end();
 
+        attachments = attachments.reverse();
         const initialPage = _.findIndex(attachments, (a) => a.source === this.props.source);
         if (initialPage === -1) {
             throw new Error('Attachment not found');
         }
 
+        const initialItem = attachments[initialPage];
+
         // Update the parent modal's state with the source and name from the mapped attachments
-        this.props.onNavigate(attachments[initialPage]);
+        this.props.onNavigate(initialItem);
 
         return {
             initialPage,
             attachments,
             containerWidth: 0,
             containerHeight: 0,
-            initialActiveSource: null,
+            initialActiveSource: initialItem.source,
         };
     }
 

From a1d7c2c04c10f1e9602b22df73087ce92850b4ad Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 21:00:40 +0200
Subject: [PATCH 073/133] improve web AttachmentCarouselView

---
 .../AttachmentCarouselView/index.js           | 46 +++++++++----------
 1 file changed, 21 insertions(+), 25 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
index 3d1093c492fb..0f948c6c8406 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
@@ -36,14 +36,11 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
             Keyboard.dismiss();
 
             if (!item) {
-                // eslint-disable-next-line react/no-unused-state
                 setActiveSource(null);
                 return;
             }
 
-            const pageIndex = index;
-
-            setPage(pageIndex);
+            setPage(index);
             setActiveSource(item.source);
 
             onNavigate(item);
@@ -51,39 +48,39 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
         [onNavigate],
     );
 
-    /**
-     * Calculate items layout information to optimize scrolling performance
-     * @param {*} data
-     * @param {Number} index
-     * @returns {{offset: Number, length: Number, index: Number}}
-     */
-    const getItemLayout = useCallback(
-        (_data, index) => ({
-            length: containerDimensions.width,
-            offset: containerDimensions.height * index,
-            index,
-        }),
-        [containerDimensions.height, containerDimensions.width],
-    );
-
     /**
      * Increments or decrements the index to get another selected item
      * @param {Number} deltaSlide
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextIndex = page - deltaSlide;
+            const nextIndex = page + deltaSlide;
             const nextItem = attachments[nextIndex];
 
             if (!nextItem || !scrollRef.current) {
                 return;
             }
 
-            // The sliding transition is a bit too much on web, because of the wider and bigger images,
-            // so we only enable it for mobile
-            scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
+            updatePage({item: nextItem, index: nextIndex});
+
+            scrollRef.current.scrollToItem({item: nextItem, animated: true});
         },
-        [attachments, page],
+        [attachments, page, updatePage],
+    );
+
+    /**
+     * Calculate items layout information to optimize scrolling performance
+     * @param {*} data
+     * @param {Number} index
+     * @returns {{offset: Number, length: Number, index: Number}}
+     */
+    const getItemLayout = useCallback(
+        (_data, index) => ({
+            length: containerDimensions.width,
+            offset: containerDimensions.height * index,
+            index,
+        }),
+        [containerDimensions.height, containerDimensions.width],
     );
 
     /**
@@ -158,7 +155,6 @@ function AttachmentCarouselView({containerDimensions, attachments, initialPage,
                     decelerationRate="fast"
                     showsHorizontalScrollIndicator={false}
                     bounces={false}
-                    disableIntervalMomentum // Scroll only one image at a time no matter how fast the user swipes
                     pagingEnabled
                     snapToAlignment="start"
                     snapToInterval={containerDimensions.width}

From e7e8538fc5460ee1b46eb6979aa7deae01411fd1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 21:41:48 +0200
Subject: [PATCH 074/133] simplify AttachmentCarousel logic

---
 .../AttachmentCarouselView/index.js           | 183 ------------
 .../AttachmentCarouselView/propTypes.js       |  29 --
 .../AttachmentCarousel/extractAttachments.js  |  59 ++++
 .../Attachments/AttachmentCarousel/index.js   | 266 +++++++++++-------
 .../index.native.js                           |  61 +++-
 .../AttachmentCarousel/propTypes.js           |  34 +++
 src/styles/styles.js                          |   5 +-
 7 files changed, 297 insertions(+), 340 deletions(-)
 delete mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
 delete mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
 create mode 100644 src/components/Attachments/AttachmentCarousel/extractAttachments.js
 rename src/components/Attachments/AttachmentCarousel/{AttachmentCarouselView => }/index.native.js (64%)
 create mode 100644 src/components/Attachments/AttachmentCarousel/propTypes.js

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
deleted file mode 100644
index 0f948c6c8406..000000000000
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.js
+++ /dev/null
@@ -1,183 +0,0 @@
-import React, {useRef, useCallback, useState} from 'react';
-import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
-import _ from 'underscore';
-import styles from '../../../../styles/styles';
-import CarouselActions from '../CarouselActions';
-import AttachmentView from '../../AttachmentView';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../../withWindowDimensions';
-import CarouselButtons from '../CarouselButtons';
-import attachmentCarouselViewPropTypes from './propTypes';
-
-const VIEWABILITY_CONFIG = {
-    // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-    // more than 95% visible. When that happens we update the page index in the state.
-    itemVisiblePercentThreshold: 95,
-};
-
-const propTypes = {
-    ...attachmentCarouselViewPropTypes,
-    ...windowDimensionsPropTypes,
-};
-
-function AttachmentCarouselView({containerDimensions, attachments, initialPage, initialActiveSource, onNavigate, ...props}) {
-    const scrollRef = useRef(null);
-
-    const [shouldShowArrows, setShouldShowArrows] = useState(false);
-    const [page, setPage] = useState(initialPage);
-    const [activeSource, setActiveSource] = useState(initialActiveSource);
-
-    /**
-     * Updates the page state when the user navigates between attachments
-     * @param {Object} item
-     * @param {number} index
-     */
-    const updatePage = useCallback(
-        ({item, index}) => {
-            Keyboard.dismiss();
-
-            if (!item) {
-                setActiveSource(null);
-                return;
-            }
-
-            setPage(index);
-            setActiveSource(item.source);
-
-            onNavigate(item);
-        },
-        [onNavigate],
-    );
-
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide) => {
-            const nextIndex = page + deltaSlide;
-            const nextItem = attachments[nextIndex];
-
-            if (!nextItem || !scrollRef.current) {
-                return;
-            }
-
-            updatePage({item: nextItem, index: nextIndex});
-
-            scrollRef.current.scrollToItem({item: nextItem, animated: true});
-        },
-        [attachments, page, updatePage],
-    );
-
-    /**
-     * Calculate items layout information to optimize scrolling performance
-     * @param {*} data
-     * @param {Number} index
-     * @returns {{offset: Number, length: Number, index: Number}}
-     */
-    const getItemLayout = useCallback(
-        (_data, index) => ({
-            length: containerDimensions.width,
-            offset: containerDimensions.height * index,
-            index,
-        }),
-        [containerDimensions.height, containerDimensions.width],
-    );
-
-    /**
-     * Defines how a container for a single attachment should be rendered
-     * @param {Object} props
-     * @returns {JSX.Element}
-     */
-    const renderCell = useCallback(
-        (cellProps) => {
-            // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
-            // considering horizontal margin and border width in centered modal
-            const modalStyles = styles.centeredModalStyles(props.isSmallScreenWidth, true);
-            const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(props.windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
-
-            return (
-                <View
-                    // eslint-disable-next-line react/jsx-props-no-spreading
-                    {...cellProps}
-                    style={style}
-                />
-            );
-        },
-        [props.isSmallScreenWidth, props.windowWidth],
-    );
-
-    /**
-     * Defines how a single attachment should be rendered
-     * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
-     * @returns {JSX.Element}
-     */
-    const renderItem = useCallback(
-        ({item}) => (
-            <AttachmentView
-                item={item}
-                isFocused={activeSource === item.source}
-                isUsedInCarousel
-            />
-        ),
-        [activeSource],
-    );
-
-    const handleViewableItemsChange = useCallback(
-        ({viewableItems}) => {
-            // Since we can have only one item in view at a time, we can use the first item in the array
-            // to get the index of the current page
-            const entry = _.first([...viewableItems]);
-            updatePage({item: entry.item, index: entry.index});
-        },
-        [updatePage],
-    );
-
-    const viewabilityConfigCallbackPairs = useRef([{viewabilityConfig: VIEWABILITY_CONFIG, onViewableItemsChanged: handleViewableItemsChange}]);
-
-    return (
-        <View
-            onMouseEnter={() => setShouldShowArrows(true)}
-            onMouseLeave={() => setShouldShowArrows(false)}
-            style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}
-        >
-            <CarouselButtons
-                shouldShowArrows={shouldShowArrows}
-                page={page}
-                attachments={attachments}
-                onBack={() => cycleThroughAttachments(-1)}
-                onForward={() => cycleThroughAttachments(1)}
-            />
-
-            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
-                <FlatList
-                    listKey="AttachmentCarousel"
-                    horizontal
-                    decelerationRate="fast"
-                    showsHorizontalScrollIndicator={false}
-                    bounces={false}
-                    pagingEnabled
-                    snapToAlignment="start"
-                    snapToInterval={containerDimensions.width}
-                    scrollEnabled={false}
-                    ref={scrollRef}
-                    initialScrollIndex={page}
-                    initialNumToRender={3}
-                    windowSize={5}
-                    maxToRenderPerBatch={3}
-                    data={attachments}
-                    CellRendererComponent={renderCell}
-                    renderItem={renderItem}
-                    getItemLayout={getItemLayout}
-                    keyExtractor={(item) => item.source}
-                    viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
-                />
-            )}
-
-            <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
-        </View>
-    );
-}
-
-AttachmentCarouselView.propTypes = propTypes;
-
-export default withWindowDimensions(AttachmentCarouselView);
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
deleted file mode 100644
index 28a5d9610068..000000000000
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/propTypes.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import PropTypes from 'prop-types';
-import * as AttachmentsPropTypes from '../../propTypes';
-
-const propTypes = {
-    /**
-     * The initial page of the carousel
-     */
-    initialPage: PropTypes.number.isRequired,
-
-    /**
-     * The attachments of the carousel
-     */
-    attachments: AttachmentsPropTypes.attachmentsPropType.isRequired,
-
-    /**
-     * The initial active ource of the carousel
-     */
-    initialActiveSource: PropTypes.string,
-
-    /**
-     * The container dimensions
-     */
-    containerDimensions: PropTypes.shape({
-        width: PropTypes.number.isRequired,
-        height: PropTypes.number.isRequired,
-    }).isRequired,
-};
-
-export default propTypes;
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.js b/src/components/Attachments/AttachmentCarousel/extractAttachments.js
new file mode 100644
index 000000000000..3cdf09a98fed
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.js
@@ -0,0 +1,59 @@
+import {Parser as HtmlParser} from 'htmlparser2';
+import _ from 'underscore';
+import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
+import CONST from '../../../CONST';
+import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
+
+/**
+ * Constructs the initial component state from report actions
+ * @param {Object} report
+ * @param {object} reportActions
+ * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}}
+ */
+function extractAttachments({report, reportActions, source}) {
+    const actions = [ReportActionsUtils.getParentReportAction(report), ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
+    let attachments = [];
+
+    const htmlParser = new HtmlParser({
+        onopentag: (name, attribs) => {
+            if (name !== 'img' || !attribs.src) {
+                return;
+            }
+
+            const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
+
+            // By iterating actions in chronological order and prepending each attachment
+            // we ensure correct order of attachments even across actions with multiple attachments.
+            attachments.unshift({
+                source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
+                isAuthTokenRequired: Boolean(expensifySource),
+                file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
+            });
+        },
+    });
+
+    _.forEach(actions, (action, key) => {
+        if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) {
+            return;
+        }
+        htmlParser.write(_.get(action, ['message', 0, 'html']));
+    });
+    htmlParser.end();
+
+    attachments = attachments.reverse();
+    const initialPage = _.findIndex(attachments, (a) => a.source === source);
+    if (initialPage === -1) {
+        throw new Error('Attachment not found');
+    }
+
+    const initialItem = attachments[initialPage];
+
+    return {
+        attachments,
+        initialPage,
+        initialItem,
+        initialActiveSource: initialItem.source,
+    };
+}
+
+export default extractAttachments;
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 81804dbc5133..5ad3759b5275 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -1,134 +1,182 @@
-import React from 'react';
-import {View, PixelRatio} from 'react-native';
-import PropTypes from 'prop-types';
+import React, {useRef, useCallback, useState, useEffect, useMemo} from 'react';
+import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
 import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import {Parser as HtmlParser} from 'htmlparser2';
 import styles from '../../../styles/styles';
-import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
-import CONST from '../../../CONST';
+import CarouselActions from './CarouselActions';
+import AttachmentView from '../AttachmentView';
+import withWindowDimensions from '../../withWindowDimensions';
+import CarouselButtons from './CarouselButtons';
+import extractAttachments from './extractAttachments';
+import {attachmentCarouselPropTypes, attachmentCarouselDefaultProps} from './propTypes';
 import ONYXKEYS from '../../../ONYXKEYS';
-import reportActionPropTypes from '../../../pages/home/report/reportActionPropTypes';
-import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
-import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import withLocalize from '../../withLocalize';
 import compose from '../../../libs/compose';
-import withWindowDimensions from '../../withWindowDimensions';
-import reportPropTypes from '../../../pages/reportPropTypes';
-import AttachmentCarouselView from './AttachmentCarouselView';
-
-const propTypes = {
-    /** source is used to determine the starting index in the array of attachments */
-    source: PropTypes.string,
-
-    /** Callback to update the parent modal's state with a source and name from the attachments array */
-    onNavigate: PropTypes.func,
 
-    /** Callback to close carousel when user swipes down (on native) */
-    onClose: PropTypes.func,
+function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallScreenWidth, windowWidth}) {
+    const scrollRef = useRef(null);
 
-    /** Object of report actions for this report */
-    reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachments(report, reportActions, source), [report, reportActions, source]);
 
-    /** The report currently being looked at */
-    report: reportPropTypes.isRequired,
+    useEffect(() => {
+        // Update the parent modal's state with the source and name from the mapped attachments
+        onNavigate(initialItem);
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [initialItem]);
 
-    ...withLocalizePropTypes,
-};
+    const [containerDimensions, setContainerDimensions] = useState({width: 0, height: 0});
+    const [shouldShowArrows, setShouldShowArrows] = useState(false);
+    const [page, setPage] = useState(initialPage);
+    const [activeSource, setActiveSource] = useState(initialActiveSource);
 
-const defaultProps = {
-    source: '',
-    reportActions: {},
-    onNavigate: () => {},
-    onClose: () => {},
-};
+    /**
+     * Updates the page state when the user navigates between attachments
+     * @param {Object} item
+     * @param {number} index
+     */
+    const updatePage = useCallback(
+        ({item, index}) => {
+            Keyboard.dismiss();
 
-class AttachmentCarousel extends React.Component {
-    constructor(props) {
-        super(props);
+            if (!item) {
+                setActiveSource(null);
+                return;
+            }
 
-        this.createInitialState = this.createInitialState.bind(this);
+            setPage(index);
+            setActiveSource(item.source);
 
-        this.state = this.createInitialState();
-    }
+            onNavigate(item);
+        },
+        [onNavigate],
+    );
 
     /**
-     * Constructs the initial component state from report actions
-     * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}}
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
      */
-    createInitialState() {
-        const actions = [ReportActionsUtils.getParentReportAction(this.props.report), ...ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions))];
-        let attachments = [];
-
-        const htmlParser = new HtmlParser({
-            onopentag: (name, attribs) => {
-                if (name !== 'img' || !attribs.src) {
-                    return;
-                }
-
-                const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
-
-                // By iterating actions in chronological order and prepending each attachment
-                // we ensure correct order of attachments even across actions with multiple attachments.
-                attachments.unshift({
-                    source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
-                    isAuthTokenRequired: Boolean(expensifySource),
-                    file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
-                });
-            },
-        });
-
-        _.forEach(actions, (action, key) => {
-            if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) {
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide) => {
+            const nextIndex = page + deltaSlide;
+            const nextItem = attachments[nextIndex];
+
+            if (!nextItem || !scrollRef.current) {
                 return;
             }
-            htmlParser.write(_.get(action, ['message', 0, 'html']));
-        });
-        htmlParser.end();
 
-        attachments = attachments.reverse();
-        const initialPage = _.findIndex(attachments, (a) => a.source === this.props.source);
-        if (initialPage === -1) {
-            throw new Error('Attachment not found');
-        }
+            updatePage({item: nextItem, index: nextIndex});
 
-        const initialItem = attachments[initialPage];
+            scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
+        },
+        [attachments, page, updatePage],
+    );
 
-        // Update the parent modal's state with the source and name from the mapped attachments
-        this.props.onNavigate(initialItem);
-
-        return {
-            initialPage,
-            attachments,
-            containerWidth: 0,
-            containerHeight: 0,
-            initialActiveSource: initialItem.source,
-        };
-    }
-
-    render() {
-        return (
-            <View
-                style={[styles.flex1, styles.attachmentCarouselContainer]}
-                onLayout={({nativeEvent}) =>
-                    // eslint-disable-next-line react/no-unused-state
-                    this.setState({containerWidth: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), containerHeight: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
-                }
-            >
-                <AttachmentCarouselView
-                    attachments={this.state.attachments}
-                    initialPage={this.state.initialPage}
-                    initialActiveSource={this.state.initialActiveSource}
-                    containerDimensions={{width: this.state.containerWidth, height: this.state.containerHeight}}
-                    onClose={this.props.onClose}
-                    onNavigate={this.props.onNavigate}
+    console.log({page});
+
+    /**
+     * Calculate items layout information to optimize scrolling performance
+     * @param {*} data
+     * @param {Number} index
+     * @returns {{offset: Number, length: Number, index: Number}}
+     */
+    const getItemLayout = useCallback(
+        (_data, index) => ({
+            length: containerDimensions.width,
+            offset: containerDimensions.height * index,
+            index,
+        }),
+        [containerDimensions.height, containerDimensions.width],
+    );
+
+    /**
+     * Defines how a container for a single attachment should be rendered
+     * @param {Object} props
+     * @returns {JSX.Element}
+     */
+    const renderCell = useCallback(
+        (cellProps) => {
+            // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
+            // considering horizontal margin and border width in centered modal
+            const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
+            const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
+
+            return (
+                <View
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    {...cellProps}
+                    style={style}
                 />
-            </View>
-        );
-    }
-}
+            );
+        },
+        [isSmallScreenWidth, windowWidth],
+    );
 
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
+    /**
+     * Defines how a single attachment should be rendered
+     * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
+     * @returns {JSX.Element}
+     */
+    const renderItem = useCallback(
+        ({item}) => (
+            <AttachmentView
+                item={item}
+                isFocused={activeSource === item.source}
+                isUsedInCarousel
+            />
+        ),
+        [activeSource],
+    );
+
+    return (
+        <View
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            onLayout={({nativeEvent}) =>
+                setContainerDimensions({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
+            }
+            onMouseEnter={() => setShouldShowArrows(true)}
+            onMouseLeave={() => setShouldShowArrows(false)}
+        >
+            <CarouselButtons
+                shouldShowArrows={shouldShowArrows}
+                page={page}
+                attachments={attachments}
+                onBack={() => cycleThroughAttachments(-1)}
+                onForward={() => cycleThroughAttachments(1)}
+            />
+
+            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
+                <FlatList
+                    contentContainerStyle={{flex: 1}}
+                    listKey="AttachmentCarousel"
+                    horizontal
+                    decelerationRate="fast"
+                    showsHorizontalScrollIndicator={false}
+                    bounces={false}
+                    pagingEnabled
+                    snapToAlignment="start"
+                    snapToInterval={containerDimensions.width}
+                    // scrollEnabled={false}
+                    // ref={scrollRef}
+                    ref={(ref) => {
+                        scrollRef.current = ref;
+                    }}
+                    initialScrollIndex={initialPage}
+                    initialNumToRender={3}
+                    windowSize={5}
+                    maxToRenderPerBatch={3}
+                    data={attachments}
+                    CellRendererComponent={renderCell}
+                    renderItem={renderItem}
+                    getItemLayout={getItemLayout}
+                    keyExtractor={(item) => item.source}
+                />
+            )}
+
+            <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
+        </View>
+    );
+}
+AttachmentCarousel.propTypes = attachmentCarouselPropTypes;
+AttachmentCarousel.defaultProps = attachmentCarouselDefaultProps;
 
 export default compose(
     withOnyx({
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
similarity index 64%
rename from src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
rename to src/components/Attachments/AttachmentCarousel/index.native.js
index da969885c87e..4f40cdae6ea0 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -1,15 +1,30 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {View, Keyboard} from 'react-native';
-import AttachmentCarouselPager from '../../AttachmentCarouselPager';
-import styles from '../../../../styles/styles';
-import CarouselButtons from '../CarouselButtons';
-import AttachmentView from '../../AttachmentView';
-import propTypes from './propTypes';
-import CONST from '../../../../CONST';
-
-function AttachmentCarouselView({attachments, initialPage, initialActiveSource, containerDimensions, onClose, onNavigate}) {
+import React, {useCallback, useEffect, useRef, useState, useMemo} from 'react';
+import {View, Keyboard, PixelRatio} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import AttachmentCarouselPager from '../AttachmentCarouselPager';
+import styles from '../../../styles/styles';
+import CarouselButtons from './CarouselButtons';
+import AttachmentView from '../AttachmentView';
+import CONST from '../../../CONST';
+import ONYXKEYS from '../../../ONYXKEYS';
+import withLocalize from '../../withLocalize';
+import compose from '../../../libs/compose';
+import withWindowDimensions from '../../withWindowDimensions';
+import {attachmentCarouselPropTypes, attachmentCarouselDefaultProps} from './propTypes';
+import extractAttachments from './extractAttachments';
+
+function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}) {
+    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachments(report, reportActions, source), [report, reportActions, source]);
+
+    useEffect(() => {
+        // Update the parent modal's state with the source and name from the mapped attachments
+        onNavigate(initialItem);
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [initialItem]);
+
     const pagerRef = useRef(null);
 
+    const [containerDimensions, setContainerDimensions] = useState({width: 0, height: 0});
     const [page, setPage] = useState(initialPage);
     const [activeSource, setActiveSource] = useState(initialActiveSource);
     const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
@@ -95,7 +110,14 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
     }, []);
 
     return (
-        <View style={[styles.flex1, styles.attachmentCarouselButtonsContainer]}>
+        <View
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            onLayout={({nativeEvent}) =>
+                setContainerDimensions({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
+            }
+            onMouseEnter={() => setShouldShowArrows(true)}
+            onMouseLeave={() => setShouldShowArrows(false)}
+        >
             <CarouselButtons
                 shouldShowArrows={shouldShowArrows && !isPinchGestureRunning}
                 page={page}
@@ -125,7 +147,16 @@ function AttachmentCarouselView({attachments, initialPage, initialActiveSource,
         </View>
     );
 }
-
-AttachmentCarouselView.propTypes = propTypes;
-
-export default AttachmentCarouselView;
+AttachmentCarousel.propTypes = attachmentCarouselPropTypes;
+AttachmentCarousel.defaultProps = attachmentCarouselDefaultProps;
+
+export default compose(
+    withOnyx({
+        reportActions: {
+            key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+            canEvict: false,
+        },
+    }),
+    withLocalize,
+    withWindowDimensions,
+)(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/propTypes.js b/src/components/Attachments/AttachmentCarousel/propTypes.js
new file mode 100644
index 000000000000..f775fb02bd9b
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/propTypes.js
@@ -0,0 +1,34 @@
+import PropTypes from 'prop-types';
+import {windowDimensionsPropTypes} from '../../withWindowDimensions';
+import reportPropTypes from '../../../pages/reportPropTypes';
+import reportActionPropTypes from '../../../pages/home/report/reportActionPropTypes';
+import {withLocalizePropTypes} from '../../withLocalize';
+
+const attachmentCarouselPropTypes = {
+    /** source is used to determine the starting index in the array of attachments */
+    source: PropTypes.string,
+
+    /** Callback to update the parent modal's state with a source and name from the attachments array */
+    onNavigate: PropTypes.func,
+
+    /** Callback to close carousel when user swipes down (on native) */
+    onClose: PropTypes.func,
+
+    /** Object of report actions for this report */
+    reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+
+    /** The report currently being looked at */
+    report: reportPropTypes.isRequired,
+
+    ...windowDimensionsPropTypes,
+    ...withLocalizePropTypes,
+};
+
+const attachmentCarouselDefaultProps = {
+    source: '',
+    reportActions: {},
+    onNavigate: () => {},
+    onClose: () => {},
+};
+
+export {attachmentCarouselPropTypes, attachmentCarouselDefaultProps};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 96f299f9a487..160c9104e32b 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2447,12 +2447,9 @@ const styles = {
     attachmentCarouselContainer: {
         height: '100%',
         width: '100%',
-        ...cursor.cursorUnset,
-    },
-
-    attachmentCarouselButtonsContainer: {
         display: 'flex',
         justifyContent: 'center',
+        ...cursor.cursorUnset,
     },
 
     attachmentArrow: {

From 5d3bb956cb10ceeed86881cc9d86f88a541c03b1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 21:49:59 +0200
Subject: [PATCH 075/133] fix: jsdoc and params

---
 .../Attachments/AttachmentCarousel/extractAttachments.js   | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.js b/src/components/Attachments/AttachmentCarousel/extractAttachments.js
index 3cdf09a98fed..e1460ca8ebc0 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachments.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.js
@@ -7,10 +7,11 @@ import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
 /**
  * Constructs the initial component state from report actions
  * @param {Object} report
- * @param {object} reportActions
- * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number}}
+ * @param {Array} reportActions
+ * @param {String} source
+ * @returns {{attachments: Array, initialPage: Number, initialItem: Object, initialActiveSource: String}}
  */
-function extractAttachments({report, reportActions, source}) {
+function extractAttachments(report, reportActions, source) {
     const actions = [ReportActionsUtils.getParentReportAction(report), ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
     let attachments = [];
 

From 1b51448c6b0cb33aee31d596126a6cd3c4040a39 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 21:50:42 +0200
Subject: [PATCH 076/133] remove log

---
 src/components/Attachments/AttachmentCarousel/index.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 5ad3759b5275..c5d78fee15a5 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -70,8 +70,6 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
         [attachments, page, updatePage],
     );
 
-    console.log({page});
-
     /**
      * Calculate items layout information to optimize scrolling performance
      * @param {*} data

From 160eeb93c4f701786b1dc26a00992a6c1b8d4a8b Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 22:39:31 +0200
Subject: [PATCH 077/133] extract arrow showing logic

---
 .../AttachmentCarousel/index.native.js        | 49 +++++--------------
 .../AttachmentCarousel/useCarouselArrows.js   | 46 +++++++++++++++++
 2 files changed, 57 insertions(+), 38 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentCarousel/useCarouselArrows.js

diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 4f40cdae6ea0..aa11c24280af 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -5,13 +5,13 @@ import AttachmentCarouselPager from '../AttachmentCarouselPager';
 import styles from '../../../styles/styles';
 import CarouselButtons from './CarouselButtons';
 import AttachmentView from '../AttachmentView';
-import CONST from '../../../CONST';
 import ONYXKEYS from '../../../ONYXKEYS';
 import withLocalize from '../../withLocalize';
 import compose from '../../../libs/compose';
 import withWindowDimensions from '../../withWindowDimensions';
 import {attachmentCarouselPropTypes, attachmentCarouselDefaultProps} from './propTypes';
 import extractAttachments from './extractAttachments';
+import useCarouselArrows from './useCarouselArrows';
 
 function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}) {
     const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachments(report, reportActions, source), [report, reportActions, source]);
@@ -28,29 +28,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
     const [page, setPage] = useState(initialPage);
     const [activeSource, setActiveSource] = useState(initialActiveSource);
     const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
-    const [shouldShowArrows, setShouldShowArrows] = useState(true);
-
-    const autoHideArrowTimeout = useRef(null);
-
-    /**
-     * Cancels the automatic hiding of the arrows.
-     */
-    const cancelAutoHideArrow = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
-
-    /**
-     * Automatically hide the arrows if there is no interaction for 3 seconds.
-     */
-    const autoHideArrow = useCallback(() => {
-        cancelAutoHideArrow();
-        autoHideArrowTimeout.current = setTimeout(() => {
-            setShouldShowArrows(false);
-        }, CONST.ARROW_HIDE_DELAY);
-    }, [cancelAutoHideArrow]);
-
-    const showArrows = useCallback(() => {
-        setShouldShowArrows(true);
-        autoHideArrow();
-    }, [autoHideArrow]);
+    const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
 
     /**
      * Updates the page state when the user navigates between attachments
@@ -60,7 +38,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
     const updatePage = useCallback(
         (newPageIndex) => {
             Keyboard.dismiss();
-            showArrows();
+            setShouldShowArrows(true);
 
             const item = attachments[newPageIndex];
 
@@ -69,7 +47,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
 
             onNavigate(item);
         },
-        [onNavigate, attachments, showArrows],
+        [setShouldShowArrows, attachments, onNavigate],
     );
 
     /**
@@ -82,9 +60,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
             updatePage(nextPageIndex);
             pagerRef.current.setPage(nextPageIndex);
 
-            autoHideArrow();
+            autoHideArrows();
         },
-        [autoHideArrow, page, updatePage],
+        [autoHideArrows, page, updatePage],
     );
 
     /**
@@ -98,17 +76,12 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                 item={item}
                 isFocused={activeSource === item.source}
                 isUsedInCarousel
-                onPress={() => (shouldShowArrows ? setShouldShowArrows(false) : showArrows())}
+                onPress={() => (shouldShowArrows ? setShouldShowArrows(false) : setShouldShowArrows(true))}
             />
         ),
-        [activeSource, shouldShowArrows, showArrows],
+        [activeSource, setShouldShowArrows, shouldShowArrows],
     );
 
-    useEffect(() => {
-        autoHideArrow();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []);
-
     return (
         <View
             style={[styles.flex1, styles.attachmentCarouselContainer]}
@@ -124,8 +97,8 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                 attachments={attachments}
                 onBack={() => cycleThroughAttachments(-1)}
                 onForward={() => cycleThroughAttachments(1)}
-                autoHideArrow={autoHideArrow}
-                cancelAutoHideArrow={cancelAutoHideArrow}
+                autoHideArrow={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
             />
 
             {containerDimensions.width > 0 && containerDimensions.height > 0 && (
@@ -136,7 +109,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                     onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
                     onPinchGestureChange={(newIsPinchGestureRunning) => {
                         setIsPinchGestureRunning(newIsPinchGestureRunning);
-                        if (!newIsPinchGestureRunning && !shouldShowArrows) showArrows();
+                        if (!newIsPinchGestureRunning && !shouldShowArrows) setShouldShowArrows(true);
                     }}
                     onSwipeDown={onClose}
                     containerWidth={containerDimensions.width}
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
new file mode 100644
index 000000000000..4a6797c7adef
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
@@ -0,0 +1,46 @@
+import {useCallback, useEffect, useRef, useState, useMemo} from 'react';
+import CONST from '../../../CONST';
+import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
+
+function useCarouselArrows() {
+    const canUseTouchScreen = useMemo(() => DeviceCapabilities.canUseTouchScreen(), []);
+
+    const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen);
+    const autoHideArrowTimeout = useRef(null);
+
+    /**
+     * Cancels the automatic hiding of the arrows.
+     */
+    const cancelAutoHideArrows = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
+
+    /**
+     * Automatically hide the arrows if there is no interaction for 3 seconds.
+     */
+    const autoHideArrows = useCallback(() => {
+        if (!canUseTouchScreen) {
+            return;
+        }
+
+        cancelAutoHideArrows();
+        autoHideArrowTimeout.current = setTimeout(() => {
+            setShouldShowArrowsInternal(false);
+        }, CONST.ARROW_HIDE_DELAY);
+    }, [canUseTouchScreen, cancelAutoHideArrows]);
+
+    const setShouldShowArrows = useCallback(
+        (show = true) => {
+            setShouldShowArrowsInternal(show);
+            autoHideArrows();
+        },
+        [autoHideArrows],
+    );
+
+    useEffect(() => {
+        autoHideArrows();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, []);
+
+    return [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows];
+}
+
+export default useCarouselArrows;

From 02ebe8a0b15a2c482de6400e1d528f680ea60533 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 22:48:26 +0200
Subject: [PATCH 078/133] simplify

---
 ...nts.js => extractAttachmentsFromReport.js} |  5 +-
 .../Attachments/AttachmentCarousel/index.js   | 85 +++++++++++++------
 .../AttachmentCarousel/index.native.js        |  6 +-
 3 files changed, 67 insertions(+), 29 deletions(-)
 rename src/components/Attachments/AttachmentCarousel/{extractAttachments.js => extractAttachmentsFromReport.js} (94%)

diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
similarity index 94%
rename from src/components/Attachments/AttachmentCarousel/extractAttachments.js
rename to src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
index e1460ca8ebc0..ceb766a4c6e6 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachments.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
@@ -11,7 +11,7 @@ import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';
  * @param {String} source
  * @returns {{attachments: Array, initialPage: Number, initialItem: Object, initialActiveSource: String}}
  */
-function extractAttachments(report, reportActions, source) {
+function extractAttachmentsFromReport(report, reportActions, source) {
     const actions = [ReportActionsUtils.getParentReportAction(report), ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
     let attachments = [];
 
@@ -42,6 +42,7 @@ function extractAttachments(report, reportActions, source) {
     htmlParser.end();
 
     attachments = attachments.reverse();
+
     const initialPage = _.findIndex(attachments, (a) => a.source === source);
     if (initialPage === -1) {
         throw new Error('Attachment not found');
@@ -57,4 +58,4 @@ function extractAttachments(report, reportActions, source) {
     };
 }
 
-export default extractAttachments;
+export default extractAttachmentsFromReport;
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index c5d78fee15a5..e10d37ed46f5 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -1,21 +1,32 @@
 import React, {useRef, useCallback, useState, useEffect, useMemo} from 'react';
 import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
 import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
 import styles from '../../../styles/styles';
 import CarouselActions from './CarouselActions';
 import AttachmentView from '../AttachmentView';
 import withWindowDimensions from '../../withWindowDimensions';
 import CarouselButtons from './CarouselButtons';
-import extractAttachments from './extractAttachments';
+import extractAttachmentsFromReport from './extractAttachmentsFromReport';
 import {attachmentCarouselPropTypes, attachmentCarouselDefaultProps} from './propTypes';
 import ONYXKEYS from '../../../ONYXKEYS';
 import withLocalize from '../../withLocalize';
 import compose from '../../../libs/compose';
+import useCarouselArrows from './useCarouselArrows';
+
+const VIEWABILITY_CONFIG = {
+    // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+    // more than 95% visible. When that happens we update the page index in the state.
+    itemVisiblePercentThreshold: 95,
+};
 
 function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallScreenWidth, windowWidth}) {
     const scrollRef = useRef(null);
 
-    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachments(report, reportActions, source), [report, reportActions, source]);
+    const canUseTouchScreen = useMemo(() => DeviceCapabilities.canUseTouchScreen(), []);
+
+    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachmentsFromReport(report, reportActions, source), [report, reportActions, source]);
 
     useEffect(() => {
         // Update the parent modal's state with the source and name from the mapped attachments
@@ -24,28 +35,32 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
     }, [initialItem]);
 
     const [containerDimensions, setContainerDimensions] = useState({width: 0, height: 0});
-    const [shouldShowArrows, setShouldShowArrows] = useState(false);
+    const [isZoomed, setIsZoomed] = useState(false);
     const [page, setPage] = useState(initialPage);
     const [activeSource, setActiveSource] = useState(initialActiveSource);
+    const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
 
     /**
      * Updates the page state when the user navigates between attachments
      * @param {Object} item
      * @param {number} index
      */
-    const updatePage = useCallback(
-        ({item, index}) => {
+    const updatePage = useRef(
+        ({viewableItems}) => {
             Keyboard.dismiss();
-
-            if (!item) {
+            // Since we can have only one item in view at a time, we can use the first item in the array
+            // to get the index of the current page
+            const entry = _.first(viewableItems);
+            if (!entry) {
                 setActiveSource(null);
                 return;
             }
 
-            setPage(index);
-            setActiveSource(item.source);
+            setPage(entry.index);
+            setActiveSource(entry.item.source);
+            setIsZoomed(false);
 
-            onNavigate(item);
+            onNavigate(entry.item);
         },
         [onNavigate],
     );
@@ -56,18 +71,38 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            const nextIndex = page + deltaSlide;
+            let delta = deltaSlide;
+            if (canUseTouchScreen) {
+                delta = deltaSlide * -1;
+            }
+
+            const nextIndex = page - delta;
             const nextItem = attachments[nextIndex];
 
             if (!nextItem || !scrollRef.current) {
                 return;
             }
 
-            updatePage({item: nextItem, index: nextIndex});
+            scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
+        },
+        [attachments, canUseTouchScreen, page],
+    );
+
+    /**
+     * Updates zoomed state to enable/disable panning the PDF
+     * @param {Number} scale current PDF scale
+     */
+    const updateZoomState = useCallback(
+        (scale) => {
+            const newIsZoomed = scale > 1;
+            if (newIsZoomed === isZoomed) {
+                return;
+            }
 
-            scrollRef.current.scrollToIndex({index: nextIndex, animated: false});
+            setShouldShowArrows(!newIsZoomed);
+            setIsZoomed(newIsZoomed);
         },
-        [attachments, page, updatePage],
+        [isZoomed, setShouldShowArrows],
     );
 
     /**
@@ -118,10 +153,11 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
             <AttachmentView
                 item={item}
                 isFocused={activeSource === item.source}
+                onScaleChanged={canUseTouchScreen ? updateZoomState : undefined}
                 isUsedInCarousel
             />
         ),
-        [activeSource],
+        [activeSource, canUseTouchScreen, updateZoomState],
     );
 
     return (
@@ -130,8 +166,8 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
             onLayout={({nativeEvent}) =>
                 setContainerDimensions({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
             }
-            onMouseEnter={() => setShouldShowArrows(true)}
-            onMouseLeave={() => setShouldShowArrows(false)}
+            onMouseEnter={() => !isZoomed && !canUseTouchScreen && setShouldShowArrows(true)}
+            onMouseLeave={() => !isZoomed && !canUseTouchScreen && setShouldShowArrows(false)}
         >
             <CarouselButtons
                 shouldShowArrows={shouldShowArrows}
@@ -139,9 +175,11 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                 attachments={attachments}
                 onBack={() => cycleThroughAttachments(-1)}
                 onForward={() => cycleThroughAttachments(1)}
+                autoHideArrow={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
             />
 
-            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
+            {containerDimensions.width > 0 && (
                 <FlatList
                     contentContainerStyle={{flex: 1}}
                     listKey="AttachmentCarousel"
@@ -152,12 +190,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                     pagingEnabled
                     snapToAlignment="start"
                     snapToInterval={containerDimensions.width}
-                    // scrollEnabled={false}
-                    // ref={scrollRef}
-                    ref={(ref) => {
-                        scrollRef.current = ref;
-                    }}
-                    initialScrollIndex={initialPage}
+                    // scrollEnabled={canUseTouchScreen && !isZoomed}
+                    ref={scrollRef}
+                    initialScrollIndex={page}
                     initialNumToRender={3}
                     windowSize={5}
                     maxToRenderPerBatch={3}
@@ -166,6 +201,8 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                     renderItem={renderItem}
                     getItemLayout={getItemLayout}
                     keyExtractor={(item) => item.source}
+                    viewabilityConfig={VIEWABILITY_CONFIG}
+                    onViewableItemsChanged={updatePage.current}
                 />
             )}
 
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index aa11c24280af..b73d093af2fc 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -10,11 +10,11 @@ import withLocalize from '../../withLocalize';
 import compose from '../../../libs/compose';
 import withWindowDimensions from '../../withWindowDimensions';
 import {attachmentCarouselPropTypes, attachmentCarouselDefaultProps} from './propTypes';
-import extractAttachments from './extractAttachments';
+import extractAttachmentsFromReport from './extractAttachmentsFromReport';
 import useCarouselArrows from './useCarouselArrows';
 
 function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}) {
-    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachments(report, reportActions, source), [report, reportActions, source]);
+    const {attachments, initialPage, initialActiveSource, initialItem} = useMemo(() => extractAttachmentsFromReport(report, reportActions, source), [report, reportActions, source]);
 
     useEffect(() => {
         // Update the parent modal's state with the source and name from the mapped attachments
@@ -101,7 +101,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                 cancelAutoHideArrow={cancelAutoHideArrows}
             />
 
-            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
+            {containerDimensions.width > 0 && (
                 <AttachmentCarouselPager
                     items={attachments}
                     renderItem={renderItem}

From c153b6b4d465ca6cf2b51589ae0c65e928625a2e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 23:06:32 +0200
Subject: [PATCH 079/133] last fixes

---
 .../Attachments/AttachmentCarousel/index.js         | 13 ++++++++++---
 .../Attachments/AttachmentCarousel/index.native.js  |  2 +-
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index e10d37ed46f5..88545d71a29c 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -48,6 +48,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
     const updatePage = useRef(
         ({viewableItems}) => {
             Keyboard.dismiss();
+
             // Since we can have only one item in view at a time, we can use the first item in the array
             // to get the index of the current page
             const entry = _.first(viewableItems);
@@ -154,10 +155,11 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                 item={item}
                 isFocused={activeSource === item.source}
                 onScaleChanged={canUseTouchScreen ? updateZoomState : undefined}
+                onPress={() => setShouldShowArrows(!shouldShowArrows)}
                 isUsedInCarousel
             />
         ),
-        [activeSource, canUseTouchScreen, updateZoomState],
+        [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows, updateZoomState],
     );
 
     return (
@@ -181,16 +183,21 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
 
             {containerDimensions.width > 0 && (
                 <FlatList
-                    contentContainerStyle={{flex: 1}}
+                    keyboardShouldPersistTaps="handled"
                     listKey="AttachmentCarousel"
                     horizontal
                     decelerationRate="fast"
                     showsHorizontalScrollIndicator={false}
                     bounces={false}
+                    // Scroll only one image at a time no matter how fast the user swipes
+                    disableIntervalMomentum
                     pagingEnabled
                     snapToAlignment="start"
                     snapToInterval={containerDimensions.width}
-                    // scrollEnabled={canUseTouchScreen && !isZoomed}
+                    // Enable scrolling by swiping on mobile (touch) devices only
+                    // disable scroll for desktop/browsers because they add their scrollbars
+                    // Enable scrolling FlatList only when PDF is not in a zoomed state
+                    scrollEnabled={canUseTouchScreen && !isZoomed}
                     ref={scrollRef}
                     initialScrollIndex={page}
                     initialNumToRender={3}
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index b73d093af2fc..d608110457f6 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -76,7 +76,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                 item={item}
                 isFocused={activeSource === item.source}
                 isUsedInCarousel
-                onPress={() => (shouldShowArrows ? setShouldShowArrows(false) : setShouldShowArrows(true))}
+                onPress={() => setShouldShowArrows(!shouldShowArrows)}
             />
         ),
         [activeSource, setShouldShowArrows, shouldShowArrows],

From 7358b4c78fc3c47dfa7e7dcb296f983880851e7a Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 23:09:23 +0200
Subject: [PATCH 080/133] only disable arrows on tap on touchscreen devices

---
 src/components/Attachments/AttachmentCarousel/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 88545d71a29c..8e30505c41e6 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -155,7 +155,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                 item={item}
                 isFocused={activeSource === item.source}
                 onScaleChanged={canUseTouchScreen ? updateZoomState : undefined}
-                onPress={() => setShouldShowArrows(!shouldShowArrows)}
+                onPress={() => canUseTouchScreen && setShouldShowArrows(!shouldShowArrows)}
                 isUsedInCarousel
             />
         ),

From 58146204c2845dbe2d8983bec166d873b9cd0192 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 19 Jul 2023 23:28:31 +0200
Subject: [PATCH 081/133] fix: remaining issues

---
 .../Attachments/AttachmentCarousel/index.js   | 51 +++++--------------
 .../AttachmentCarousel/index.native.js        |  2 +-
 2 files changed, 13 insertions(+), 40 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 8e30505c41e6..a93901220f45 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -34,8 +34,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [initialItem]);
 
-    const [containerDimensions, setContainerDimensions] = useState({width: 0, height: 0});
-    const [isZoomed, setIsZoomed] = useState(false);
+    const [containerWidth, setContainerWidth] = useState(0);
     const [page, setPage] = useState(initialPage);
     const [activeSource, setActiveSource] = useState(initialActiveSource);
     const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
@@ -59,7 +58,6 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
 
             setPage(entry.index);
             setActiveSource(entry.item.source);
-            setIsZoomed(false);
 
             onNavigate(entry.item);
         },
@@ -72,12 +70,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
      */
     const cycleThroughAttachments = useCallback(
         (deltaSlide) => {
-            let delta = deltaSlide;
-            if (canUseTouchScreen) {
-                delta = deltaSlide * -1;
-            }
-
-            const nextIndex = page - delta;
+            const nextIndex = page + deltaSlide;
             const nextItem = attachments[nextIndex];
 
             if (!nextItem || !scrollRef.current) {
@@ -89,23 +82,6 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
         [attachments, canUseTouchScreen, page],
     );
 
-    /**
-     * Updates zoomed state to enable/disable panning the PDF
-     * @param {Number} scale current PDF scale
-     */
-    const updateZoomState = useCallback(
-        (scale) => {
-            const newIsZoomed = scale > 1;
-            if (newIsZoomed === isZoomed) {
-                return;
-            }
-
-            setShouldShowArrows(!newIsZoomed);
-            setIsZoomed(newIsZoomed);
-        },
-        [isZoomed, setShouldShowArrows],
-    );
-
     /**
      * Calculate items layout information to optimize scrolling performance
      * @param {*} data
@@ -114,11 +90,11 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
      */
     const getItemLayout = useCallback(
         (_data, index) => ({
-            length: containerDimensions.width,
-            offset: containerDimensions.height * index,
+            length: containerWidth,
+            offset: containerWidth * index,
             index,
         }),
-        [containerDimensions.height, containerDimensions.width],
+        [containerWidth],
     );
 
     /**
@@ -154,22 +130,19 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
             <AttachmentView
                 item={item}
                 isFocused={activeSource === item.source}
-                onScaleChanged={canUseTouchScreen ? updateZoomState : undefined}
                 onPress={() => canUseTouchScreen && setShouldShowArrows(!shouldShowArrows)}
                 isUsedInCarousel
             />
         ),
-        [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows, updateZoomState],
+        [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows],
     );
 
     return (
         <View
             style={[styles.flex1, styles.attachmentCarouselContainer]}
-            onLayout={({nativeEvent}) =>
-                setContainerDimensions({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)})
-            }
-            onMouseEnter={() => !isZoomed && !canUseTouchScreen && setShouldShowArrows(true)}
-            onMouseLeave={() => !isZoomed && !canUseTouchScreen && setShouldShowArrows(false)}
+            onLayout={({nativeEvent}) => setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width))}
+            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
+            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
         >
             <CarouselButtons
                 shouldShowArrows={shouldShowArrows}
@@ -181,7 +154,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                 cancelAutoHideArrow={cancelAutoHideArrows}
             />
 
-            {containerDimensions.width > 0 && (
+            {containerWidth > 0 && (
                 <FlatList
                     keyboardShouldPersistTaps="handled"
                     listKey="AttachmentCarousel"
@@ -193,11 +166,11 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
                     disableIntervalMomentum
                     pagingEnabled
                     snapToAlignment="start"
-                    snapToInterval={containerDimensions.width}
+                    snapToInterval={containerWidth}
                     // Enable scrolling by swiping on mobile (touch) devices only
                     // disable scroll for desktop/browsers because they add their scrollbars
                     // Enable scrolling FlatList only when PDF is not in a zoomed state
-                    scrollEnabled={canUseTouchScreen && !isZoomed}
+                    scrollEnabled={canUseTouchScreen}
                     ref={scrollRef}
                     initialScrollIndex={page}
                     initialNumToRender={3}
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index d608110457f6..1d64fe64c01b 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -101,7 +101,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
                 cancelAutoHideArrow={cancelAutoHideArrows}
             />
 
-            {containerDimensions.width > 0 && (
+            {containerDimensions.width > 0 && containerDimensions.height > 0 && (
                 <AttachmentCarouselPager
                     items={attachments}
                     renderItem={renderItem}

From 6571838c2730101b504aa9201556d19300d4e4bd Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 20 Jul 2023 13:01:27 +0200
Subject: [PATCH 082/133] fix: AttachmentView issues propTypes

---
 .../Attachments/AttachmentCarousel/index.js   |  4 +-
 .../AttachmentCarousel/index.native.js        |  4 +-
 .../AttachmentViewImage/index.js              | 10 ++---
 .../AttachmentViewImage/index.native.js       | 14 +++----
 .../AttachmentView/AttachmentViewPdf/index.js |  4 +-
 .../AttachmentViewPdf/index.native.js         |  4 +-
 .../AttachmentViewPdf/propTypes.js            | 11 ++++--
 .../Attachments/AttachmentView/index.js       | 37 ++++++++++++++-----
 .../Attachments/AttachmentView/propTypes.js   | 18 +++++----
 src/components/Attachments/propTypes.js       | 13 ++++---
 10 files changed, 76 insertions(+), 43 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 8513b54235b4..a4077bd7d731 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -132,7 +132,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, isSmallS
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
-                item={item}
+                source={item.source}
+                file={item.file}
+                isAuthTokenRequired={item.isAuthTokenRequired}
                 isFocused={activeSource === item.source}
                 onPress={() => canUseTouchScreen && setShouldShowArrows(!shouldShowArrows)}
                 isUsedInCarousel
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 1d64fe64c01b..73dd32d8be30 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -73,7 +73,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose}
     const renderItem = useCallback(
         ({item}) => (
             <AttachmentView
-                item={item}
+                source={item.source}
+                file={item.file}
+                isAuthTokenRequired={item.isAuthTokenRequired}
                 isFocused={activeSource === item.source}
                 isUsedInCarousel
                 onPress={() => setShouldShowArrows(!shouldShowArrows)}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
index 4329fe24e7d6..7de4417a4efc 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
@@ -12,13 +12,13 @@ const propTypes = {
     ...withLocalizePropTypes,
 };
 
-function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChanged, translate}) {
+function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, onPress, isImage, onScaleChanged, translate}) {
     const children = (
         <ImageView
             onScaleChanged={onScaleChanged}
-            url={item.source}
-            fileName={item.file.name}
-            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+            url={source}
+            fileName={file.name}
+            isAuthTokenRequired={isImage && isAuthTokenRequired}
         />
     );
     return onPress ? (
@@ -27,7 +27,7 @@ function AttachmentViewImage({item, loadComplete, onPress, isImage, onScaleChang
             disabled={loadComplete}
             style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
             accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
-            accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
+            accessibilityLabel={file.name || translate('attachmentView.unknownFilename')}
         >
             {children}
         </PressableWithoutFeedback>
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
index 70c66fa05dd8..77dca3d9b5ae 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -13,19 +13,19 @@ const propTypes = {
     ...withLocalizePropTypes,
 };
 
-function AttachmentViewImage({item, isFocused, isUsedInCarousel, loadComplete, onPress, isImage, onScaleChanged, translate}) {
+function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUsedInCarousel, loadComplete, onPress, isImage, onScaleChanged, translate}) {
     const children = isUsedInCarousel ? (
         <AttachmentCarouselPage
-            item={item}
+            item={{source, file, isAuthTokenRequired}}
             isActive={isFocused}
-            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+            isAuthTokenRequired={isImage && isAuthTokenRequired}
         />
     ) : (
         <ImageView
             onScaleChanged={onScaleChanged}
-            url={item.source}
-            fileName={item.file.name}
-            isAuthTokenRequired={isImage && item.isAuthTokenRequired}
+            url={source}
+            fileName={file.name}
+            isAuthTokenRequired={isImage && isAuthTokenRequired}
         />
     );
 
@@ -35,7 +35,7 @@ function AttachmentViewImage({item, isFocused, isUsedInCarousel, loadComplete, o
             disabled={loadComplete}
             style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
             accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
-            accessibilityLabel={item.file.name || translate('attachmentView.unknownFilename')}
+            accessibilityLabel={file.name || translate('attachmentView.unknownFilename')}
         >
             {children}
         </PressableWithoutFeedback>
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
index b5a80554883d..fc17f79a0aaa 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js
@@ -3,13 +3,13 @@ import styles from '../../../../styles/styles';
 import {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps} from './propTypes';
 import PDFView from '../../../PDFView';
 
-function AttachmentViewPdf({item, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete}) {
+function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete}) {
     return (
         <PDFView
             onPress={onPress}
             isFocused={isFocused}
             sourceURL={encryptedSourceUrl}
-            fileName={item.file.name}
+            fileName={file.name}
             style={styles.imageModalPDF}
             onToggleKeyboard={onToggleKeyboard}
             onScaleChanged={onScaleChanged}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
index 47e26fd09f8e..663dcc22391c 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
@@ -5,7 +5,7 @@ import PDFView from '../../../PDFView';
 
 import AttachmentCarouselPagerContext from '../../AttachmentCarouselPager/AttachmentCarouselPagerContext';
 
-function AttachmentViewPdf({item, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete}) {
+function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete}) {
     const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
 
     const onScaleChanged = useCallback(
@@ -29,7 +29,7 @@ function AttachmentViewPdf({item, encryptedSourceUrl, isFocused, isUsedInCarouse
             onPress={onPress}
             isFocused={isFocused}
             sourceURL={encryptedSourceUrl}
-            fileName={item.file.name}
+            fileName={file.name}
             style={styles.imageModalPDF}
             onToggleKeyboard={onToggleKeyboard}
             onScaleChanged={onScaleChanged}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
index 91d298979831..32169757e839 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
@@ -1,14 +1,19 @@
 import PropTypes from 'prop-types';
-import {attachmentViewPropTypes, attachmentViewDefaultProps} from '../propTypes';
+import * as AttachmentsPropTypes from '../propTypes';
 
 const attachmentViewPdfPropTypes = {
-    ...attachmentViewPropTypes,
+    /** File object maybe be instance of File or Object */
+    file: AttachmentsPropTypes.attachmentFilePropType.isRequired,
 
     encryptedSourceUrl: PropTypes.string.isRequired,
     onToggleKeyboard: PropTypes.func.isRequired,
     onLoadComplete: PropTypes.func.isRequired,
 };
 
-const attachmentViewPdfDefaultProps = attachmentViewDefaultProps;
+const attachmentViewPdfDefaultProps = {
+    file: {
+        name: '',
+    },
+};
 
 export {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps};
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index 9bf277350328..d3e623ac1a47 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -44,14 +44,27 @@ const defaultProps = {
     containerStyles: [],
 };
 
-function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, onScaleChanged, onToggleKeyboard, translate, isFocused}) {
+function AttachmentView({
+    source,
+    file,
+    isAuthTokenRequired,
+    isUsedInCarousel,
+    onPress,
+    shouldShowLoadingSpinnerIcon,
+    shouldShowDownloadIcon,
+    containerStyles,
+    onScaleChanged,
+    onToggleKeyboard,
+    translate,
+    isFocused,
+}) {
     const [loadComplete, setLoadComplete] = useState(false);
 
     // Handles case where source is a component (ex: SVG)
-    if (_.isFunction(item.source)) {
+    if (_.isFunction(source)) {
         return (
             <Icon
-                src={item.source}
+                src={source}
                 height={variables.defaultAvatarPreviewSize}
                 width={variables.defaultAvatarPreviewSize}
             />
@@ -60,12 +73,14 @@ function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinn
 
     // Check both source and file.name since PDFs dragged into the the text field
     // will appear with a source that is a blob
-    if (Str.isPDF(item.source) || (item.file && Str.isPDF(item.file.name || translate('attachmentView.unknownFilename')))) {
-        const encryptedSourceUrl = item.isAuthTokenRequired ? addEncryptedAuthTokenToURL(item.source) : item.source;
+    if ((source != null && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) {
+        const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source;
 
         return (
             <AttachmentViewPdf
-                item={item}
+                source={source}
+                file={file}
+                isAuthTokenRequired={isAuthTokenRequired}
                 encryptedSourceUrl={encryptedSourceUrl}
                 isUsedInCarousel={isUsedInCarousel}
                 isFocused={isFocused}
@@ -79,11 +94,13 @@ function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinn
 
     // For this check we use both source and file.name since temporary file source is a blob
     // both PDFs and images will appear as images when pasted into the the text field
-    const isImage = Str.isImage(item.source);
-    if (isImage || (item.file && Str.isImage(item.file.name))) {
+    const isImage = source != null && Str.isImage(source);
+    if (isImage || (file && Str.isImage(file.name))) {
         return (
             <AttachmentViewImage
-                item={item}
+                source={source}
+                file={file}
+                isAuthTokenRequired={isAuthTokenRequired}
                 isUsedInCarousel={isUsedInCarousel}
                 loadComplete={loadComplete}
                 isFocused={isFocused}
@@ -100,7 +117,7 @@ function AttachmentView({item, isUsedInCarousel, onPress, shouldShowLoadingSpinn
                 <Icon src={Expensicons.Paperclip} />
             </View>
 
-            <Text style={[styles.textStrong, styles.flexShrink1, styles.breakAll, styles.flexWrap, styles.mw100]}>{item.file && item.file.name}</Text>
+            <Text style={[styles.textStrong, styles.flexShrink1, styles.breakAll, styles.flexWrap, styles.mw100]}>{file && file.name}</Text>
             {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
                 <Tooltip text={translate('common.download')}>
                     <View style={styles.ml2}>
diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js
index 021715b944a1..c9f42861fb54 100644
--- a/src/components/Attachments/AttachmentView/propTypes.js
+++ b/src/components/Attachments/AttachmentView/propTypes.js
@@ -2,8 +2,14 @@ import PropTypes from 'prop-types';
 import * as AttachmentsPropTypes from '../propTypes';
 
 const attachmentViewPropTypes = {
-    /** Attachment to display */
-    item: AttachmentsPropTypes.attachmentPropType,
+    /** Whether source url requires authentication */
+    isAuthTokenRequired: PropTypes.bool,
+
+    /** URL to full-sized attachment or SVG function */
+    source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
+
+    /** File object maybe be instance of File or Object */
+    file: AttachmentsPropTypes.attachmentFilePropType,
 
     /** Whether this view is the active screen  */
     isFocused: PropTypes.bool,
@@ -19,11 +25,9 @@ const attachmentViewPropTypes = {
 };
 
 const attachmentViewDefaultProps = {
-    item: {
-        isAuthTokenRequired: false,
-        file: {
-            name: '',
-        },
+    isAuthTokenRequired: false,
+    file: {
+        name: '',
     },
     isFocused: false,
     isUsedInCarousel: false,
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
index 0abbaa1254e6..b3f0af6ab217 100644
--- a/src/components/Attachments/propTypes.js
+++ b/src/components/Attachments/propTypes.js
@@ -1,18 +1,21 @@
 import PropTypes from 'prop-types';
 
+const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func]);
+const attachmentFilePropType = PropTypes.shape({
+    name: PropTypes.string,
+});
+
 const attachmentPropType = PropTypes.shape({
     /** Whether source url requires authentication */
     isAuthTokenRequired: PropTypes.bool,
 
     /** URL to full-sized attachment or SVG function */
-    source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
+    source: attachmentSourcePropType.isRequired,
 
     /** File object maybe be instance of File or Object */
-    file: PropTypes.shape({
-        name: PropTypes.string,
-    }),
+    file: attachmentFilePropType,
 });
 
 const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
 
-export {attachmentPropType, attachmentsPropType};
+export {attachmentSourcePropType, attachmentFilePropType, attachmentPropType, attachmentsPropType};

From eff55e300115403e1dc404bf08cbdc405a0cd657 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 20 Jul 2023 13:01:55 +0200
Subject: [PATCH 083/133] remove null check

---
 src/components/Attachments/AttachmentView/index.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index d3e623ac1a47..3ad643d34bcd 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -73,7 +73,7 @@ function AttachmentView({
 
     // Check both source and file.name since PDFs dragged into the the text field
     // will appear with a source that is a blob
-    if ((source != null && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) {
+    if (Str.isPDF(source) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) {
         const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source;
 
         return (
@@ -94,7 +94,7 @@ function AttachmentView({
 
     // For this check we use both source and file.name since temporary file source is a blob
     // both PDFs and images will appear as images when pasted into the the text field
-    const isImage = source != null && Str.isImage(source);
+    const isImage = Str.isImage(source);
     if (isImage || (file && Str.isImage(file.name))) {
         return (
             <AttachmentViewImage

From 263eb6af1c0d421fee442bb2a68f67b24939333f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 20 Jul 2023 13:04:22 +0200
Subject: [PATCH 084/133] fix: wrong import

---
 .../Attachments/AttachmentView/AttachmentViewPdf/propTypes.js   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
index 32169757e839..ea17cd9490b3 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js
@@ -1,5 +1,5 @@
 import PropTypes from 'prop-types';
-import * as AttachmentsPropTypes from '../propTypes';
+import * as AttachmentsPropTypes from '../../propTypes';
 
 const attachmentViewPdfPropTypes = {
     /** File object maybe be instance of File or Object */

From fff9fa4289e8e107500788fed1a3e70fc2dcbc67 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 20 Jul 2023 13:14:15 +0200
Subject: [PATCH 085/133] fix types and restructure

---
 .../Pager}/AttachmentCarouselPage.js          | 35 +++++++++++++------
 .../Pager}/AttachmentCarouselPagerContext.js  |  0
 .../Pager}/ImageTransformer.js                |  2 +-
 .../Pager}/ImageWrapper.js                    |  2 +-
 .../Pager}/index.js                           |  2 +-
 .../AttachmentCarousel/index.native.js        |  2 +-
 .../AttachmentViewImage/index.native.js       |  7 ++--
 7 files changed, 33 insertions(+), 17 deletions(-)
 rename src/components/Attachments/{AttachmentCarouselPager => AttachmentCarousel/Pager}/AttachmentCarouselPage.js (85%)
 rename src/components/Attachments/{AttachmentCarouselPager => AttachmentCarousel/Pager}/AttachmentCarouselPagerContext.js (100%)
 rename src/components/Attachments/{AttachmentCarouselPager => AttachmentCarousel/Pager}/ImageTransformer.js (99%)
 rename src/components/Attachments/{AttachmentCarouselPager => AttachmentCarousel/Pager}/ImageWrapper.js (92%)
 rename src/components/Attachments/{AttachmentCarouselPager => AttachmentCarousel/Pager}/index.js (99%)

diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
similarity index 85%
rename from src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
rename to src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index aa241f0a80ce..f7103e2a3412 100644
--- a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -2,11 +2,11 @@
 import React, {useContext, useEffect, useState, useMemo} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
-import Image from '../../Image';
+import Image from '../../../Image';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageTransformer from './ImageTransformer';
 import ImageWrapper from './ImageWrapper';
-import * as AttachmentsPropTypes from '../propTypes';
+import * as AttachmentsPropTypes from '../../propTypes';
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
     const scaleFactorX = canvasWidth / imageWidth;
@@ -18,15 +18,29 @@ function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight})
 const cachedDimensions = new Map();
 
 const pagePropTypes = {
+    /** Whether source url requires authentication */
+    isAuthTokenRequired: PropTypes.bool,
+
+    /** URL to full-sized attachment or SVG function */
+    source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
+
+    /** File object maybe be instance of File or Object */
+    file: AttachmentsPropTypes.attachmentFilePropType,
+
     isActive: PropTypes.bool.isRequired,
-    item: AttachmentsPropTypes.attachmentPropType.isRequired,
-    isAuthTokenRequired: PropTypes.bool.isRequired,
 };
 
-function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequired}) {
+const defaultProps = {
+    isAuthTokenRequired: false,
+    file: {
+        name: '',
+    },
+};
+
+function AttachmentCarouselPage({source, file, isAuthTokenRequired, isActive: isActiveProp}) {
     const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
-    const dimensions = cachedDimensions.get(item.source);
+    const dimensions = cachedDimensions.get(source);
 
     const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0;
 
@@ -66,7 +80,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
                         imageScale={dimensions?.imageScale}
                     >
                         <Image
-                            source={{uri: item.source}}
+                            source={{uri: source}}
                             style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
                             onLoad={(evt) => {
@@ -77,7 +91,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
 
                                 // Don't update the dimensions if they are already set
                                 if (dimensions?.imageWidth !== imageWidth || dimensions?.imageHeight !== imageHeight || dimensions?.imageScale !== imageScale) {
-                                    cachedDimensions.set(item.source, {
+                                    cachedDimensions.set(source, {
                                         ...dimensions,
                                         imageWidth,
                                         imageHeight,
@@ -97,7 +111,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
             {(!isActive || showFallback) && (
                 <ImageWrapper>
                     <Image
-                        source={{uri: item.source}}
+                        source={{uri: source}}
                         isAuthTokenRequired={isAuthTokenRequired}
                         onLoad={(evt) => {
                             const imageWidth = evt.nativeEvent.width;
@@ -111,7 +125,7 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
                             // Don't update the dimensions if they are already set
                             if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return;
 
-                            cachedDimensions.set(item.source, {
+                            cachedDimensions.set(source, {
                                 ...dimensions,
                                 scaledImageWidth,
                                 scaledImageHeight,
@@ -133,5 +147,6 @@ function AttachmentCarouselPage({isActive: isActiveProp, item, isAuthTokenRequir
     );
 }
 AttachmentCarouselPage.propTypes = pagePropTypes;
+AttachmentCarouselPage.defaultProps = defaultProps;
 
 export default AttachmentCarouselPage;
diff --git a/src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js
similarity index 100%
rename from src/components/Attachments/AttachmentCarouselPager/AttachmentCarouselPagerContext.js
rename to src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js
diff --git a/src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
similarity index 99%
rename from src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js
rename to src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index e6ba9d28cd43..bf35ceeacad9 100644
--- a/src/components/Attachments/AttachmentCarouselPager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -15,7 +15,7 @@ import Animated, {
     withDecay,
     withSpring,
 } from 'react-native-reanimated';
-import styles from '../../../styles/styles';
+import styles from '../../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
diff --git a/src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageWrapper.js
similarity index 92%
rename from src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js
rename to src/components/Attachments/AttachmentCarousel/Pager/ImageWrapper.js
index 740f0d3367e4..a6a935bbba01 100644
--- a/src/components/Attachments/AttachmentCarouselPager/ImageWrapper.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageWrapper.js
@@ -3,7 +3,7 @@ import React from 'react';
 import {StyleSheet} from 'react-native';
 import PropTypes from 'prop-types';
 import Animated from 'react-native-reanimated';
-import styles from '../../../styles/styles';
+import styles from '../../../../styles/styles';
 
 const imageWrapperPropTypes = {
     children: PropTypes.node.isRequired,
diff --git a/src/components/Attachments/AttachmentCarouselPager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js
similarity index 99%
rename from src/components/Attachments/AttachmentCarouselPager/index.js
rename to src/components/Attachments/AttachmentCarousel/Pager/index.js
index 96703b659a57..3cc57b795b7b 100644
--- a/src/components/Attachments/AttachmentCarouselPager/index.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js
@@ -6,7 +6,7 @@ import {GestureHandlerRootView, createNativeWrapper} from 'react-native-gesture-
 import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated';
 import PagerView from 'react-native-pager-view';
 import _ from 'underscore';
-import styles from '../../../styles/styles';
+import styles from '../../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 
 const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 73dd32d8be30..f992f1f8e6cc 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -1,7 +1,7 @@
 import React, {useCallback, useEffect, useRef, useState, useMemo} from 'react';
 import {View, Keyboard, PixelRatio} from 'react-native';
 import {withOnyx} from 'react-native-onyx';
-import AttachmentCarouselPager from '../AttachmentCarouselPager';
+import AttachmentCarouselPager from './Pager';
 import styles from '../../../styles/styles';
 import CarouselButtons from './CarouselButtons';
 import AttachmentView from '../AttachmentView';
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
index 77dca3d9b5ae..7334e5391bc1 100755
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js
@@ -5,7 +5,7 @@ import ImageView from '../../../ImageView';
 import compose from '../../../../libs/compose';
 import PressableWithoutFeedback from '../../../Pressable/PressableWithoutFeedback';
 import CONST from '../../../../CONST';
-import AttachmentCarouselPage from '../../AttachmentCarouselPager/AttachmentCarouselPage';
+import AttachmentCarouselPage from '../../AttachmentCarousel/Pager/AttachmentCarouselPage';
 import {attachmentViewImagePropTypes, attachmentViewImageDefaultProps} from './propTypes';
 
 const propTypes = {
@@ -16,9 +16,10 @@ const propTypes = {
 function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUsedInCarousel, loadComplete, onPress, isImage, onScaleChanged, translate}) {
     const children = isUsedInCarousel ? (
         <AttachmentCarouselPage
-            item={{source, file, isAuthTokenRequired}}
-            isActive={isFocused}
+            source={source}
             isAuthTokenRequired={isImage && isAuthTokenRequired}
+            file={file}
+            isActive={isFocused}
         />
     ) : (
         <ImageView

From 67ab08545865b8aead433db788e214b515faec21 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 20 Jul 2023 13:16:44 +0200
Subject: [PATCH 086/133] remove unused prop for now

---
 .../AttachmentCarousel/Pager/AttachmentCarouselPage.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index f7103e2a3412..e99d9a789e7d 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -37,7 +37,7 @@ const defaultProps = {
     },
 };
 
-function AttachmentCarouselPage({source, file, isAuthTokenRequired, isActive: isActiveProp}) {
+function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActiveProp}) {
     const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
     const dimensions = cachedDimensions.get(source);

From feda2e43afc6bd331edd9fbdf1917c50efd2d3f4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sat, 22 Jul 2023 21:14:22 +0200
Subject: [PATCH 087/133] adapt min/max scales by initial image scale

---
 .../Pager/ImageTransformer.js                 | 23 +++++++++++--------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index bf35ceeacad9..56151bfd79c5 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -1,5 +1,5 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {useContext, useEffect, useRef, useState} from 'react';
+import React, {useContext, useEffect, useRef, useState, useMemo} from 'react';
 import {StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
@@ -20,6 +20,7 @@ import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
 const DOUBLE_TAP_SCALE = 3;
+const DEFAULT_INITIAL_SCALE = 1;
 const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
 
@@ -56,10 +57,14 @@ const imageTransformerDefaultProps = {
 function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
     const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
 
+    const initialScale = useMemo(() => DEFAULT_INITIAL_SCALE * imageScale, [imageScale]);
+    const minScale = useMemo(() => MIN_SCALE * imageScale, [imageScale]);
+    const maxScale = useMemo(() => MAX_SCALE * imageScale, [imageScale]);
+
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
     // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + imageScale - 1, 0), [imageScale]);
+    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + initialScale - 1, 0), []);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
     const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
@@ -156,7 +161,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
         const deceleration = 0.9915;
 
         if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_SCALE) {
+            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= maxScale) {
                 offsetX.value = withDecay({
                     velocity: panVelocityX.value,
                     clamp: [minVector.x, maxVector.x],
@@ -171,7 +176,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
         if (isInBoundaryY) {
             if (
                 Math.abs(panVelocityY.value) > 0 &&
-                zoomScale.value <= MAX_SCALE &&
+                zoomScale.value <= maxScale &&
                 // limit vertical pan only when image is smaller than screen
                 offsetY.value !== minVector.y &&
                 offsetY.value !== maxVector.y
@@ -434,9 +439,9 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            zoomScale.value = clamp(scaleOffset.value * evt.scale, MIN_SCALE, MAX_SCALE);
+            zoomScale.value = clamp(scaleOffset.value * evt.scale, minScale, maxScale);
 
-            if (zoomScale.value > MIN_SCALE && zoomScale.value < MAX_SCALE) {
+            if (zoomScale.value > minScale && zoomScale.value < maxScale) {
                 gestureScale.value = evt.scale;
             }
 
@@ -459,9 +464,9 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
 
                 // this runs the timing animation
                 zoomScale.value = withSpring(1, SPRING_CONFIG);
-            } else if (scaleOffset.value > MAX_SCALE) {
-                scaleOffset.value = MAX_SCALE;
-                zoomScale.value = withSpring(MAX_SCALE, SPRING_CONFIG);
+            } else if (scaleOffset.value > maxScale) {
+                scaleOffset.value = maxScale;
+                zoomScale.value = withSpring(maxScale, SPRING_CONFIG);
             }
 
             afterGesture();

From d16e5a444aebd9218cbe7fbeef7aea92daaa2e2f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sat, 22 Jul 2023 21:19:32 +0200
Subject: [PATCH 088/133] adjust double tap scale too

---
 .../AttachmentCarousel/Pager/ImageTransformer.js           | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 56151bfd79c5..60cfab951c11 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -19,10 +19,10 @@ import styles from '../../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
-const DOUBLE_TAP_SCALE = 3;
 const DEFAULT_INITIAL_SCALE = 1;
 const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
+const DOUBLE_TAP_SCALE = 3;
 
 const SPRING_CONFIG = {
     mass: 3,
@@ -60,6 +60,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
     const initialScale = useMemo(() => DEFAULT_INITIAL_SCALE * imageScale, [imageScale]);
     const minScale = useMemo(() => MIN_SCALE * imageScale, [imageScale]);
     const maxScale = useMemo(() => MAX_SCALE * imageScale, [imageScale]);
+    const doubleTapScale = useMemo(() => DOUBLE_TAP_SCALE * imageScale, [imageScale]);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
@@ -248,8 +249,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
 
             offsetX.value = withSpring(target.x, SPRING_CONFIG);
             offsetY.value = withSpring(target.y, SPRING_CONFIG);
-            zoomScale.value = withSpring(DOUBLE_TAP_SCALE, SPRING_CONFIG);
-            scaleOffset.value = DOUBLE_TAP_SCALE;
+            zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG);
+            scaleOffset.value = doubleTapScale;
         },
         [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight],
     );

From 9fc39938f5d2e2c4c82db90bb09eba1a5a3d621e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sat, 22 Jul 2023 21:23:29 +0200
Subject: [PATCH 089/133] fix: wrong import

---
 .../AttachmentView/AttachmentViewPdf/index.native.js           | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
index 663dcc22391c..5d7fd73e47fb 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
@@ -2,8 +2,7 @@ import React, {memo, useCallback, useContext} from 'react';
 import styles from '../../../../styles/styles';
 import {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps} from './propTypes';
 import PDFView from '../../../PDFView';
-
-import AttachmentCarouselPagerContext from '../../AttachmentCarouselPager/AttachmentCarouselPagerContext';
+import AttachmentCarouselPagerContext from '../../AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
 
 function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete}) {
     const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);

From fdac91d07c5ecab8ac32450dbb77685e1eb573d2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sat, 22 Jul 2023 21:48:11 +0200
Subject: [PATCH 090/133] fix: use doubleTapScale

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 60cfab951c11..125b4d3342b4 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -212,8 +212,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
             };
 
             const targetImageSize = {
-                x: usableImage.x * DOUBLE_TAP_SCALE,
-                y: usableImage.y * DOUBLE_TAP_SCALE,
+                x: usableImage.x * doubleTapScale,
+                y: usableImage.y * doubleTapScale,
             };
 
             const CENTER = {

From 4e81df31c52c3cefd6fd1e3a25f822126d9891cc Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sat, 22 Jul 2023 22:27:38 +0200
Subject: [PATCH 091/133] simplify scaling logic

---
 .../AttachmentCarousel/Pager/ImageTransformer.js           | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 125b4d3342b4..8d649d5452ca 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -19,7 +19,6 @@ import styles from '../../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
-const DEFAULT_INITIAL_SCALE = 1;
 const MAX_SCALE = 20;
 const MIN_SCALE = 0.7;
 const DOUBLE_TAP_SCALE = 3;
@@ -57,15 +56,13 @@ const imageTransformerDefaultProps = {
 function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
     const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
 
-    const initialScale = useMemo(() => DEFAULT_INITIAL_SCALE * imageScale, [imageScale]);
     const minScale = useMemo(() => MIN_SCALE * imageScale, [imageScale]);
     const maxScale = useMemo(() => MAX_SCALE * imageScale, [imageScale]);
     const doubleTapScale = useMemo(() => DOUBLE_TAP_SCALE * imageScale, [imageScale]);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
-    // Substracting 1, because both scales have the initial image as the base reference
-    const totalScale = useDerivedValue(() => Math.max(zoomScale.value + initialScale - 1, 0), []);
+    const totalScale = useDerivedValue(() => clamp(zoomScale.value * imageScale, minScale, maxScale), [imageScale]);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
     const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
@@ -507,7 +504,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth
                 {scale: totalScale.value},
             ],
         };
-    }, []);
+    });
 
     // reacts to scale change and enables/disables pager scroll
     useAnimatedReaction(

From ac753aeba4244421233b10f80c915e9c693c279c Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Sun, 23 Jul 2023 15:11:51 +0200
Subject: [PATCH 092/133] remove file from propTypes

---
 .../AttachmentCarousel/Pager/AttachmentCarouselPage.js      | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index e99d9a789e7d..128c0c98b450 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -24,17 +24,11 @@ const pagePropTypes = {
     /** URL to full-sized attachment or SVG function */
     source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
 
-    /** File object maybe be instance of File or Object */
-    file: AttachmentsPropTypes.attachmentFilePropType,
-
     isActive: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
     isAuthTokenRequired: false,
-    file: {
-        name: '',
-    },
 };
 
 function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActiveProp}) {

From 50ac1c12911865eee8527a2d18f578b98d0814a5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 27 Jul 2023 13:14:14 +0200
Subject: [PATCH 093/133] fix: maxScale based on max imageScale

---
 .../Pager/ImageTransformer.js                 | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 8d649d5452ca..92a500e5acbb 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -38,7 +38,8 @@ function clamp(value, lowerBound, upperBound) {
 const imageTransformerPropTypes = {
     imageWidth: PropTypes.number,
     imageHeight: PropTypes.number,
-    imageScale: PropTypes.number,
+    imageScaleX: PropTypes.number,
+    imageScaleY: PropTypes.number,
     scaledImageWidth: PropTypes.number,
     scaledImageHeight: PropTypes.number,
     isActive: PropTypes.bool.isRequired,
@@ -48,21 +49,25 @@ const imageTransformerPropTypes = {
 const imageTransformerDefaultProps = {
     imageWidth: 0,
     imageHeight: 0,
-    imageScale: 1,
+    imageScaleX: 1,
+    imageScaleY: 1,
     scaledImageWidth: 0,
     scaledImageHeight: 0,
 };
 
-function ImageTransformer({imageWidth, imageHeight, imageScale, scaledImageWidth, scaledImageHeight, isActive, children}) {
+function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, scaledImageWidth, scaledImageHeight, isActive, children}) {
     const {canvasWidth, canvasHeight, onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = useContext(AttachmentCarouselPagerContext);
 
-    const minScale = useMemo(() => MIN_SCALE * imageScale, [imageScale]);
-    const maxScale = useMemo(() => MAX_SCALE * imageScale, [imageScale]);
-    const doubleTapScale = useMemo(() => DOUBLE_TAP_SCALE * imageScale, [imageScale]);
+    const minImageScale = useMemo(() => Math.min(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
+    const maxImageScale = useMemo(() => Math.max(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
+
+    const minScale = useMemo(() => MIN_SCALE * minImageScale, [minImageScale]);
+    const maxScale = useMemo(() => MAX_SCALE * maxImageScale, [maxImageScale]);
+    const doubleTapScale = useMemo(() => DOUBLE_TAP_SCALE * minImageScale, [minImageScale]);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
-    const totalScale = useDerivedValue(() => clamp(zoomScale.value * imageScale, minScale, maxScale), [imageScale]);
+    const totalScale = useDerivedValue(() => clamp(zoomScale.value * minImageScale, minScale, maxScale), [minImageScale]);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
     const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);

From 9e7a0d5fcb9f402f8534fdf82c45f0ad54fbae56 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 27 Jul 2023 13:15:59 +0200
Subject: [PATCH 094/133] rename and simplify pinch gesture variables

---
 .../Pager/ImageTransformer.js                 | 56 +++++++++----------
 1 file changed, 27 insertions(+), 29 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 92a500e5acbb..a8abf45f3fd9 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -80,8 +80,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     const isSwiping = useSharedValue(false);
 
     // used for moving fingers when pinching
-    const scaleTranslateX = useSharedValue(0);
-    const scaleTranslateY = useSharedValue(0);
+    const pinchTranslateX = useSharedValue(0);
+    const pinchTranslateY = useSharedValue(0);
 
     // storage for the the origin of the gesture
     const origin = {
@@ -94,7 +94,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     const panVelocityY = useSharedValue(0);
 
     // store scale in between gestures
-    const scaleOffset = useSharedValue(1);
+    const pinchScaleOffset = useSharedValue(1);
 
     // disable pan vertically when image is smaller than screen
     const canPanVertically = useDerivedValue(() => canvasHeight < zoomScaledImageHeight.value, [canvasHeight]);
@@ -146,8 +146,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             offsetY.value === 0 &&
             translateX.value === 0 &&
             translateY.value === 0 &&
-            scaleTranslateX.value === 0 &&
-            scaleTranslateY.value === 0
+            pinchTranslateX.value === 0 &&
+            pinchTranslateY.value === 0
         ) {
             // we don't need to run any animations
             return;
@@ -155,7 +155,6 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
         if (zoomScale.value <= 1) {
             // just center it
-            // reset(true);
             offsetX.value = withSpring(0, SPRING_CONFIG);
             offsetY.value = withSpring(0, SPRING_CONFIG);
             return;
@@ -252,13 +251,13 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             offsetX.value = withSpring(target.x, SPRING_CONFIG);
             offsetY.value = withSpring(target.y, SPRING_CONFIG);
             zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG);
-            scaleOffset.value = doubleTapScale;
+            pinchScaleOffset.value = doubleTapScale;
         },
         [scaledImageWidth, scaledImageHeight, canvasWidth, canvasHeight],
     );
 
     const reset = useWorkletCallback((animated) => {
-        scaleOffset.value = 1;
+        pinchScaleOffset.value = 1;
 
         stopAnimation();
 
@@ -272,8 +271,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             translateY.value = 0;
             offsetX.value = 0;
             offsetY.value = 0;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
+            pinchTranslateX.value = 0;
+            pinchTranslateY.value = 0;
         }
     });
 
@@ -420,8 +419,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     );
 
     // used to store event scale value when we limit scale
-    const gestureScale = useSharedValue(1);
-
+    const pinchGestureScale = useSharedValue(1);
     const pinchGestureRunning = useSharedValue(false);
     const pinchGesture = Gesture.Pinch()
         .onTouchesDown((evt, state) => {
@@ -442,33 +440,33 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            zoomScale.value = clamp(scaleOffset.value * evt.scale, minScale, maxScale);
+            zoomScale.value = clamp(pinchScaleOffset.value * evt.scale, minScale, maxScale);
 
             if (zoomScale.value > minScale && zoomScale.value < maxScale) {
-                gestureScale.value = evt.scale;
+                pinchGestureScale.value = evt.scale;
             }
 
             const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
 
-            scaleTranslateX.value = adjustFocal.x + gestureScale.value * origin.x.value * -1;
-            scaleTranslateY.value = adjustFocal.y + gestureScale.value * origin.y.value * -1;
+            pinchTranslateX.value = adjustFocal.x + pinchGestureScale.value * origin.x.value * -1;
+            pinchTranslateY.value = adjustFocal.y + pinchGestureScale.value * origin.y.value * -1;
         })
         .onEnd(() => {
-            offsetX.value += scaleTranslateX.value;
-            offsetY.value += scaleTranslateY.value;
-            scaleTranslateX.value = 0;
-            scaleTranslateY.value = 0;
-            scaleOffset.value = zoomScale.value;
-            gestureScale.value = 1;
-
-            if (scaleOffset.value < 1) {
+            offsetX.value += pinchTranslateX.value;
+            offsetY.value += pinchTranslateY.value;
+            pinchTranslateX.value = 0;
+            pinchTranslateY.value = 0;
+            pinchScaleOffset.value = zoomScale.value;
+            pinchGestureScale.value = 1;
+
+            if (pinchScaleOffset.value < 1) {
                 // make sure we don't add stuff below the 1
-                scaleOffset.value = 1;
+                pinchScaleOffset.value = 1;
 
                 // this runs the timing animation
                 zoomScale.value = withSpring(1, SPRING_CONFIG);
-            } else if (scaleOffset.value > maxScale) {
-                scaleOffset.value = maxScale;
+            } else if (pinchScaleOffset.value > maxScale) {
+                pinchScaleOffset.value = maxScale;
                 zoomScale.value = withSpring(maxScale, SPRING_CONFIG);
             }
 
@@ -491,8 +489,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]);
 
     const animatedStyles = useAnimatedStyle(() => {
-        const x = scaleTranslateX.value + translateX.value + offsetX.value;
-        const y = scaleTranslateY.value + translateY.value + offsetY.value;
+        const x = pinchTranslateX.value + translateX.value + offsetX.value;
+        const y = pinchTranslateY.value + translateY.value + offsetY.value;
 
         if (isSwiping.value) {
             onSwipe(y);

From ac1a79e602ed9392460cf5188fc61efdab90c3f4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 27 Jul 2023 13:16:16 +0200
Subject: [PATCH 095/133] simplify zoomToCoordinates

---
 .../Pager/ImageTransformer.js                 | 21 +++++++------------
 1 file changed, 8 insertions(+), 13 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index a8abf45f3fd9..52ebc58eb6dd 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -207,31 +207,26 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
             stopAnimation();
 
-            const usableImage = {
-                x: scaledImageWidth,
-                y: scaledImageHeight,
-            };
-
             const targetImageSize = {
-                x: usableImage.x * doubleTapScale,
-                y: usableImage.y * doubleTapScale,
+                width: scaledImageWidth * doubleTapScale,
+                height: scaledImageHeight * doubleTapScale,
             };
 
-            const CENTER = {
+            const canvasCenter = {
                 x: canvasWidth / 2,
                 y: canvasHeight / 2,
             };
 
             const imageCenter = {
-                x: usableImage.x / 2,
-                y: usableImage.y / 2,
+                x: scaledImageWidth / 2,
+                y: scaledImageHeight / 2,
             };
 
             const focal = {x, y};
 
             const currentOrigin = {
-                x: (targetImageSize.x / 2 - CENTER.x) * -1,
-                y: (targetImageSize.y / 2 - CENTER.y) * -1,
+                x: (targetImageSize.width / 2 - canvasCenter.x) * -1,
+                y: (targetImageSize.height / 2 - canvasCenter.y) * -1,
             };
 
             const koef = {
@@ -244,7 +239,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 y: currentOrigin.y * koef.y,
             };
 
-            if (targetImageSize.y < canvasHeight) {
+            if (targetImageSize.height < canvasHeight) {
                 target.y = 0;
             }
 

From 5e6081f81cbd95312a5dcc182ecc5c040dee2cc2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 27 Jul 2023 13:16:36 +0200
Subject: [PATCH 096/133] pass both horizontal and vertical iamge scale to
 ImageTransformer

---
 .../Pager/AttachmentCarouselPage.js           | 29 ++++++++++++-------
 1 file changed, 19 insertions(+), 10 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index 128c0c98b450..b70376a0a821 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -9,10 +9,10 @@ import ImageWrapper from './ImageWrapper';
 import * as AttachmentsPropTypes from '../../propTypes';
 
 function getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight}) {
-    const scaleFactorX = canvasWidth / imageWidth;
-    const scaleFactorY = canvasHeight / imageHeight;
+    const imageScaleX = canvasWidth / imageWidth;
+    const imageScaleY = canvasHeight / imageHeight;
 
-    return scaleFactorY > scaleFactorX ? scaleFactorX : scaleFactorY;
+    return {imageScaleX, imageScaleY};
 }
 
 const cachedDimensions = new Map();
@@ -71,7 +71,9 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                         imageHeight={dimensions?.imageHeight}
                         scaledImageWidth={dimensions?.scaledImageWidth}
                         scaledImageHeight={dimensions?.scaledImageHeight}
-                        imageScale={dimensions?.imageScale}
+                        minImageScale={dimensions?.minImageScale}
+                        imageScaleX={dimensions?.imageScaleX}
+                        imageScaleY={dimensions?.imageScaleY}
                     >
                         <Image
                             source={{uri: source}}
@@ -81,15 +83,21 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
                                 const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
 
-                                const imageScale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
+                                const {imageScaleX, imageScaleY} = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
 
                                 // Don't update the dimensions if they are already set
-                                if (dimensions?.imageWidth !== imageWidth || dimensions?.imageHeight !== imageHeight || dimensions?.imageScale !== imageScale) {
+                                if (
+                                    dimensions?.imageWidth !== imageWidth ||
+                                    dimensions?.imageHeight !== imageHeight ||
+                                    dimensions?.imageScaleX !== imageScaleX ||
+                                    dimensions?.imageScaleY !== imageScaleY
+                                ) {
                                     cachedDimensions.set(source, {
                                         ...dimensions,
                                         imageWidth,
                                         imageHeight,
-                                        imageScale,
+                                        imageScaleX,
+                                        imageScaleY,
                                     });
                                 }
 
@@ -111,10 +119,11 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                             const imageWidth = evt.nativeEvent.width;
                             const imageHeight = evt.nativeEvent.height;
 
-                            const scale = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
+                            const {imageScaleX, imageScaleY} = getCanvasFitScale({canvasWidth, canvasHeight, imageWidth, imageHeight});
+                            const minImageScale = Math.min(imageScaleX, imageScaleY);
 
-                            const scaledImageWidth = imageWidth * scale;
-                            const scaledImageHeight = imageHeight * scale;
+                            const scaledImageWidth = imageWidth * minImageScale;
+                            const scaledImageHeight = imageHeight * minImageScale;
 
                             // Don't update the dimensions if they are already set
                             if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return;

From 5b9b4333705e1d659834d80deca5569eb550ad9b Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Thu, 27 Jul 2023 13:22:32 +0200
Subject: [PATCH 097/133] simplify variables

---
 .../AttachmentCarousel/Pager/ImageTransformer.js          | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 52ebc58eb6dd..7fb956324243 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -202,7 +202,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     });
 
     const zoomToCoordinates = useWorkletCallback(
-        (x, y) => {
+        (focalX, focalY) => {
             'worklet';
 
             stopAnimation();
@@ -222,16 +222,14 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 y: scaledImageHeight / 2,
             };
 
-            const focal = {x, y};
-
             const currentOrigin = {
                 x: (targetImageSize.width / 2 - canvasCenter.x) * -1,
                 y: (targetImageSize.height / 2 - canvasCenter.y) * -1,
             };
 
             const koef = {
-                x: (1 / imageCenter.x) * focal.x - 1,
-                y: (1 / imageCenter.y) * focal.y - 1,
+                x: (1 / imageCenter.x) * focalX - 1,
+                y: (1 / imageCenter.y) * focalY - 1,
             };
 
             const target = {

From da5aca7bdbce16dd51e4b6c209ec2481ea274555 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 11:36:29 +0200
Subject: [PATCH 098/133] disallow panning, when image has smaller width than
 canvas

---
 .../AttachmentCarousel/Pager/ImageTransformer.js           | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 7fb956324243..d2c2dacafb02 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -103,10 +103,13 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     // can we pan left/right/up/down
     // can be used to limit gesture or implementing tension effect
     const getBounds = useWorkletCallback(() => {
-        const rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2;
-
+        let rightBoundary = 0;
         let topBoundary = 0;
 
+        if (canvasWidth < zoomScaledImageWidth.value) {
+            rightBoundary = Math.abs(canvasWidth - zoomScaledImageWidth.value) / 2;
+        }
+
         if (canvasHeight < zoomScaledImageHeight.value) {
             topBoundary = Math.abs(zoomScaledImageHeight.value - canvasHeight) / 2;
         }

From 2a190af43b7d9295e36cab57e5522e4b2266c4d2 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 11:38:26 +0200
Subject: [PATCH 099/133] simplify zoomToCoordinates variables

---
 .../Pager/ImageTransformer.js                 | 20 ++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index d2c2dacafb02..b152135f919d 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -220,19 +220,29 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 y: canvasHeight / 2,
             };
 
-            const imageCenter = {
+            const originImageCenter = {
                 x: scaledImageWidth / 2,
                 y: scaledImageHeight / 2,
             };
 
+            const targetImageSize = {
+                width: scaledImageWidth * doubleTapScale,
+                height: scaledImageHeight * doubleTapScale,
+            };
+
+            const targetImageCenter = {
+                x: targetImageSize.width / 2,
+                y: targetImageSize.height / 2,
+            };
+
             const currentOrigin = {
-                x: (targetImageSize.width / 2 - canvasCenter.x) * -1,
-                y: (targetImageSize.height / 2 - canvasCenter.y) * -1,
+                x: (targetImageCenter.x - canvasCenter.x) * -1,
+                y: (targetImageCenter.y - canvasCenter.y) * -1,
             };
 
             const koef = {
-                x: (1 / imageCenter.x) * focalX - 1,
-                y: (1 / imageCenter.y) * focalY - 1,
+                x: (1 / originImageCenter.x) * focalX - 1,
+                y: (1 / originImageCenter.y) * focalY - 1,
             };
 
             const target = {

From 8fdb989b499025ba8791769b8693368d9648bfc5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 11:38:45 +0200
Subject: [PATCH 100/133] remove old variable

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index b152135f919d..a23557be4610 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -210,11 +210,6 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
             stopAnimation();
 
-            const targetImageSize = {
-                width: scaledImageWidth * doubleTapScale,
-                height: scaledImageHeight * doubleTapScale,
-            };
-
             const canvasCenter = {
                 x: canvasWidth / 2,
                 y: canvasHeight / 2,

From c8244afbecf33d92eaaa45f23b9cf21308e9aca4 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 12:18:42 +0200
Subject: [PATCH 101/133] add comment about totalScale calculation

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index a23557be4610..9f9d5db5a3f2 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -67,6 +67,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
+    // Using the smaller imageScale, so that the immage is not bigger than the canvas
+    // and not smaller than needed to fit
     const totalScale = useDerivedValue(() => clamp(zoomScale.value * minImageScale, minScale, maxScale), [minImageScale]);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);

From 6c505f625fb0a9ee3a8698bc111c69aef838152d Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 12:18:57 +0200
Subject: [PATCH 102/133] simplify ImageTransformer render structure

---
 .../Pager/ImageTransformer.js                 | 32 ++++++-------------
 1 file changed, 9 insertions(+), 23 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 9f9d5db5a3f2..bd4e3e1fd215 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -542,29 +542,15 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 },
             ]}
         >
-            <GestureDetector gesture={pinchGesture}>
-                <Animated.View
-                    collapsable
-                    style={StyleSheet.absoluteFill}
-                >
-                    <GestureDetector gesture={Gesture.Race(pinchGesture, singleTap, panGesture)}>
-                        <Animated.View
-                            collapsable
-                            style={StyleSheet.absoluteFill}
-                        >
-                            <ImageWrapper>
-                                <GestureDetector gesture={doubleTap}>
-                                    <Animated.View
-                                        collapsable={false}
-                                        style={[animatedStyles]}
-                                    >
-                                        {children}
-                                    </Animated.View>
-                                </GestureDetector>
-                            </ImageWrapper>
-                        </Animated.View>
-                    </GestureDetector>
-                </Animated.View>
+            <GestureDetector gesture={Gesture.Simultaneous(pinchGesture, doubleTap, Gesture.Race(pinchGesture, singleTap, panGesture))}>
+                <ImageWrapper>
+                    <Animated.View
+                        collapsable={false}
+                        style={[animatedStyles]}
+                    >
+                        {children}
+                    </Animated.View>
+                </ImageWrapper>
             </GestureDetector>
         </View>
     );

From a99e8db31dae96fbcbcdab2b449e7d005341adbb Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 12:40:35 +0200
Subject: [PATCH 103/133] remove unused import

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index bd4e3e1fd215..e92f98a5545e 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -1,6 +1,6 @@
 /* eslint-disable es/no-optional-chaining */
 import React, {useContext, useEffect, useRef, useState, useMemo} from 'react';
-import {StyleSheet, View} from 'react-native';
+import {View} from 'react-native';
 import PropTypes from 'prop-types';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
 import Animated, {

From 4874dd26d00363cea4045cf128bf33564b6213e5 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 12:40:57 +0200
Subject: [PATCH 104/133] improve doubleTap zoom to fill

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index e92f98a5545e..91cfb2cc356f 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -63,7 +63,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
     const minScale = useMemo(() => MIN_SCALE * minImageScale, [minImageScale]);
     const maxScale = useMemo(() => MAX_SCALE * maxImageScale, [maxImageScale]);
-    const doubleTapScale = useMemo(() => DOUBLE_TAP_SCALE * minImageScale, [minImageScale]);
+    // On double tap zoom to fill, but at least 3x zoom
+    const doubleTapScale = useMemo(() => Math.max(maxImageScale / minImageScale, DOUBLE_TAP_SCALE * minImageScale), [maxImageScale, minImageScale]);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas

From 88695c7a7d2362d4f12834369b8ac1f42beb6ce9 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 12:44:17 +0200
Subject: [PATCH 105/133] improve max/min scales

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 91cfb2cc356f..140f3312641b 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -61,8 +61,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     const minImageScale = useMemo(() => Math.min(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
     const maxImageScale = useMemo(() => Math.max(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
 
-    const minScale = useMemo(() => MIN_SCALE * minImageScale, [minImageScale]);
-    const maxScale = useMemo(() => MAX_SCALE * maxImageScale, [maxImageScale]);
+    const minScale = useMemo(() => Math.min(MIN_SCALE, MIN_SCALE * minImageScale), [minImageScale]);
+    const maxScale = useMemo(() => Math.max(MAX_SCALE, MAX_SCALE * maxImageScale), [maxImageScale]);
     // On double tap zoom to fill, but at least 3x zoom
     const doubleTapScale = useMemo(() => Math.max(maxImageScale / minImageScale, DOUBLE_TAP_SCALE * minImageScale), [maxImageScale, minImageScale]);
 

From ba5f0a6fb43e450eba1ba4727e0672d1a93719bb Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 13:01:40 +0200
Subject: [PATCH 106/133] fix: focal point on double tap to zoom

---
 .../AttachmentCarousel/Pager/ImageTransformer.js      | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 140f3312641b..087f64f5128f 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -208,11 +208,16 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     });
 
     const zoomToCoordinates = useWorkletCallback(
-        (focalX, focalY) => {
+        (canvasFocalX, canvasFocalY) => {
             'worklet';
 
             stopAnimation();
 
+            const imageFocal = {
+                x: clamp(canvasFocalX, 0, scaledImageWidth),
+                y: clamp(canvasFocalY, 0, scaledImageHeight),
+            };
+
             const canvasCenter = {
                 x: canvasWidth / 2,
                 y: canvasHeight / 2,
@@ -239,8 +244,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             };
 
             const koef = {
-                x: (1 / originImageCenter.x) * focalX - 1,
-                y: (1 / originImageCenter.y) * focalY - 1,
+                x: (1 / originImageCenter.x) * imageFocal.x - 1,
+                y: (1 / originImageCenter.y) * imageFocal.y - 1,
             };
 
             const target = {

From ec29a4377075d56459eb03050565251618350f77 Mon Sep 17 00:00:00 2001
From: c3024 <chaitanya3024@gmail.com>
Date: Mon, 31 Jul 2023 19:56:49 +0530
Subject: [PATCH 107/133] gray out date in MoneyRequestView

---
 src/components/ReportActionItem/MoneyRequestView.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 06cb6465fccd..dc8916bdaecb 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -79,6 +79,7 @@ function MoneyRequestView(props) {
             <MenuItemWithTopDescription
                 description={translate('common.date')}
                 title={formattedTransactionDate}
+                disabled={isSettled}
                 // shouldShowRightIcon={!isSettled}
                 // onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
             />

From 7e06d57a3af8b26752de194b7e9cfe4e84d088f9 Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Sat, 29 Jul 2023 08:56:00 +0530
Subject: [PATCH 108/133] converted emojipickermenu native to functional
 component

---
 .../EmojiPickerMenu/index.native.js           | 201 +++++++-----------
 1 file changed, 80 insertions(+), 121 deletions(-)

diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index 88e1c7df6986..d80bcf9dc105 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -1,11 +1,11 @@
-import React, {Component} from 'react';
-import {View, findNodeHandle} from 'react-native';
+import React, {useState} from 'react';
+import {View} from 'react-native';
 import {withOnyx} from 'react-native-onyx';
 import PropTypes from 'prop-types';
 import _ from 'underscore';
-import Animated, {runOnUI, _scrollTo} from 'react-native-reanimated';
+import Animated, {runOnUI, scrollTo, useAnimatedRef} from 'react-native-reanimated';
+import useWindowDimensions from '../../../hooks/useWindowDimensions';
 import compose from '../../../libs/compose';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
 import CONST from '../../../CONST';
 import ONYXKEYS from '../../../ONYXKEYS';
 import styles from '../../../styles/styles';
@@ -27,9 +27,6 @@ const propTypes = {
     /** Stores user's preferred skin tone */
     preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
 
-    /** Props related to the dimensions of the window */
-    ...windowDimensionsPropTypes,
-
     /** Props related to translation */
     ...withLocalizePropTypes,
 };
@@ -38,107 +35,72 @@ const defaultProps = {
     preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
 };
 
-class EmojiPickerMenu extends Component {
-    constructor(props) {
-        super(props);
-
-        // Ref for emoji FlatList
-        this.emojiList = undefined;
-
-        this.emojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis);
+const allEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis);
+const headerEmojis = EmojiUtils.getHeaderEmojis(allEmojis);
+const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW));
 
-        // Get the header emojis along with the code, index and icon.
-        // index is the actual header index starting at the first emoji and counting each one
-        this.headerEmojis = EmojiUtils.getHeaderEmojis(this.emojis);
+function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone, translate}) {
 
-        // This is the indices of each header's Row
-        // The positions are static, and are calculated as index/numColumns (8 in our case)
-        // This is because each row of 8 emojis counts as one index to the flatlist
-        this.headerRowIndices = _.map(this.headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW));
-
-        this.renderItem = this.renderItem.bind(this);
-        this.isMobileLandscape = this.isMobileLandscape.bind(this);
-        this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
-        this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300);
-        this.scrollToHeader = this.scrollToHeader.bind(this);
-        this.getItemLayout = this.getItemLayout.bind(this);
-
-        this.state = {
-            filteredEmojis: this.emojis,
-            headerIndices: this.headerRowIndices,
-        };
-    }
+    const emojiList = useAnimatedRef();
+    const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
+    const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
+    const {windowWidth} = useWindowDimensions();
 
-    getItemLayout(data, index) {
-        return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index};
-    }
+    const getItemLayout = (data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index})
 
     /**
      * Filter the entire list of emojis to only emojis that have the search term in their keywords
      *
      * @param {String} searchTerm
      */
-    filterEmojis(searchTerm) {
+    const filterEmojis = _.debounce((searchTerm) => {
         const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
 
-        if (this.emojiList) {
-            this.emojiList.scrollToOffset({offset: 0, animated: false});
+        if (emojiList.current) {
+            emojiList.current.scrollToOffset({offset: 0, animated: false});
         }
 
         if (normalizedSearchTerm === '') {
-            this.setState({
-                filteredEmojis: this.emojis,
-                headerIndices: this.headerRowIndices,
-            });
+            setFilteredEmojis(allEmojis);
+            setHeaderIndices(headerRowIndices);
 
             return;
         }
-        const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, this.emojis.length);
+        const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);
 
-        this.setState({
-            filteredEmojis: newFilteredEmojiList,
-            headerIndices: undefined,
-        });
-    }
+        setFilteredEmojis(newFilteredEmojiList);
+        setHeaderIndices(undefined);
+    }, 300);
 
     /**
      * @param {String} emoji
      * @param {Object} emojiObject
      */
-    addToFrequentAndSelectEmoji(emoji, emojiObject) {
+    const addToFrequentAndSelectEmoji = (emoji, emojiObject) => {
         const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject);
         User.updateFrequentlyUsedEmojis(frequentEmojiList);
-        this.props.onEmojiSelected(emoji, emojiObject);
-    }
-
-    /**
-     * Check if its a landscape mode of mobile device
-     *
-     * @returns {Boolean}
-     */
-    isMobileLandscape() {
-        return this.props.windowWidth >= this.props.windowHeight;
+        onEmojiSelected(emoji, emojiObject);
     }
 
     /**
      * @param {Number} skinTone
      */
-    updatePreferredSkinTone(skinTone) {
-        if (this.props.preferredSkinTone === skinTone) {
+    const updatePreferredSkinTone = (skinTone) => {
+        if (preferredSkinTone === skinTone) {
             return;
         }
 
         User.updatePreferredSkinTone(skinTone);
     }
 
-    scrollToHeader(headerIndex) {
+    const scrollToHeader = (headerIndex) => {
         const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
-        this.emojiList.flashScrollIndicators();
-        const node = findNodeHandle(this.emojiList);
+        emojiList.current.flashScrollIndicators();
+        // const node = findNodeHandle(this.emojiList);
         runOnUI(() => {
             'worklet';
 
-            _scrollTo(node, 0, calculatedOffset, true);
+            scrollTo(emojiList, 0, calculatedOffset, true);
         })();
     }
 
@@ -149,9 +111,7 @@ class EmojiPickerMenu extends Component {
      * @param {Number} index
      * @returns {String}
      */
-    keyExtractor(item, index) {
-        return `${index}${item.code}`;
-    }
+    const keyExtractor = (item, index) => `${index}${item.code}`
 
     /**
      * Given an emoji item object, render a component based on its type.
@@ -161,7 +121,7 @@ class EmojiPickerMenu extends Component {
      * @param {Object} item
      * @returns {*}
      */
-    renderItem({item}) {
+    const renderItem = ({item}) => {
         const {code, types} = item;
         if (item.spacer) {
             return null;
@@ -170,75 +130,74 @@ class EmojiPickerMenu extends Component {
         if (item.header) {
             return (
                 <View style={styles.emojiHeaderContainer}>
-                    <Text style={styles.textLabelSupporting}>{this.props.translate(`emojiPicker.headers.${code}`)}</Text>
+                    <Text style={styles.textLabelSupporting}>{translate(`emojiPicker.headers.${code}`)}</Text>
                 </View>
             );
         }
 
-        const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code;
+        const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code;
 
         return (
             <EmojiPickerMenuItem
-                onPress={(emoji) => this.addToFrequentAndSelectEmoji(emoji, item)}
+                onPress={(emoji) => addToFrequentAndSelectEmoji(emoji, item)}
                 emoji={emojiCode}
             />
         );
     }
 
-    render() {
-        const isFiltered = this.emojis.length !== this.state.filteredEmojis.length;
-        return (
-            <View style={styles.emojiPickerContainer}>
-                <View style={[styles.ph4, styles.pb1, styles.pt2]}>
-                    <TextInput
-                        label={this.props.translate('common.search')}
-                        accessibilityLabel={this.props.translate('common.search')}
-                        accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
-                        onChangeText={this.filterEmojis}
-                    />
-                </View>
-                {!isFiltered && (
-                    <CategoryShortcutBar
-                        headerEmojis={this.headerEmojis}
-                        onPress={this.scrollToHeader}
-                    />
-                )}
-                <Animated.FlatList
-                    ref={(el) => (this.emojiList = el)}
-                    keyboardShouldPersistTaps="handled"
-                    data={this.state.filteredEmojis}
-                    renderItem={this.renderItem}
-                    keyExtractor={this.keyExtractor}
-                    numColumns={CONST.EMOJI_NUM_PER_ROW}
-                    style={[
-                        StyleUtils.getEmojiPickerListHeight(isFiltered),
-                        {
-                            width: this.props.windowWidth,
-                        },
-                    ]}
-                    stickyHeaderIndices={this.state.headerIndices}
-                    getItemLayout={this.getItemLayout}
-                    showsVerticalScrollIndicator
-                    // used because of a bug in RN where stickyHeaderIndices can't be updated after the list is rendered https://github.com/facebook/react-native/issues/25157
-                    removeClippedSubviews={false}
-                    contentContainerStyle={styles.flexGrow1}
-                    ListEmptyComponent={<Text style={[styles.disabledText]}>{this.props.translate('common.noResultsFound')}</Text>}
-                    alwaysBounceVertical={this.state.filteredEmojis.length !== 0}
-                />
-                <EmojiSkinToneList
-                    updatePreferredSkinTone={this.updatePreferredSkinTone}
-                    preferredSkinTone={this.props.preferredSkinTone}
+    const isFiltered = allEmojis.length !== filteredEmojis.length;
+
+    return (
+        <View style={styles.emojiPickerContainer}>
+            <View style={[styles.ph4, styles.pb1, styles.pt2]}>
+                <TextInput
+                    label={translate('common.search')}
+                    accessibilityLabel={translate('common.search')}
+                    accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
+                    onChangeText={filterEmojis}
                 />
             </View>
-        );
-    }
+            {!isFiltered && (
+                <CategoryShortcutBar
+                    headerEmojis={headerEmojis}
+                    onPress={scrollToHeader}
+                />
+            )}
+            <Animated.FlatList
+                ref={emojiList}
+                keyboardShouldPersistTaps="handled"
+                data={filteredEmojis}
+                renderItem={renderItem}
+                keyExtractor={keyExtractor}
+                numColumns={CONST.EMOJI_NUM_PER_ROW}
+                style={[
+                    StyleUtils.getEmojiPickerListHeight(isFiltered),
+                    {
+                        width: windowWidth,
+                    },
+                ]}
+                stickyHeaderIndices={headerIndices}
+                getItemLayout={getItemLayout}
+                showsVerticalScrollIndicator
+                // used because of a bug in RN where stickyHeaderIndices can't be updated after the list is rendered https://github.com/facebook/react-native/issues/25157
+                removeClippedSubviews={false}
+                contentContainerStyle={styles.flexGrow1}
+                ListEmptyComponent={<Text style={[styles.disabledText]}>{translate('common.noResultsFound')}</Text>}
+                alwaysBounceVertical={filteredEmojis.length !== 0}
+            />
+            <EmojiSkinToneList
+                updatePreferredSkinTone={updatePreferredSkinTone}
+                preferredSkinTone={preferredSkinTone}
+            />
+        </View>
+    );
 }
 
+EmojiPickerMenu.displayName = 'EmojiPickerMenu';
 EmojiPickerMenu.propTypes = propTypes;
 EmojiPickerMenu.defaultProps = defaultProps;
 
 export default compose(
-    withWindowDimensions,
     withLocalize,
     withOnyx({
         preferredSkinTone: {

From 7f663871b76f977bdd62897ec1d1f9b50936c2bc Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Sat, 29 Jul 2023 17:35:37 +0530
Subject: [PATCH 109/133] small bug fix

---
 .../EmojiPicker/EmojiPickerMenu/index.native.js          | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index d80bcf9dc105..154afb0b0e46 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -1,4 +1,4 @@
-import React, {useState} from 'react';
+import React, {useState, useMemo} from 'react';
 import {View} from 'react-native';
 import {withOnyx} from 'react-native-onyx';
 import PropTypes from 'prop-types';
@@ -35,13 +35,12 @@ const defaultProps = {
     preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
 };
 
-const allEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis);
-const headerEmojis = EmojiUtils.getHeaderEmojis(allEmojis);
-const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW));
-
 function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone, translate}) {
 
     const emojiList = useAnimatedRef();
+    const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), []);
+    const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
+    const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)), [headerEmojis]);
     const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
     const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
     const {windowWidth} = useWindowDimensions();

From 8e0678e8a2f0be051d9fa0401de0885605d511d3 Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Sun, 30 Jul 2023 17:31:25 +0530
Subject: [PATCH 110/133] removed unecessary comment

---
 src/components/EmojiPicker/EmojiPickerMenu/index.native.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index 154afb0b0e46..0ac672cedecd 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -95,7 +95,6 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
     const scrollToHeader = (headerIndex) => {
         const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
         emojiList.current.flashScrollIndicators();
-        // const node = findNodeHandle(this.emojiList);
         runOnUI(() => {
             'worklet';
 

From d90507ede4db8160341ec67e01680ee28f11e01f Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Mon, 31 Jul 2023 16:29:19 +0530
Subject: [PATCH 111/133] prettier lint

---
 .../EmojiPicker/EmojiPickerMenu/index.native.js   | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index 0ac672cedecd..6092c9d301da 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -35,8 +35,7 @@ const defaultProps = {
     preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
 };
 
-function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone, translate}) {
-
+function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate}) {
     const emojiList = useAnimatedRef();
     const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), []);
     const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
@@ -45,7 +44,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
     const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
     const {windowWidth} = useWindowDimensions();
 
-    const getItemLayout = (data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index})
+    const getItemLayout = (data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index});
 
     /**
      * Filter the entire list of emojis to only emojis that have the search term in their keywords
@@ -79,7 +78,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
         const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject);
         User.updateFrequentlyUsedEmojis(frequentEmojiList);
         onEmojiSelected(emoji, emojiObject);
-    }
+    };
 
     /**
      * @param {Number} skinTone
@@ -90,7 +89,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
         }
 
         User.updatePreferredSkinTone(skinTone);
-    }
+    };
 
     const scrollToHeader = (headerIndex) => {
         const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
@@ -100,7 +99,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
 
             scrollTo(emojiList, 0, calculatedOffset, true);
         })();
-    }
+    };
 
     /**
      * Return a unique key for each emoji item
@@ -109,7 +108,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
      * @param {Number} index
      * @returns {String}
      */
-    const keyExtractor = (item, index) => `${index}${item.code}`
+    const keyExtractor = (item, index) => `${index}${item.code}`;
 
     /**
      * Given an emoji item object, render a component based on its type.
@@ -141,7 +140,7 @@ function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone,
                 emoji={emojiCode}
             />
         );
-    }
+    };
 
     const isFiltered = allEmojis.length !== filteredEmojis.length;
 

From 0db10d8b5d2e86cf14a79d1da9b5be932c6da2fa Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Mon, 31 Jul 2023 16:58:56 +0530
Subject: [PATCH 112/133] ReBump Reanimated to 3.4.0

This reverts commit 74fd18ba6a6d7e94005c2336df7cc8d01da3fa4b.
---
 ios/Podfile.lock                            |   6 +-
 package-lock.json                           |  14 +-
 package.json                                |   2 +-
 patches/react-native-reanimated+3.1.0.patch | 177 --------------------
 4 files changed, 12 insertions(+), 187 deletions(-)
 delete mode 100644 patches/react-native-reanimated+3.1.0.patch

diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index e3c339b3af9a..bec32561cec0 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -674,11 +674,12 @@ PODS:
     - React-Core
   - RNReactNativeHapticFeedback (1.14.0):
     - React-Core
-  - RNReanimated (3.1.0):
+  - RNReanimated (3.4.0):
     - DoubleConversion
     - FBLazyVector
     - FBReactNativeSpec
     - glog
+    - hermes-engine
     - RCT-Folly
     - RCTRequired
     - RCTTypeSafety
@@ -688,6 +689,7 @@ PODS:
     - React-Core/RCTWebSocket
     - React-CoreModules
     - React-cxxreact
+    - React-hermes
     - React-jsi
     - React-jsiexecutor
     - React-jsinspector
@@ -1136,7 +1138,7 @@ SPEC CHECKSUMS:
   RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
   RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c
   RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
-  RNReanimated: b1220a0e5168745283ff5d53bfc7d2144b2cee1b
+  RNReanimated: 532818a3578a0832d95ea9d0a6a9dc0e73bbce29
   RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9
   RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
   SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
diff --git a/package-lock.json b/package-lock.json
index d43ea9372ba1..cecb1f7adab3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -88,7 +88,7 @@
         "react-native-plaid-link-sdk": "^10.0.0",
         "react-native-qrcode-svg": "^6.2.0",
         "react-native-quick-sqlite": "^8.0.0-beta.2",
-        "react-native-reanimated": "3.1.0",
+        "react-native-reanimated": "3.4.0",
         "react-native-render-html": "6.3.1",
         "react-native-safe-area-context": "4.4.1",
         "react-native-screens": "3.17.0",
@@ -36689,9 +36689,9 @@
       }
     },
     "node_modules/react-native-reanimated": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.1.0.tgz",
-      "integrity": "sha512-8YJR7yHnrqK6yKWzkGLVEawi1WZqJ9bGIehKEnE8zG58yLrSwUZe1T220XTbftpkA3r37Sy0kJJ/HOOiaIU+HQ==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.4.0.tgz",
+      "integrity": "sha512-B5cZJseoIkYlZTRBRN0xuU1NBxUza/6GSHhiEBQfbOufWVlUMMcWUecIRVglW49l8d2wXbfCdQlNyVoFqmHkaQ==",
       "dependencies": {
         "@babel/plugin-transform-object-assign": "^7.16.7",
         "@babel/preset-typescript": "^7.16.7",
@@ -68240,9 +68240,9 @@
       "requires": {}
     },
     "react-native-reanimated": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.1.0.tgz",
-      "integrity": "sha512-8YJR7yHnrqK6yKWzkGLVEawi1WZqJ9bGIehKEnE8zG58yLrSwUZe1T220XTbftpkA3r37Sy0kJJ/HOOiaIU+HQ==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.4.0.tgz",
+      "integrity": "sha512-B5cZJseoIkYlZTRBRN0xuU1NBxUza/6GSHhiEBQfbOufWVlUMMcWUecIRVglW49l8d2wXbfCdQlNyVoFqmHkaQ==",
       "requires": {
         "@babel/plugin-transform-object-assign": "^7.16.7",
         "@babel/preset-typescript": "^7.16.7",
diff --git a/package.json b/package.json
index 8f8093cc7e96..d7a417556274 100644
--- a/package.json
+++ b/package.json
@@ -126,7 +126,7 @@
     "react-native-plaid-link-sdk": "^10.0.0",
     "react-native-qrcode-svg": "^6.2.0",
     "react-native-quick-sqlite": "^8.0.0-beta.2",
-    "react-native-reanimated": "3.1.0",
+    "react-native-reanimated": "3.4.0",
     "react-native-render-html": "6.3.1",
     "react-native-safe-area-context": "4.4.1",
     "react-native-screens": "3.17.0",
diff --git a/patches/react-native-reanimated+3.1.0.patch b/patches/react-native-reanimated+3.1.0.patch
deleted file mode 100644
index 6dc6e0d3bc9b..000000000000
--- a/patches/react-native-reanimated+3.1.0.patch
+++ /dev/null
@@ -1,177 +0,0 @@
-diff --git a/node_modules/react-native-reanimated/ios/REANodesManager.mm b/node_modules/react-native-reanimated/ios/REANodesManager.mm
-index 26bb253..4108293 100644
---- a/node_modules/react-native-reanimated/ios/REANodesManager.mm
-+++ b/node_modules/react-native-reanimated/ios/REANodesManager.mm
-@@ -85,19 +85,77 @@ - (void)runSyncUIUpdatesWithObserver:(id<RCTUIManagerObserver>)observer
- 
- @end
- 
--@interface REANodesManager () <RCTUIManagerObserver>
-+#ifndef RCT_NEW_ARCH_ENABLED
- 
-+@interface REASyncUpdateObserver : NSObject <RCTUIManagerObserver>
- @end
- 
-+@implementation REASyncUpdateObserver {
-+  volatile void (^_mounting)(void);
-+  volatile BOOL _waitTimedOut;
-+  dispatch_semaphore_t _semaphore;
-+}
-+
-+- (instancetype)init
-+{
-+  self = [super init];
-+  if (self) {
-+    _mounting = nil;
-+    _waitTimedOut = NO;
-+    _semaphore = dispatch_semaphore_create(0);
-+  }
-+  return self;
-+}
-+
-+- (void)dealloc
-+{
-+  RCTAssert(_mounting == nil, @"Mouting block was set but never executed. This may lead to UI inconsistencies");
-+}
-+
-+- (void)unblockUIThread
-+{
-+  RCTAssertUIManagerQueue();
-+  dispatch_semaphore_signal(_semaphore);
-+}
-+
-+- (void)waitAndMountWithTimeout:(NSTimeInterval)timeout
-+{
-+  RCTAssertMainQueue();
-+  long result = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC));
-+  if (result != 0) {
-+    @synchronized(self) {
-+      _waitTimedOut = YES;
-+    }
-+  }
-+  if (_mounting) {
-+    _mounting();
-+    _mounting = nil;
-+  }
-+}
-+
-+- (BOOL)uiManager:(RCTUIManager *)manager performMountingWithBlock:(RCTUIManagerMountingBlock)block
-+{
-+  RCTAssertUIManagerQueue();
-+  @synchronized(self) {
-+    if (_waitTimedOut) {
-+      return NO;
-+    } else {
-+      _mounting = block;
-+      return YES;
-+    }
-+  }
-+}
-+
-+@end
-+
-+#endif
-+
- @implementation REANodesManager {
-   CADisplayLink *_displayLink;
-   BOOL _wantRunUpdates;
-   NSMutableArray<REAOnAnimationCallback> *_onAnimationCallbacks;
-   BOOL _tryRunBatchUpdatesSynchronously;
-   REAEventHandler _eventHandler;
--  volatile void (^_mounting)(void);
--  NSObject *_syncLayoutUpdatesWaitLock;
--  volatile BOOL _syncLayoutUpdatesWaitTimedOut;
-   NSMutableDictionary<NSNumber *, ComponentUpdate *> *_componentUpdateBuffer;
-   NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry;
- #ifdef RCT_NEW_ARCH_ENABLED
-@@ -125,7 +183,6 @@ - (nonnull instancetype)initWithModule:(REAModule *)reanimatedModule
-     _operationsInBatch = [NSMutableDictionary new];
-     _componentUpdateBuffer = [NSMutableDictionary new];
-     _viewRegistry = [_uiManager valueForKey:@"_viewRegistry"];
--    _syncLayoutUpdatesWaitLock = [NSObject new];
-   }
- 
-   _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onAnimationFrame:)];
-@@ -241,19 +298,6 @@ - (void)onAnimationFrame:(CADisplayLink *)displayLink
-   }
- }
- 
--- (BOOL)uiManager:(RCTUIManager *)manager performMountingWithBlock:(RCTUIManagerMountingBlock)block
--{
--  RCTAssert(_mounting == nil, @"Mouting block is expected to not be set");
--  @synchronized(_syncLayoutUpdatesWaitLock) {
--    if (_syncLayoutUpdatesWaitTimedOut) {
--      return NO;
--    } else {
--      _mounting = block;
--      return YES;
--    }
--  }
--}
--
- - (void)performOperations
- {
- #ifdef RCT_NEW_ARCH_ENABLED
-@@ -268,8 +312,7 @@ - (void)performOperations
-     _tryRunBatchUpdatesSynchronously = NO;
- 
-     __weak __typeof__(self) weakSelf = self;
--    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
--    _syncLayoutUpdatesWaitTimedOut = NO;
-+    REASyncUpdateObserver *syncUpdateObserver = [REASyncUpdateObserver new];
-     RCTExecuteOnUIManagerQueue(^{
-       __typeof__(self) strongSelf = weakSelf;
-       if (strongSelf == nil) {
-@@ -278,7 +321,7 @@ - (void)performOperations
-       BOOL canUpdateSynchronously = trySynchronously && ![strongSelf.uiManager hasEnqueuedUICommands];
- 
-       if (!canUpdateSynchronously) {
--        dispatch_semaphore_signal(semaphore);
-+        [syncUpdateObserver unblockUIThread];
-       }
- 
-       for (int i = 0; i < copiedOperationsQueue.count; i++) {
-@@ -286,8 +329,8 @@ - (void)performOperations
-       }
- 
-       if (canUpdateSynchronously) {
--        [strongSelf.uiManager runSyncUIUpdatesWithObserver:strongSelf];
--        dispatch_semaphore_signal(semaphore);
-+        [strongSelf.uiManager runSyncUIUpdatesWithObserver:syncUpdateObserver];
-+        [syncUpdateObserver unblockUIThread];
-       }
-       // In case canUpdateSynchronously=true we still have to send uiManagerWillPerformMounting event
-       // to observers because some components (e.g. TextInput) update their UIViews only on that event.
-@@ -298,17 +341,7 @@ - (void)performOperations
-       // from CADisplayLink but it is easier to hardcode it for the time being.
-       // The reason why we use frame duration here is that if takes longer than one frame to complete layout tasks
-       // there is no point of synchronizing layout with the UI interaction as we get that one frame delay anyways.
--      long result = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 16 * NSEC_PER_MSEC));
--      if (result != 0) {
--        @synchronized(_syncLayoutUpdatesWaitLock) {
--          _syncLayoutUpdatesWaitTimedOut = YES;
--        }
--      }
--    }
--
--    if (_mounting) {
--      _mounting();
--      _mounting = nil;
-+      [syncUpdateObserver waitAndMountWithTimeout:0.016];
-     }
-   }
-   _wantRunUpdates = NO;
-diff --git a/node_modules/react-native-reanimated/mock.js b/node_modules/react-native-reanimated/mock.js
-index 68b20d2..b088001 100644
---- a/node_modules/react-native-reanimated/mock.js
-+++ b/node_modules/react-native-reanimated/mock.js
-@@ -41,6 +41,9 @@ const Reanimated = {
-   createAnimatedComponent: (Component) => Component,
-   addWhitelistedUIProps: NOOP,
-   addWhitelistedNativeProps: NOOP,
-+
-+  // used by react-navigation fork
-+  isConfigured: () => true,
- };
- 
- module.exports = {

From e162d753edb9539cf2e9821d2d64fd1c8140d0a4 Mon Sep 17 00:00:00 2001
From: Shubham Agrawal <shubham.agra1206@gmail.com>
Date: Mon, 31 Jul 2023 19:59:58 +0530
Subject: [PATCH 113/133] updated  for newer definition

---
 src/libs/updatePropsPaperWorklet/index.native.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/libs/updatePropsPaperWorklet/index.native.js b/src/libs/updatePropsPaperWorklet/index.native.js
index f346562b2cc7..ed79b38ffab5 100644
--- a/src/libs/updatePropsPaperWorklet/index.native.js
+++ b/src/libs/updatePropsPaperWorklet/index.native.js
@@ -3,5 +3,11 @@ export default function (viewTag, viewName, updates) {
 
     // _updatePropsPaper is a function that is worklet function from react-native-reanimated which is not available on web
     // eslint-disable-next-line no-undef
-    _updatePropsPaper(viewTag, viewName, updates);
+    _updatePropsPaper([
+        {
+            tag: viewTag,
+            name: viewName,
+            updates,
+        },
+    ]);
 }

From a787399a6e887a5ccb3325ad92ab196c1b705a24 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Tue, 25 Jul 2023 16:55:43 +0800
Subject: [PATCH 114/133] append current time to the downloaded file name

---
 src/libs/fileDownload/FileUtils.js          | 13 +++++++++++--
 src/libs/fileDownload/index.android.js      |  2 +-
 src/libs/fileDownload/index.ios.js          |  2 +-
 src/libs/fileDownload/index.js              |  2 +-
 src/libs/localFileDownload/index.android.js |  5 +++--
 src/libs/localFileDownload/index.ios.js     |  6 ++++--
 src/libs/localFileDownload/index.js         |  4 +++-
 7 files changed, 24 insertions(+), 10 deletions(-)

diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js
index 81dcfa933660..3a9751aeb7bf 100644
--- a/src/libs/fileDownload/FileUtils.js
+++ b/src/libs/fileDownload/FileUtils.js
@@ -104,7 +104,7 @@ function getFileType(fileUrl) {
 function splitExtensionFromFileName(fullFileName) {
     const fileName = fullFileName.trim();
     const splitFileName = fileName.split('.');
-    const fileExtension = splitFileName.pop();
+    const fileExtension = splitFileName.length > 1 ? splitFileName.pop() : '';
     return {fileName: splitFileName.join('.'), fileExtension};
 }
 
@@ -118,4 +118,13 @@ function cleanFileName(fileName) {
     return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_');
 }
 
-export {showGeneralErrorAlert, showSuccessAlert, showPermissionErrorAlert, splitExtensionFromFileName, getAttachmentName, getFileType, cleanFileName};
+function appendTimeToFileName(fileName) {
+    const file = splitExtensionFromFileName(fileName);
+    let newFileName = `${file.fileName} - ${new Date().toISOString()}`;
+    if (file.fileExtension) {
+        newFileName += `.${file.fileExtension}`;
+    }
+    return newFileName;
+}
+
+export {showGeneralErrorAlert, showSuccessAlert, showPermissionErrorAlert, splitExtensionFromFileName, getAttachmentName, getFileType, cleanFileName, appendTimeToFileName};
diff --git a/src/libs/fileDownload/index.android.js b/src/libs/fileDownload/index.android.js
index c14575881fea..c19301cbde49 100644
--- a/src/libs/fileDownload/index.android.js
+++ b/src/libs/fileDownload/index.android.js
@@ -41,7 +41,7 @@ function handleDownload(url, fileName) {
 
         // Android files will download to Download directory
         const path = dirs.DownloadDir;
-        const attachmentName = fileName || FileUtils.getAttachmentName(url);
+        const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url);
 
         const isLocalFile = url.startsWith('file://');
 
diff --git a/src/libs/fileDownload/index.ios.js b/src/libs/fileDownload/index.ios.js
index 7a5aab1c1944..95d92f1e3103 100644
--- a/src/libs/fileDownload/index.ios.js
+++ b/src/libs/fileDownload/index.ios.js
@@ -75,7 +75,7 @@ export default function fileDownload(fileUrl, fileName) {
     return new Promise((resolve) => {
         let fileDownloadPromise = null;
         const fileType = FileUtils.getFileType(fileUrl);
-        const attachmentName = fileName || FileUtils.getAttachmentName(fileUrl);
+        const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(fileUrl);
 
         switch (fileType) {
             case CONST.ATTACHMENT_FILE_TYPE.IMAGE:
diff --git a/src/libs/fileDownload/index.js b/src/libs/fileDownload/index.js
index 002cc17d64c9..712a6ca2affb 100644
--- a/src/libs/fileDownload/index.js
+++ b/src/libs/fileDownload/index.js
@@ -23,7 +23,7 @@ export default function fileDownload(url, fileName) {
                 link.style.display = 'none';
                 link.setAttribute(
                     'download',
-                    fileName || FileUtils.getAttachmentName(url), // generating the file name
+                    FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url), // generating the file name
                 );
 
                 // Append to html link element page
diff --git a/src/libs/localFileDownload/index.android.js b/src/libs/localFileDownload/index.android.js
index acdc9c89e6a4..b3e39e7a7a53 100644
--- a/src/libs/localFileDownload/index.android.js
+++ b/src/libs/localFileDownload/index.android.js
@@ -10,13 +10,14 @@ import * as FileUtils from '../fileDownload/FileUtils';
  * @param {String} textContent
  */
 export default function localFileDownload(fileName, textContent) {
+    const newFileName = FileUtils.appendTimeToFileName(fileName);
     const dir = RNFetchBlob.fs.dirs.DocumentDir;
-    const path = `${dir}/${fileName}.txt`;
+    const path = `${dir}/${newFileName}.txt`;
 
     RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => {
         RNFetchBlob.MediaCollection.copyToMediaStore(
             {
-                name: fileName,
+                name: newFileName,
                 parentFolder: '', // subdirectory in the Media Store, empty goes to 'Downloads'
                 mimeType: 'text/plain',
             },
diff --git a/src/libs/localFileDownload/index.ios.js b/src/libs/localFileDownload/index.ios.js
index 7113db49ac8b..1241f5a535db 100644
--- a/src/libs/localFileDownload/index.ios.js
+++ b/src/libs/localFileDownload/index.ios.js
@@ -1,5 +1,6 @@
 import {Share} from 'react-native';
 import RNFetchBlob from 'react-native-blob-util';
+import * as FileUtils from '../fileDownload/FileUtils';
 
 /**
  * Writes a local file to the app's internal directory with the given fileName
@@ -10,11 +11,12 @@ import RNFetchBlob from 'react-native-blob-util';
  * @param {String} textContent
  */
 export default function localFileDownload(fileName, textContent) {
+    const newFileName = FileUtils.appendTimeToFileName(fileName);
     const dir = RNFetchBlob.fs.dirs.DocumentDir;
-    const path = `${dir}/${fileName}.txt`;
+    const path = `${dir}/${newFileName}.txt`;
 
     RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => {
-        Share.share({url: path, title: fileName}).finally(() => {
+        Share.share({url: path, title: newFileName}).finally(() => {
             RNFetchBlob.fs.unlink(path);
         });
     });
diff --git a/src/libs/localFileDownload/index.js b/src/libs/localFileDownload/index.js
index f52d2fa0115e..427928834c9c 100644
--- a/src/libs/localFileDownload/index.js
+++ b/src/libs/localFileDownload/index.js
@@ -1,3 +1,5 @@
+import * as FileUtils from '../fileDownload/FileUtils';
+
 /**
  * Creates a Blob with the given fileName and textContent, then dynamically
  * creates a temporary anchor, just to programmatically click it, so the file
@@ -10,7 +12,7 @@ export default function localFileDownload(fileName, textContent) {
     const blob = new Blob([textContent], {type: 'text/plain'});
     const url = URL.createObjectURL(blob);
     const link = document.createElement('a');
-    link.download = `${fileName}.txt`;
+    link.download = FileUtils.appendTimeToFileName(`${fileName}.txt`);
     link.href = url;
     link.click();
 }

From 6d89f4826a3156fddc4828ea2824fb03803d5437 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Tue, 25 Jul 2023 21:02:58 +0800
Subject: [PATCH 115/133] create unit test

---
 tests/unit/FileUtilsTest.js | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)
 create mode 100644 tests/unit/FileUtilsTest.js

diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
new file mode 100644
index 000000000000..f439262e41d1
--- /dev/null
+++ b/tests/unit/FileUtilsTest.js
@@ -0,0 +1,37 @@
+import * as FileUtils from '../../src/libs/fileDownload/FileUtils';
+
+describe('FileUtils', () => {
+    describe('splitExtensionFromFileName', () => {
+        it('should return correct file name and extensions', () => {
+            const file = FileUtils.splitExtensionFromFileName('image.jpg');
+            expect(file.fileName).toEqual('image');
+            expect(file.fileExtension).toEqual('jpg');
+        }); 
+        
+        it('should return correct file name and extensions even with multiple dots on the file name', () => {
+            const file = FileUtils.splitExtensionFromFileName('image.pdf.jpg');
+            expect(file.fileName).toEqual('image.pdf');
+            expect(file.fileExtension).toEqual('jpg');
+        });
+
+        it('should return empty extension if the file name does not have it', () => {
+            const file = FileUtils.splitExtensionFromFileName('image');
+            expect(file.fileName).toEqual('image');
+            expect(file.fileExtension).toEqual('');
+        });
+    });
+
+    describe('appendTimeToFileName', () => {
+        it('should append current time to the end of the file name', () => {
+            const actualFileName = FileUtils.appendTimeToFileName('image.jpg');
+            const expectedFileName = `image - ${new Date().toISOString()}.jpg`;
+            expect(actualFileName).toEqual(expectedFileName);
+        });
+
+        it('should append current time to the end of the file name without extension', () => {
+            const actualFileName = FileUtils.appendTimeToFileName('image');
+            const expectedFileName = `image - ${new Date().toISOString()}`;
+            expect(actualFileName).toEqual(expectedFileName);
+        });
+    });
+});

From ea964f4cabefa92bfbec031b39ed634cfffbb089 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Tue, 25 Jul 2023 21:05:38 +0800
Subject: [PATCH 116/133] update wording

---
 tests/unit/FileUtilsTest.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
index f439262e41d1..05633b231fd9 100644
--- a/tests/unit/FileUtilsTest.js
+++ b/tests/unit/FileUtilsTest.js
@@ -2,13 +2,13 @@ import * as FileUtils from '../../src/libs/fileDownload/FileUtils';
 
 describe('FileUtils', () => {
     describe('splitExtensionFromFileName', () => {
-        it('should return correct file name and extensions', () => {
+        it('should return correct file name and extension', () => {
             const file = FileUtils.splitExtensionFromFileName('image.jpg');
             expect(file.fileName).toEqual('image');
             expect(file.fileExtension).toEqual('jpg');
         }); 
         
-        it('should return correct file name and extensions even with multiple dots on the file name', () => {
+        it('should return correct file name and extension even with multiple dots on the file name', () => {
             const file = FileUtils.splitExtensionFromFileName('image.pdf.jpg');
             expect(file.fileName).toEqual('image.pdf');
             expect(file.fileExtension).toEqual('jpg');

From b4a47736aaaf58e74e23123c24f2f6225ea17c81 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Tue, 25 Jul 2023 21:07:07 +0800
Subject: [PATCH 117/133] prettier

---
 tests/unit/FileUtilsTest.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
index 05633b231fd9..e03441840010 100644
--- a/tests/unit/FileUtilsTest.js
+++ b/tests/unit/FileUtilsTest.js
@@ -6,8 +6,8 @@ describe('FileUtils', () => {
             const file = FileUtils.splitExtensionFromFileName('image.jpg');
             expect(file.fileName).toEqual('image');
             expect(file.fileExtension).toEqual('jpg');
-        }); 
-        
+        });
+
         it('should return correct file name and extension even with multiple dots on the file name', () => {
             const file = FileUtils.splitExtensionFromFileName('image.pdf.jpg');
             expect(file.fileName).toEqual('image.pdf');

From b631544402b05e73488dfc69b78b8d4d73b8efe8 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Wed, 26 Jul 2023 11:56:31 +0800
Subject: [PATCH 118/133] add jsdoc

---
 src/libs/fileDownload/FileUtils.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js
index 3a9751aeb7bf..4166ad8d5a32 100644
--- a/src/libs/fileDownload/FileUtils.js
+++ b/src/libs/fileDownload/FileUtils.js
@@ -118,6 +118,10 @@ function cleanFileName(fileName) {
     return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_');
 }
 
+/**
+ * @param {String} fileName
+ * @returns {String}
+ */
 function appendTimeToFileName(fileName) {
     const file = splitExtensionFromFileName(fileName);
     let newFileName = `${file.fileName} - ${new Date().toISOString()}`;

From 784b95ced1f0d36f9d4b1940cd5c3b46234a97a5 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Wed, 26 Jul 2023 12:10:19 +0800
Subject: [PATCH 119/133] rename function

---
 src/libs/fileDownload/getAttachmentDetails.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/fileDownload/getAttachmentDetails.js b/src/libs/fileDownload/getAttachmentDetails.js
index 8105a78c4799..72cbad115681 100644
--- a/src/libs/fileDownload/getAttachmentDetails.js
+++ b/src/libs/fileDownload/getAttachmentDetails.js
@@ -6,7 +6,7 @@ import tryResolveUrlFromApiRoot from '../tryResolveUrlFromApiRoot';
  * @param {String} html
  * @returns {Object}
  */
-export default function getAttachmentName(html) {
+export default function getAttachmentDetails(html) {
     // Files can be rendered either as anchor tag or as an image so based on that we have to form regex.
     const IS_IMAGE_TAG = /<img([\w\W]+?)\/>/i.test(html);
     const PREVIEW_SOURCE_REGEX = new RegExp(`${CONST.ATTACHMENT_PREVIEW_ATTRIBUTE}*=*"(.+?)"`, 'i');

From 7bf113e1e31fb495cf1b60f24ccd68c7e4d52c3a Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Thu, 27 Jul 2023 12:46:36 +0800
Subject: [PATCH 120/133] make the date format consistent

---
 src/libs/fileDownload/FileUtils.js | 6 +++---
 tests/unit/FileUtilsTest.js        | 5 +++--
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js
index 4166ad8d5a32..fc9aed8b5345 100644
--- a/src/libs/fileDownload/FileUtils.js
+++ b/src/libs/fileDownload/FileUtils.js
@@ -1,7 +1,7 @@
 import {Alert, Linking} from 'react-native';
-import moment from 'moment';
 import CONST from '../../CONST';
 import * as Localize from '../Localize';
+import DateUtils from '../DateUtils';
 
 /**
  * Show alert on successful attachment download
@@ -57,7 +57,7 @@ function getAttachmentName(url) {
     if (!url) {
         return '';
     }
-    return `${moment().format('DDMMYYYYHHmmss')}.${url.split(/[#?]/)[0].split('.').pop().trim()}`;
+    return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop().trim()}`;
 }
 
 /**
@@ -124,7 +124,7 @@ function cleanFileName(fileName) {
  */
 function appendTimeToFileName(fileName) {
     const file = splitExtensionFromFileName(fileName);
-    let newFileName = `${file.fileName} - ${new Date().toISOString()}`;
+    let newFileName = `${file.fileName} - ${DateUtils.getDBTime()}`;
     if (file.fileExtension) {
         newFileName += `.${file.fileExtension}`;
     }
diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
index e03441840010..8e6a0b998944 100644
--- a/tests/unit/FileUtilsTest.js
+++ b/tests/unit/FileUtilsTest.js
@@ -1,3 +1,4 @@
+import DateUtils from '../../src/libs/DateUtils';
 import * as FileUtils from '../../src/libs/fileDownload/FileUtils';
 
 describe('FileUtils', () => {
@@ -24,13 +25,13 @@ describe('FileUtils', () => {
     describe('appendTimeToFileName', () => {
         it('should append current time to the end of the file name', () => {
             const actualFileName = FileUtils.appendTimeToFileName('image.jpg');
-            const expectedFileName = `image - ${new Date().toISOString()}.jpg`;
+            const expectedFileName = `image - ${DateUtils.getDBTime()}.jpg`;
             expect(actualFileName).toEqual(expectedFileName);
         });
 
         it('should append current time to the end of the file name without extension', () => {
             const actualFileName = FileUtils.appendTimeToFileName('image');
-            const expectedFileName = `image - ${new Date().toISOString()}`;
+            const expectedFileName = `image - ${DateUtils.getDBTime()}`;
             expect(actualFileName).toEqual(expectedFileName);
         });
     });

From c0bce398e99fe1e7b6dd633e4f694921f3969c22 Mon Sep 17 00:00:00 2001
From: Bernhard Owen Josephus <bernhard.josephus@gmail.com>
Date: Fri, 28 Jul 2023 12:48:29 +0800
Subject: [PATCH 121/133] remove spaces between hyphen

---
 src/libs/fileDownload/FileUtils.js | 2 +-
 tests/unit/FileUtilsTest.js        | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js
index fc9aed8b5345..9faa5c39626c 100644
--- a/src/libs/fileDownload/FileUtils.js
+++ b/src/libs/fileDownload/FileUtils.js
@@ -124,7 +124,7 @@ function cleanFileName(fileName) {
  */
 function appendTimeToFileName(fileName) {
     const file = splitExtensionFromFileName(fileName);
-    let newFileName = `${file.fileName} - ${DateUtils.getDBTime()}`;
+    let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`;
     if (file.fileExtension) {
         newFileName += `.${file.fileExtension}`;
     }
diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
index 8e6a0b998944..5b6ad7fe3ad3 100644
--- a/tests/unit/FileUtilsTest.js
+++ b/tests/unit/FileUtilsTest.js
@@ -25,13 +25,13 @@ describe('FileUtils', () => {
     describe('appendTimeToFileName', () => {
         it('should append current time to the end of the file name', () => {
             const actualFileName = FileUtils.appendTimeToFileName('image.jpg');
-            const expectedFileName = `image - ${DateUtils.getDBTime()}.jpg`;
+            const expectedFileName = `image-${DateUtils.getDBTime()}.jpg`;
             expect(actualFileName).toEqual(expectedFileName);
         });
 
         it('should append current time to the end of the file name without extension', () => {
             const actualFileName = FileUtils.appendTimeToFileName('image');
-            const expectedFileName = `image - ${DateUtils.getDBTime()}`;
+            const expectedFileName = `image-${DateUtils.getDBTime()}`;
             expect(actualFileName).toEqual(expectedFileName);
         });
     });

From af08d4cca18625cf1584d682253af885d9f2e8f1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 13:18:34 +0200
Subject: [PATCH 122/133] increase animation speed

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 087f64f5128f..37a6d027ec88 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -24,7 +24,7 @@ const MIN_SCALE = 0.7;
 const DOUBLE_TAP_SCALE = 3;
 
 const SPRING_CONFIG = {
-    mass: 3,
+    mass: 1,
     stiffness: 1000,
     damping: 500,
 };

From 848aff607bccf8a7898f997d87aa0cabd1e142e1 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 13:52:05 +0200
Subject: [PATCH 123/133] fix: arrows not showing on web

---
 src/components/Attachments/AttachmentCarousel/index.js    | 2 +-
 .../Attachments/AttachmentCarousel/useCarouselArrows.js   | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 293e22df9dd4..951f15e068da 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -16,7 +16,7 @@ import compose from '../../../libs/compose';
 import useCarouselArrows from './useCarouselArrows';
 import useWindowDimensions from '../../../hooks/useWindowDimensions';
 
-const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen;
+const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
 const viewabilityConfig = {
     // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
     // more than 95% visible. When that happens we update the page index in the state.
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
index 4a6797c7adef..f43a26ab94ee 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
@@ -1,10 +1,10 @@
-import {useCallback, useEffect, useRef, useState, useMemo} from 'react';
+import {useCallback, useEffect, useRef, useState} from 'react';
 import CONST from '../../../CONST';
 import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
 
-function useCarouselArrows() {
-    const canUseTouchScreen = useMemo(() => DeviceCapabilities.canUseTouchScreen(), []);
+const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
 
+function useCarouselArrows() {
     const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen);
     const autoHideArrowTimeout = useRef(null);
 
@@ -25,7 +25,7 @@ function useCarouselArrows() {
         autoHideArrowTimeout.current = setTimeout(() => {
             setShouldShowArrowsInternal(false);
         }, CONST.ARROW_HIDE_DELAY);
-    }, [canUseTouchScreen, cancelAutoHideArrows]);
+    }, [cancelAutoHideArrows]);
 
     const setShouldShowArrows = useCallback(
         (show = true) => {

From 33c626158ff1a260659b19590a8409bbf50bb00b Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 18:19:14 +0200
Subject: [PATCH 124/133] add bounce effect

---
 .../Pager/ImageTransformer.js                 | 62 ++++++++++++-------
 1 file changed, 38 insertions(+), 24 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 37a6d027ec88..442636078425 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -19,9 +19,13 @@ import styles from '../../../../styles/styles';
 import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
-const MAX_SCALE = 20;
-const MIN_SCALE = 0.7;
-const DOUBLE_TAP_SCALE = 3;
+const MIN_ZOOM_SCALE_WITHOUT_BOUNCE = 1;
+const MAX_ZOOM_SCALE_WITHOUT_BOUNCE = 10;
+
+const MIN_ZOOM_SCALE_WITH_BOUNCE = MIN_ZOOM_SCALE_WITHOUT_BOUNCE * 0.7;
+const MAX_ZOOM_SCALE_WITH_BOUNCE = MAX_ZOOM_SCALE_WITHOUT_BOUNCE * 1.5;
+
+const DOUBLE_TAP_SCALE = 2;
 
 const SPRING_CONFIG = {
     mass: 1,
@@ -61,16 +65,14 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     const minImageScale = useMemo(() => Math.min(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
     const maxImageScale = useMemo(() => Math.max(imageScaleX, imageScaleY), [imageScaleX, imageScaleY]);
 
-    const minScale = useMemo(() => Math.min(MIN_SCALE, MIN_SCALE * minImageScale), [minImageScale]);
-    const maxScale = useMemo(() => Math.max(MAX_SCALE, MAX_SCALE * maxImageScale), [maxImageScale]);
     // On double tap zoom to fill, but at least 3x zoom
-    const doubleTapScale = useMemo(() => Math.max(maxImageScale / minImageScale, DOUBLE_TAP_SCALE * minImageScale), [maxImageScale, minImageScale]);
+    const doubleTapScale = useMemo(() => Math.max(maxImageScale / minImageScale, DOUBLE_TAP_SCALE), [maxImageScale, minImageScale]);
 
     const zoomScale = useSharedValue(1);
     // Adding together the pinch zoom scale and the initial scale to fit the image into the canvas
     // Using the smaller imageScale, so that the immage is not bigger than the canvas
     // and not smaller than needed to fit
-    const totalScale = useDerivedValue(() => clamp(zoomScale.value * minImageScale, minScale, maxScale), [minImageScale]);
+    const totalScale = useDerivedValue(() => zoomScale.value * minImageScale, [minImageScale]);
 
     const zoomScaledImageWidth = useDerivedValue(() => imageWidth * totalScale.value, [imageWidth]);
     const zoomScaledImageHeight = useDerivedValue(() => imageHeight * totalScale.value, [imageHeight]);
@@ -85,6 +87,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     // used for moving fingers when pinching
     const pinchTranslateX = useSharedValue(0);
     const pinchTranslateY = useSharedValue(0);
+    const pinchBounceTranslateX = useSharedValue(0);
+    const pinchBounceTranslateY = useSharedValue(0);
 
     // storage for the the origin of the gesture
     const origin = {
@@ -169,7 +173,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
         const deceleration = 0.9915;
 
         if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= maxScale) {
+            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_ZOOM_SCALE_WITH_BOUNCE) {
                 offsetX.value = withDecay({
                     velocity: panVelocityX.value,
                     clamp: [minVector.x, maxVector.x],
@@ -184,7 +188,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
         if (isInBoundaryY) {
             if (
                 Math.abs(panVelocityY.value) > 0 &&
-                zoomScale.value <= maxScale &&
+                zoomScale.value <= MAX_ZOOM_SCALE_WITH_BOUNCE &&
                 // limit vertical pan only when image is smaller than screen
                 offsetY.value !== minVector.y &&
                 offsetY.value !== maxVector.y
@@ -449,16 +453,24 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             origin.y.value = adjustFocal.y;
         })
         .onChange((evt) => {
-            zoomScale.value = clamp(pinchScaleOffset.value * evt.scale, minScale, maxScale);
+            const newZoomScale = pinchScaleOffset.value * evt.scale;
 
-            if (zoomScale.value > minScale && zoomScale.value < maxScale) {
+            if (zoomScale.value >= MIN_ZOOM_SCALE_WITH_BOUNCE && zoomScale.value <= MAX_ZOOM_SCALE_WITH_BOUNCE) {
+                zoomScale.value = newZoomScale;
                 pinchGestureScale.value = evt.scale;
             }
 
-            const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+            const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY);
+            const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1;
+            const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1;
 
-            pinchTranslateX.value = adjustFocal.x + pinchGestureScale.value * origin.x.value * -1;
-            pinchTranslateY.value = adjustFocal.y + pinchGestureScale.value * origin.y.value * -1;
+            if (zoomScale.value >= MIN_ZOOM_SCALE_WITHOUT_BOUNCE && zoomScale.value <= MAX_ZOOM_SCALE_WITHOUT_BOUNCE) {
+                pinchTranslateX.value = newPinchTranslateX;
+                pinchTranslateY.value = newPinchTranslateY;
+            } else {
+                pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value;
+                pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value;
+            }
         })
         .onEnd(() => {
             offsetX.value += pinchTranslateX.value;
@@ -468,15 +480,17 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
             pinchScaleOffset.value = zoomScale.value;
             pinchGestureScale.value = 1;
 
-            if (pinchScaleOffset.value < 1) {
-                // make sure we don't add stuff below the 1
-                pinchScaleOffset.value = 1;
+            if (pinchScaleOffset.value < MIN_ZOOM_SCALE_WITHOUT_BOUNCE) {
+                pinchScaleOffset.value = MIN_ZOOM_SCALE_WITHOUT_BOUNCE;
+                zoomScale.value = withSpring(MIN_ZOOM_SCALE_WITHOUT_BOUNCE, SPRING_CONFIG);
+            } else if (pinchScaleOffset.value > MAX_ZOOM_SCALE_WITHOUT_BOUNCE) {
+                pinchScaleOffset.value = MAX_ZOOM_SCALE_WITHOUT_BOUNCE;
+                zoomScale.value = withSpring(MAX_ZOOM_SCALE_WITHOUT_BOUNCE, SPRING_CONFIG);
+            }
 
-                // this runs the timing animation
-                zoomScale.value = withSpring(1, SPRING_CONFIG);
-            } else if (pinchScaleOffset.value > maxScale) {
-                pinchScaleOffset.value = maxScale;
-                zoomScale.value = withSpring(maxScale, SPRING_CONFIG);
+            if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) {
+                pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG);
+                pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG);
             }
 
             afterGesture();
@@ -498,8 +512,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
     useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]);
 
     const animatedStyles = useAnimatedStyle(() => {
-        const x = pinchTranslateX.value + translateX.value + offsetX.value;
-        const y = pinchTranslateY.value + translateY.value + offsetY.value;
+        const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value;
+        const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value;
 
         if (isSwiping.value) {
             onSwipe(y);

From e7a3f2c768541341c5ad61b990e173b25ee76fec Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Tue, 1 Aug 2023 18:26:50 +0200
Subject: [PATCH 125/133] simplify code

---
 .../Pager/ImageTransformer.js                 | 20 +++++--------------
 1 file changed, 5 insertions(+), 15 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 442636078425..04d241dbd769 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -143,22 +143,14 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
         };
     }, [canvasWidth, canvasHeight]);
 
-    const afterGesture = useWorkletCallback(() => {
+    const afterPanGesture = useWorkletCallback(() => {
         const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds();
 
         if (!canPanVertically.value) {
             offsetY.value = withSpring(target.y, SPRING_CONFIG);
         }
 
-        if (
-            zoomScale.value === 1 &&
-            offsetX.value === 0 &&
-            offsetY.value === 0 &&
-            translateX.value === 0 &&
-            translateY.value === 0 &&
-            pinchTranslateX.value === 0 &&
-            pinchTranslateY.value === 0
-        ) {
+        if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) {
             // we don't need to run any animations
             return;
         }
@@ -173,7 +165,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
         const deceleration = 0.9915;
 
         if (isInBoundaryX) {
-            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_ZOOM_SCALE_WITH_BOUNCE) {
+            if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= MAX_ZOOM_SCALE_WITHOUT_BOUNCE) {
                 offsetX.value = withDecay({
                     velocity: panVelocityX.value,
                     clamp: [minVector.x, maxVector.x],
@@ -188,7 +180,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
         if (isInBoundaryY) {
             if (
                 Math.abs(panVelocityY.value) > 0 &&
-                zoomScale.value <= MAX_ZOOM_SCALE_WITH_BOUNCE &&
+                zoomScale.value <= MAX_ZOOM_SCALE_WITHOUT_BOUNCE &&
                 // limit vertical pan only when image is smaller than screen
                 offsetY.value !== minVector.y &&
                 offsetY.value !== maxVector.y
@@ -416,7 +408,7 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 }
             }
 
-            afterGesture();
+            afterPanGesture();
 
             panVelocityX.value = 0;
             panVelocityY.value = 0;
@@ -493,8 +485,6 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG);
             }
 
-            afterGesture();
-
             pinchGestureRunning.value = false;
         });
 

From ec358faae03bcfe2d5057bb6be83fccd62dd004f Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 10:11:06 +0200
Subject: [PATCH 126/133] fix: focal point not correct in image double tap

---
 .../AttachmentCarousel/Pager/ImageTransformer.js      | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 04d241dbd769..91db44671999 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -25,7 +25,7 @@ const MAX_ZOOM_SCALE_WITHOUT_BOUNCE = 10;
 const MIN_ZOOM_SCALE_WITH_BOUNCE = MIN_ZOOM_SCALE_WITHOUT_BOUNCE * 0.7;
 const MAX_ZOOM_SCALE_WITH_BOUNCE = MAX_ZOOM_SCALE_WITHOUT_BOUNCE * 1.5;
 
-const DOUBLE_TAP_SCALE = 2;
+const DOUBLE_TAP_SCALE = 4;
 
 const SPRING_CONFIG = {
     mass: 1,
@@ -209,9 +209,12 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
 
             stopAnimation();
 
+            const canvasOffsetX = Math.max(0, (canvasWidth - scaledImageWidth) / 2);
+            const canvasOffsetY = Math.max(0, (canvasHeight - scaledImageHeight) / 2);
+
             const imageFocal = {
-                x: clamp(canvasFocalX, 0, scaledImageWidth),
-                y: clamp(canvasFocalY, 0, scaledImageHeight),
+                x: clamp(canvasFocalX - canvasOffsetX, 0, scaledImageWidth),
+                y: clamp(canvasFocalY - canvasOffsetY, 0, scaledImageHeight),
             };
 
             const canvasCenter = {
@@ -239,6 +242,8 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 y: (targetImageCenter.y - canvasCenter.y) * -1,
             };
 
+            console.log({x: (imageFocal.x / scaledImageWidth) * 100, y: (imageFocal.y / scaledImageHeight) * 100});
+
             const koef = {
                 x: (1 / originImageCenter.x) * imageFocal.x - 1,
                 y: (1 / originImageCenter.y) * imageFocal.y - 1,

From bd344746de34257362d9c89577281e834509f241 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 10:16:20 +0200
Subject: [PATCH 127/133] remove log and fix doubleTapScale

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js  | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 91db44671999..64030d893ba2 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -25,7 +25,7 @@ const MAX_ZOOM_SCALE_WITHOUT_BOUNCE = 10;
 const MIN_ZOOM_SCALE_WITH_BOUNCE = MIN_ZOOM_SCALE_WITHOUT_BOUNCE * 0.7;
 const MAX_ZOOM_SCALE_WITH_BOUNCE = MAX_ZOOM_SCALE_WITHOUT_BOUNCE * 1.5;
 
-const DOUBLE_TAP_SCALE = 4;
+const DOUBLE_TAP_SCALE = 3;
 
 const SPRING_CONFIG = {
     mass: 1,
@@ -242,8 +242,6 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
                 y: (targetImageCenter.y - canvasCenter.y) * -1,
             };
 
-            console.log({x: (imageFocal.x / scaledImageWidth) * 100, y: (imageFocal.y / scaledImageHeight) * 100});
-
             const koef = {
                 x: (1 / originImageCenter.x) * imageFocal.x - 1,
                 y: (1 / originImageCenter.y) * imageFocal.y - 1,

From c48f2fab10b3315a7b9d6a4a9082ed994b218002 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 11:16:03 +0200
Subject: [PATCH 128/133] only show image after onLoadEnd is triggered

---
 .../Pager/AttachmentCarouselPage.js             | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index b70376a0a821..d0fd0aa812ff 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -1,5 +1,5 @@
 /* eslint-disable es/no-optional-chaining */
-import React, {useContext, useEffect, useState, useMemo} from 'react';
+import React, {useContext, useEffect, useState} from 'react';
 import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
 import PropTypes from 'prop-types';
 import Image from '../../../Image';
@@ -36,9 +36,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
 
     const dimensions = cachedDimensions.get(source);
 
-    const areImageDimensionsSet = dimensions?.imageWidth !== 0 && dimensions?.imageHeight !== 0;
-
-    const [isImageLoading, setIsImageLoading] = useState(!areImageDimensionsSet);
+    const [isImageLoading, setIsImageLoading] = useState(isActiveProp);
 
     const [isActive, setIsActive] = useState(isActiveProp);
     // We delay setting a page to active state by a (few) millisecond(s),
@@ -49,11 +47,6 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
         else setIsActive(false);
     }, [isActiveProp]);
 
-    useMemo(() => {
-        if (!isActiveProp) return;
-        setIsImageLoading(true);
-    }, [isActiveProp]);
-
     const [showFallback, setShowFallback] = useState(isImageLoading);
     // We delay hiding the fallback image while image transformer is still rendering
     useEffect(() => {
@@ -79,6 +72,8 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                             source={{uri: source}}
                             style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
+                            onLoadStart={() => setIsImageLoading(true)}
+                            onLoadEnd={() => setIsImageLoading(false)}
                             onLoad={(evt) => {
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
                                 const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
@@ -100,9 +95,6 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                                         imageScaleY,
                                     });
                                 }
-
-                                if (imageWidth === 0 || imageHeight === 0) return;
-                                setIsImageLoading(false);
                             }}
                         />
                     </ImageTransformer>
@@ -115,6 +107,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                     <Image
                         source={{uri: source}}
                         isAuthTokenRequired={isAuthTokenRequired}
+                        onLoadStart={() => setIsImageLoading(true)}
                         onLoad={(evt) => {
                             const imageWidth = evt.nativeEvent.width;
                             const imageHeight = evt.nativeEvent.height;

From 48c28f11e8c534ec722e89cb198c8689d7dd7089 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 11:24:34 +0200
Subject: [PATCH 129/133] return undefined style instead of empty object

---
 .../AttachmentCarousel/Pager/AttachmentCarouselPage.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index d0fd0aa812ff..776bb128d12d 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -70,7 +70,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                     >
                         <Image
                             source={{uri: source}}
-                            style={dimensions == null ? {} : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
+                            style={dimensions == null ? undefined : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
                             onLoadStart={() => setIsImageLoading(true)}
                             onLoadEnd={() => setIsImageLoading(false)}

From b2f4f3a63ad43ab3acf6c2ad39d81b984f1cb713 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 14:45:41 +0200
Subject: [PATCH 130/133] fix: initial page not stopping to load

---
 .../AttachmentCarousel/Pager/AttachmentCarouselPage.js      | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index 776bb128d12d..165b7c4da5c6 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -36,9 +36,9 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
 
     const dimensions = cachedDimensions.get(source);
 
-    const [isImageLoading, setIsImageLoading] = useState(isActiveProp);
-
     const [isActive, setIsActive] = useState(isActiveProp);
+    const [isImageLoading, setIsImageLoading] = useState();
+
     // We delay setting a page to active state by a (few) millisecond(s),
     // to prevent the image transformer from flashing while still rendering
     // Instead, we show the fallback image while the image transformer is loading the image
@@ -72,7 +72,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                             source={{uri: source}}
                             style={dimensions == null ? undefined : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
-                            onLoadStart={() => setIsImageLoading(true)}
+                            onLoadStart={() => isImageLoading === false && setIsImageLoading(true)}
                             onLoadEnd={() => setIsImageLoading(false)}
                             onLoad={(evt) => {
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();

From 7dd20ab603c10b3e06fabb23f6a405639a2d9898 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 15:24:17 +0200
Subject: [PATCH 131/133] fix: initial load of first active page

---
 .../Pager/AttachmentCarouselPage.js           | 22 +++++++++++++------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index 165b7c4da5c6..b1a844e4172d 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -31,23 +31,24 @@ const defaultProps = {
     isAuthTokenRequired: false,
 };
 
-function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActiveProp}) {
+function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialIsActive}) {
     const {canvasWidth, canvasHeight} = useContext(AttachmentCarouselPagerContext);
 
     const dimensions = cachedDimensions.get(source);
 
-    const [isActive, setIsActive] = useState(isActiveProp);
-    const [isImageLoading, setIsImageLoading] = useState();
-
+    const [isActive, setIsActive] = useState(initialIsActive);
     // We delay setting a page to active state by a (few) millisecond(s),
     // to prevent the image transformer from flashing while still rendering
     // Instead, we show the fallback image while the image transformer is loading the image
     useEffect(() => {
-        if (isActiveProp) setTimeout(() => setIsActive(true), 1);
+        if (initialIsActive) setTimeout(() => setIsActive(true), 1);
         else setIsActive(false);
-    }, [isActiveProp]);
+    }, [initialIsActive]);
 
+    const [initialActivePageLoad, setInitialActivePageLoad] = useState(isActive);
+    const [isImageLoading, setIsImageLoading] = useState(true);
     const [showFallback, setShowFallback] = useState(isImageLoading);
+
     // We delay hiding the fallback image while image transformer is still rendering
     useEffect(() => {
         if (isImageLoading) setShowFallback(true);
@@ -72,7 +73,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                             source={{uri: source}}
                             style={dimensions == null ? undefined : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
                             isAuthTokenRequired={isAuthTokenRequired}
-                            onLoadStart={() => isImageLoading === false && setIsImageLoading(true)}
+                            onLoadStart={() => setIsImageLoading(true)}
                             onLoadEnd={() => setIsImageLoading(false)}
                             onLoad={(evt) => {
                                 const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
@@ -95,6 +96,13 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: isActive
                                         imageScaleY,
                                     });
                                 }
+
+                                // On the initial render of the active page, the onLoadEnd event is never fired.
+                                // That's why we instead set isImageLoading to false in the onLoad event.
+                                if (initialActivePageLoad) {
+                                    setIsImageLoading(false);
+                                    setInitialActivePageLoad(false);
+                                }
                             }}
                         />
                     </ImageTransformer>

From b3ac3a1ed193882ea19c912c4688aaacd854db04 Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 15:24:34 +0200
Subject: [PATCH 132/133] fix: don't unmount next inactive pages

---
 src/components/Attachments/AttachmentCarousel/Pager/index.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js
index 3cc57b795b7b..636a041cbb83 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js
@@ -143,6 +143,7 @@ function AttachmentCarouselPager({
             >
                 <AnimatedPagerView
                     pageMargin={40}
+                    offscreenPageLimit={1}
                     onPageScroll={pageScrollHandler}
                     animatedProps={animatedProps}
                     onPageSelected={onPageSelected}

From 703a2308a1df06ca250d3287eabde094c619383e Mon Sep 17 00:00:00 2001
From: Christoph Pader <christoph@margelo.io>
Date: Wed, 2 Aug 2023 15:30:42 +0200
Subject: [PATCH 133/133] increase max zoom scale

---
 .../Attachments/AttachmentCarousel/Pager/ImageTransformer.js    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 64030d893ba2..4475df168df2 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -20,7 +20,7 @@ import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
 import ImageWrapper from './ImageWrapper';
 
 const MIN_ZOOM_SCALE_WITHOUT_BOUNCE = 1;
-const MAX_ZOOM_SCALE_WITHOUT_BOUNCE = 10;
+const MAX_ZOOM_SCALE_WITHOUT_BOUNCE = 20;
 
 const MIN_ZOOM_SCALE_WITH_BOUNCE = MIN_ZOOM_SCALE_WITHOUT_BOUNCE * 0.7;
 const MAX_ZOOM_SCALE_WITH_BOUNCE = MAX_ZOOM_SCALE_WITHOUT_BOUNCE * 1.5;
