From 597b942883b79d1a58d4c525d955d2754fd809fa Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Thu, 28 May 2026 21:16:05 -0400 Subject: [PATCH 1/9] chore(deps): Install latest beta of RNGH v3 - v3 was released as stable today, but we won't install it immediately due to our PNPM rules of waiting a day after a package has been released before installing it. --- mobile/package.json | 2 +- mobile/pnpm-lock.yaml | 32 +++++------------------------- mobile/src/resources/licenses.json | 2 +- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/mobile/package.json b/mobile/package.json index b7c45e329..06c45e229 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -59,7 +59,7 @@ "react-native-android-widget": "0.20.3", "react-native-audio-browser": "github:MissingCore/react-native-audio-browser#bb9da301fdc284e144a54488c83c1af87b0ba61f", "react-native-bootsplash": "7.3.1", - "react-native-gesture-handler": "2.31.2", + "react-native-gesture-handler": "3.0.0-beta.5", "react-native-keyboard-controller": "1.21.8", "react-native-markdown-renderer": "4.1.1", "react-native-nitro-modules": "0.35.7", diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 51223f829..05f50c4fc 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -121,8 +121,8 @@ importers: specifier: 7.3.1 version: 7.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native-gesture-handler: - specifier: 2.31.2 - version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + specifier: 3.0.0-beta.5 + version: 3.0.0-beta.5(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.8 version: 1.21.8(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) @@ -619,10 +619,6 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@egjs/hammerjs@2.0.17': - resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} - engines: {node: '>=0.8.0'} - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -1979,9 +1975,6 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/hammerjs@2.0.46': - resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -3444,9 +3437,6 @@ packages: hermes-parser@0.35.0: resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hosted-git-info@6.1.3: resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4598,8 +4588,8 @@ packages: react: '*' react-native: '*' - react-native-gesture-handler@2.31.2: - resolution: {integrity: sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A==} + react-native-gesture-handler@3.0.0-beta.5: + resolution: {integrity: sha512-tDoS9I5Jtf8tPDLwMR8VtiPT6K1+67+7D5wI06gz/HGiBvr8NpY17n3xwq4BFu9y4EASLAJQGVIfcHKexPrJLw==} peerDependencies: react: '*' react-native: '*' @@ -5897,10 +5887,6 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} - '@egjs/hammerjs@2.0.17': - dependencies: - '@types/hammerjs': 2.0.46 - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -7202,8 +7188,6 @@ snapshots: '@types/estree@1.0.9': {} - '@types/hammerjs@2.0.46': {} - '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -8871,10 +8855,6 @@ snapshots: dependencies: hermes-estree: 0.35.0 - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - hosted-git-info@6.1.3: dependencies: lru-cache: 7.18.3 @@ -10003,11 +9983,9 @@ snapshots: - supports-color - typescript - react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + react-native-gesture-handler@3.0.0-beta.5(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: - '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 - hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3) diff --git a/mobile/src/resources/licenses.json b/mobile/src/resources/licenses.json index 14e93b649..a441c1a57 100644 --- a/mobile/src/resources/licenses.json +++ b/mobile/src/resources/licenses.json @@ -288,7 +288,7 @@ }, "react-native-gesture-handler": { "name": "react-native-gesture-handler", - "version": "2.31.2", + "version": "3.0.0-beta.5", "source": "https://github.com/software-mansion/react-native-gesture-handler", "license": "MIT", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Software Mansion \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." From 574c21abe0fccbce1b13af52f8a55881d7f3212d Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Thu, 28 May 2026 21:47:00 -0400 Subject: [PATCH 2/9] refactor: Rewrite `useVinylSeekbar` to use RNGH 3 - Gestures are now created with hooks. - `onStart` & `onEnd` were renamed. - We can pass SharedValue to the config properties. --- .../now-playing/helpers/useVinylSeekbar.ts | 109 ++++++++---------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts b/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts index 35005ac79..174791a37 100644 --- a/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts +++ b/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts @@ -1,6 +1,6 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { useCallback, useMemo, useRef, useState } from "react"; -import { Gesture } from "react-native-gesture-handler"; +import { useCallback, useMemo, useRef } from "react"; +import { usePanGesture } from "react-native-gesture-handler"; import type Animated from "react-native-reanimated"; import { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import { scheduleOnRN } from "react-native-worklets"; @@ -65,68 +65,55 @@ export function useVinylSeekbar() { //#endregion //#region Gesture - const [gestureInBound, setGestureInBound] = useState(true); + const gestureInBound = useSharedValue(true); const prevAngle = useSharedValue(0); const hasUpdatedPosition = useSharedValue(false); - const seekGesture = useMemo( - () => - Gesture.Pan() - .shouldCancelWhenOutside(true) - .enabled(gestureInBound) - .onStart(({ absoluteX, absoluteY }) => { - if (isWithinBound({ absoluteX, absoluteY })) { - scheduleOnRN(setIsSeeking, true); - prevAngle.set(getAngle({ absoluteX, absoluteY })); - } else { - scheduleOnRN(setGestureInBound, false); - } - }) - .onUpdate(({ absoluteX, absoluteY }) => { - if (isWithinBound({ absoluteX, absoluteY })) { - let currAngle = getAngle({ absoluteX, absoluteY }); - // Ensure arctan calculation is continuous. - while (currAngle < prevAngle.get() - Math.PI) - currAngle += 2 * Math.PI; - while (currAngle > prevAngle.get() + Math.PI) - currAngle -= 2 * Math.PI; - const rotateAmount = - ((currAngle - prevAngle.get()) * 180) / Math.PI; - - prevAngle.set(currAngle); - - // Calculate new position. - const changeDelta = convertUnit(rotateAmount, "degrees"); - const newPosition = timedPosition.get() + changeDelta; - if (newPosition < 0) timedPosition.set(0); - else if (newPosition > duration) timedPosition.set(duration); - else timedPosition.set(newPosition); - - hasUpdatedPosition.set(true); - } else { - scheduleOnRN(setGestureInBound, false); - } - }) - .onEnd(() => { - if (hasUpdatedPosition.get()) - scheduleOnRN(PlaybackControls.seekTo, timedPosition.get()); - }) - .onFinalize(() => { - scheduleOnRN(setIsSeeking, false); - scheduleOnRN(setGestureInBound, true); - hasUpdatedPosition.set(false); - }), - [ - isWithinBound, - getAngle, - setIsSeeking, - timedPosition, - duration, - prevAngle, - hasUpdatedPosition, - gestureInBound, - ], - ); + const seekGesture = usePanGesture({ + shouldCancelWhenOutside: true, + enabled: gestureInBound, + onActivate: ({ absoluteX, absoluteY }) => { + if (!isWithinBound({ absoluteX, absoluteY })) { + gestureInBound.set(false); + return; + } + + scheduleOnRN(setIsSeeking, true); + prevAngle.set(getAngle({ absoluteX, absoluteY })); + }, + onUpdate: ({ absoluteX, absoluteY }) => { + if (!isWithinBound({ absoluteX, absoluteY })) { + gestureInBound.set(false); + return; + } + + let currAngle = getAngle({ absoluteX, absoluteY }); + // Ensure arctan calculation is continuous. + while (currAngle < prevAngle.get() - Math.PI) currAngle += 2 * Math.PI; + while (currAngle > prevAngle.get() + Math.PI) currAngle -= 2 * Math.PI; + const rotateAmount = ((currAngle - prevAngle.get()) * 180) / Math.PI; + + prevAngle.set(currAngle); + + // Calculate new position. + const changeDelta = convertUnit(rotateAmount, "degrees"); + const newPosition = timedPosition.get() + changeDelta; + if (newPosition < 0) timedPosition.set(0); + else if (newPosition > duration) timedPosition.set(duration); + else timedPosition.set(newPosition); + + hasUpdatedPosition.set(true); + }, + onDeactivate: () => { + if (hasUpdatedPosition.get()) + scheduleOnRN(PlaybackControls.seekTo, timedPosition.get()); + }, + onFinalize: () => { + scheduleOnRN(setIsSeeking, false); + gestureInBound.set(true); + hasUpdatedPosition.set(false); + }, + }); //#endregion return useMemo( From 2be4ba999dff8918fb9b4c45e5ed8ff465c5fa9d Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Thu, 28 May 2026 22:24:17 -0400 Subject: [PATCH 3/9] chore: Convert remaining uses of RNGH to use v3 --- mobile/src/components/DragList.tsx | 111 ++++++++---------- mobile/src/components/Form/Slider.tsx | 88 ++++++-------- mobile/src/components/NScrollbar.tsx | 43 ++++--- mobile/src/components/Swipeable.tsx | 24 ++-- .../theme/components/ColorPicker.tsx | 97 +++++++-------- .../src/navigation/components/MiniPlayer.tsx | 29 ++--- .../now-playing/helpers/useVinylSeekbar.ts | 6 +- 7 files changed, 174 insertions(+), 224 deletions(-) diff --git a/mobile/src/components/DragList.tsx b/mobile/src/components/DragList.tsx index 65d700fee..7dbed9b20 100644 --- a/mobile/src/components/DragList.tsx +++ b/mobile/src/components/DragList.tsx @@ -1,12 +1,10 @@ +import { createContext, use, useCallback, useMemo, useRef } from "react"; import { - createContext, - use, - useCallback, - useMemo, - useRef, - useState, -} from "react"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; + GestureDetector, + useNativeGesture, + usePanGesture, + useSimultaneousGestures, +} from "react-native-gesture-handler"; import type { SharedValue } from "react-native-reanimated"; import Animated, { clamp, @@ -71,7 +69,7 @@ function DragListImpl({ onReordered, ...props }: DragListProps) { - const [enabled, setEnabled] = useState(true); + const enabled = useSharedValue(true); const dataRef = useRef(data); const pan = useDragListStore((s) => s.pan); @@ -123,8 +121,9 @@ function DragListImpl({ pan.set(0); shifted.set(0); }); - setEnabled(true); + enabled.set(true); }, [ + enabled, setReactiveActiveIndex, autoScrollDirection, autoScrollAmount, @@ -133,55 +132,39 @@ function DragListImpl({ shifted, ]); - const panListenerGesture = useMemo( - () => - Gesture.Pan() - .enabled(enabled) - .onStart(() => { - // Bail out of gesture early. - if (activeIndex.get() === INACTIVE) scheduleOnRN(setEnabled, false); - }) - .onUpdate(({ translationY, y }) => { - //? Stop auto-scroll when we move (clears the timer which continues the auto-scroll). - autoScrollDirection.set(0); - if (activeIndex.get() === INACTIVE) return; - pan.set(autoScrollAmount.get() + translationY); - revalidatedShifted(); - - //? Auto-scroll handling. - let direction = 0; - if (y < estimatedItemSize) direction = -1; - else if (y > listHeight.get() - estimatedItemSize) direction = 1; - autoScrollDirection.set(direction); - }) - .onFinalize(() => { - // Ensure we reset the gesture focus. - scheduleOnRN(setEnabled, false); - if (activeIndex.get() !== INACTIVE) { - if (onDragEnd) scheduleOnRN(onDragEnd); - scheduleOnRN( - onReordered, - activeIndex.get(), - activeIndex.get() + shifted.get(), - ); - } - scheduleOnRN(onCleanup); - }), - [ - enabled, - listHeight, - autoScrollDirection, - autoScrollAmount, - revalidatedShifted, - estimatedItemSize, - activeIndex, - pan, - onDragEnd, - onReordered, - onCleanup, - shifted, - ], - ); + const panListenerGesture = usePanGesture({ + enabled, + onActivate: () => { + // Bail out of gesture early. + if (activeIndex.get() === INACTIVE) enabled.set(false); + }, + onUpdate: ({ translationY, y }) => { + //? Stop auto-scroll when we move (clears the timer which continues the auto-scroll). + autoScrollDirection.set(0); + if (activeIndex.get() === INACTIVE) return; + pan.set(autoScrollAmount.get() + translationY); + revalidatedShifted(); + + //? Auto-scroll handling. + let direction = 0; + if (y < estimatedItemSize) direction = -1; + else if (y > listHeight.get() - estimatedItemSize) direction = 1; + autoScrollDirection.set(direction); + }, + onFinalize: () => { + // Ensure we reset the gesture focus. + enabled.set(false); + if (activeIndex.get() !== INACTIVE) { + if (onDragEnd) scheduleOnRN(onDragEnd); + scheduleOnRN( + onReordered, + activeIndex.get(), + activeIndex.get() + shifted.get(), + ); + } + scheduleOnRN(onCleanup); + }, + }); useAnimatedReaction( () => autoScrollDirection.get(), @@ -206,11 +189,9 @@ function DragListImpl({ }, ); - const gesture = useMemo( - // `Gesture.Native()` allows for scroll to work. - () => Gesture.Simultaneous(Gesture.Native(), panListenerGesture), - [panListenerGesture], - ); + // `Gesture.Native()` allows for scroll to work. + const nativeGesture = useNativeGesture({}); + const gestures = useSimultaneousGestures(nativeGesture, panListenerGesture); const renderDragItem = useCallback( (info: ListRenderItemInfo) => ( @@ -228,7 +209,7 @@ function DragListImpl({ } return ( - + - Gesture.Tap() - .enabled(!props.disabled) - .onBegin(() => setIsInteracting(true)) - .onEnd(({ x, y }) => { - const finalizedValue = calculateNextValue(onVerticalWorklet(y, x)); - setCurrVal(finalizedValue); - setIsInteracting(false); - if (onCompleteRef.current) - scheduleOnRN(onCompleteRef.current, finalizedValue); - }), - [ - onVerticalWorklet, - calculateNextValue, - setIsInteracting, - setCurrVal, - props.disabled, - ], - ); + const tapGesture = useTapGesture({ + enabled: !props.disabled, + onBegin: () => setIsInteracting(true), + onDeactivate: ({ x, y }) => { + const finalizedValue = calculateNextValue(onVerticalWorklet(y, x)); + setCurrVal(finalizedValue); + setIsInteracting(false); + if (onCompleteRef.current) + scheduleOnRN(onCompleteRef.current, finalizedValue); + }, + }); - const panGesture = useMemo( - () => - Gesture.Pan() - .enabled(!props.disabled) - .onBegin(() => setIsInteracting(true)) - .onStart(({ x, y }) => debounceFrom.set(onVerticalWorklet(y, x))) - .onUpdate(({ x, y, velocityX, velocityY }) => { - const nextValue = calculateNextValue(onVerticalWorklet(y, x)); - setCurrVal(nextValue); - debouncedOnChange(nextValue, onVerticalWorklet(velocityY, velocityX)); - }) - .onEnd(({ x, y }) => { - const finalizedValue = calculateNextValue(onVerticalWorklet(y, x)); - setCurrVal(finalizedValue); - if (onCompleteRef.current) - scheduleOnRN(onCompleteRef.current, finalizedValue); - }) - .onFinalize(() => setIsInteracting(false)), - [ - onVerticalWorklet, - calculateNextValue, - setIsInteracting, - setCurrVal, - debounceFrom, - debouncedOnChange, - props.disabled, - ], - ); + const panGesture = usePanGesture({ + enabled: !props.disabled, + onBegin: () => setIsInteracting(true), + onActivate: ({ x, y }) => debounceFrom.set(onVerticalWorklet(y, x)), + onUpdate: ({ x, y, velocityX, velocityY }) => { + const nextValue = calculateNextValue(onVerticalWorklet(y, x)); + setCurrVal(nextValue); + debouncedOnChange(nextValue, onVerticalWorklet(velocityY, velocityX)); + }, + onDeactivate: ({ x, y }) => { + const finalizedValue = calculateNextValue(onVerticalWorklet(y, x)); + setCurrVal(finalizedValue); + if (onCompleteRef.current) + scheduleOnRN(onCompleteRef.current, finalizedValue); + }, + onFinalize: () => setIsInteracting(false), + }); - const gestures = useMemo( - () => Gesture.Race(tapGesure, panGesture), - [tapGesure, panGesture], - ); + const gestures = useCompetingGestures(tapGesture, panGesture); //#endregion //#region Styling diff --git a/mobile/src/components/NScrollbar.tsx b/mobile/src/components/NScrollbar.tsx index e6b8cd697..247c4ad27 100644 --- a/mobile/src/components/NScrollbar.tsx +++ b/mobile/src/components/NScrollbar.tsx @@ -1,6 +1,11 @@ import { useCallback, useMemo, useState } from "react"; import type { LayoutChangeEvent } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { + GestureDetector, + useLongPressGesture, + usePanGesture, + useSimultaneousGestures, +} from "react-native-gesture-handler"; import type { AnimatedRef, SharedValue } from "react-native-reanimated"; import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"; import Animated, { @@ -122,19 +127,20 @@ export function Scrollbar({ //#endregion //#region Gestures - const pressGesture = Gesture.LongPress() - .enabled(scrollbarVisible) - .minDuration(0) - .onStart(persistScrollbar) - .onEnd(dismissScrollbar); - - const scrollGesture = Gesture.Pan() - .enabled(scrollbarVisible) - .onStart(({ absoluteY }) => { + const pressGesture = useLongPressGesture({ + enabled: scrollbarVisible, + minDuration: 0, + onActivate: persistScrollbar, + onDeactivate: dismissScrollbar, + }); + + const scrollGesture = usePanGesture({ + enabled: scrollbarVisible, + onActivate: ({ absoluteY }) => { persistScrollbar(); prevY.set(absoluteY); - }) - .onUpdate(({ absoluteY }) => { + }, + onUpdate: ({ absoluteY }) => { persistScrollbar(); const changeDelta = absoluteY - prevY.get(); const clampedScaledPosition = clamp( @@ -147,17 +153,18 @@ export function Scrollbar({ nextScrollPosition.set(unscaledScrollAmount); prevY.set(absoluteY); - }) - .onEnd(() => { + }, + onDeactivate: () => { dismissScrollbar(); nextScrollPosition.set(-1); prevY.set(-1); - }) - .onFinalize(() => { + }, + onFinalize: () => { if (onEnd) onEnd(); - }); + }, + }); - const gestures = Gesture.Simultaneous(pressGesture, scrollGesture); + const gestures = useSimultaneousGestures(pressGesture, scrollGesture); //#endregion //#region Styles diff --git a/mobile/src/components/Swipeable.tsx b/mobile/src/components/Swipeable.tsx index dbb55428c..d9ff7bac7 100644 --- a/mobile/src/components/Swipeable.tsx +++ b/mobile/src/components/Swipeable.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { LayoutChangeEvent } from "react-native"; import { Animated, View } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { GestureDetector, usePanGesture } from "react-native-gesture-handler"; import { NothingArrowRight } from "~/resources/icons/NothingArrowRight"; @@ -76,19 +76,16 @@ export function Swipeable({ [props.onSwipeLeft, props.onSwipeRight], ); - const swipeGesture = Gesture.Pan() + const swipeGesture = usePanGesture({ // Since we're not using `react-native-reanimated`. - .runOnJS(true) + runOnJS: true, // Allows scrolling to work without triggering gesture. - .activeOffsetX([-10, 10]) - .enabled(!disabled) - .onStart(() => { - animationRef.current?.stop(); - }) - .onUpdate(({ translationX }) => { - dragX.setValue(clampSwipeAmount(translationX)); - }) - .onEnd(({ translationX, velocityX }) => { + activeOffsetX: [-10, 10], + enabled: !disabled, + onActivate: () => animationRef.current?.stop(), + onUpdate: ({ translationX }) => + dragX.setValue(clampSwipeAmount(translationX)), + onDeactivate: ({ translationX, velocityX }) => { // Include velocity in final translated amount if overshoot is enabled. const velocityDistance = (overshootSwipe ? 1 : 0) * velocityX * DRAG_TOSS; const clampedTranslation = clampSwipeAmount( @@ -148,7 +145,8 @@ export function Swipeable({ useNativeDriver: true, }); }); - }); + }, + }); const onRowLayout = useCallback((e: LayoutChangeEvent) => { rowWidth.current = e.nativeEvent.layout.width; diff --git a/mobile/src/modules/customization/theme/components/ColorPicker.tsx b/mobile/src/modules/customization/theme/components/ColorPicker.tsx index 1026dce27..e80085713 100644 --- a/mobile/src/modules/customization/theme/components/ColorPicker.tsx +++ b/mobile/src/modules/customization/theme/components/ColorPicker.tsx @@ -1,7 +1,12 @@ import { LinearGradient } from "expo-linear-gradient"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import { View } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { + GestureDetector, + useCompetingGestures, + usePanGesture, + useTapGesture, +} from "react-native-gesture-handler"; import Animated, { clamp, useAnimatedStyle, @@ -63,34 +68,26 @@ export function ColorPicker({ }, [hue, saturation, brightness, onComplete]); //#region Panel Gestures + Styles - const panelTapGesture = useMemo( - () => - Gesture.Tap().onEnd(({ x, y }) => { - if (contentWidth.get() <= 0) return; - saturation.set(clamp(x / contentWidth.get(), 0, 1)); - brightness.set(clamp(1 - y / PANEL_HEIGHT, 0, 1)); - - scheduleOnRN(emitCurrentColor); - }), - [contentWidth, saturation, brightness, emitCurrentColor], - ); - - const panelPanGesture = useMemo( - () => - Gesture.Pan() - .onUpdate(({ x, y }) => { - if (contentWidth.get() <= 0) return; - saturation.set(clamp(x / contentWidth.get(), 0, 1)); - brightness.set(clamp(1 - y / PANEL_HEIGHT, 0, 1)); - }) - .onEnd(() => scheduleOnRN(emitCurrentColor)), - [contentWidth, saturation, brightness, emitCurrentColor], - ); - - const panelGestures = useMemo( - () => Gesture.Race(panelTapGesture, panelPanGesture), - [panelPanGesture, panelTapGesture], - ); + const panelTapGesture = useTapGesture({ + onDeactivate: ({ x, y }) => { + if (contentWidth.get() <= 0) return; + saturation.set(clamp(x / contentWidth.get(), 0, 1)); + brightness.set(clamp(1 - y / PANEL_HEIGHT, 0, 1)); + + scheduleOnRN(emitCurrentColor); + }, + }); + + const panelPanGesture = usePanGesture({ + onUpdate: ({ x, y }) => { + if (contentWidth.get() <= 0) return; + saturation.set(clamp(x / contentWidth.get(), 0, 1)); + brightness.set(clamp(1 - y / PANEL_HEIGHT, 0, 1)); + }, + onDeactivate: () => scheduleOnRN(emitCurrentColor), + }); + + const panelGestures = useCompetingGestures(panelTapGesture, panelPanGesture); const panelStyle = useAnimatedStyle(() => ({ height: PANEL_HEIGHT, @@ -109,32 +106,24 @@ export function ColorPicker({ //#endregion //#region Hue Slider Gestures + Styles - const hueTapGesture = useMemo( - () => - Gesture.Tap().onEnd(({ x }) => { - if (contentWidth.get() <= 0) return; - hue.set(clamp(((x / contentWidth.get()) * 360) | 0, 0, 360)); + const hueTapGesture = useTapGesture({ + onDeactivate: ({ x }) => { + if (contentWidth.get() <= 0) return; + hue.set(clamp(((x / contentWidth.get()) * 360) | 0, 0, 360)); - scheduleOnRN(emitCurrentColor); - }), - [contentWidth, hue, emitCurrentColor], - ); + scheduleOnRN(emitCurrentColor); + }, + }); - const huePanGesture = useMemo( - () => - Gesture.Pan() - .onUpdate(({ x }) => { - if (contentWidth.get() <= 0) return; - hue.set(clamp(((x / contentWidth.get()) * 360) | 0, 0, 360)); - }) - .onEnd(() => scheduleOnRN(emitCurrentColor)), - [contentWidth, hue, emitCurrentColor], - ); + const huePanGesture = usePanGesture({ + onUpdate: ({ x }) => { + if (contentWidth.get() <= 0) return; + hue.set(clamp(((x / contentWidth.get()) * 360) | 0, 0, 360)); + }, + onDeactivate: () => scheduleOnRN(emitCurrentColor), + }); - const hueGesture = useMemo( - () => Gesture.Race(hueTapGesture, huePanGesture), - [huePanGesture, hueTapGesture], - ); + const hueGestures = useCompetingGestures(hueTapGesture, huePanGesture); const sliderHandleStyle = useAnimatedStyle(() => ({ height: HANDLE_SIZE, @@ -174,7 +163,7 @@ export function ColorPicker({ - + panAmount.set(0), 1000); }, [panAmount, resetPlaybackStore]); - const panGesture = useMemo( - () => - Gesture.Pan() - .enabled(dragClearPlayback) - // Only register for vertical pan, allowing swipe gesture to work. - .activeOffsetY([-10, 10]) - .onUpdate(({ translationY }) => - panAmount.set(Math.max(0, translationY)), - ) - .onEnd(({ velocityY }) => { - //? Resetting the playback store is based off pan velocity. - const metThreshold = velocityY > 500; - panAmount.set(withSpring(metThreshold ? insets.bottom + 256 : 0)); - }), - [panAmount, insets, dragClearPlayback], - ); + const panGesture = usePanGesture({ + enabled: dragClearPlayback, + // Only register for vertical pan, allowing swipe gesture to work. + activeOffsetY: [-10, 10], + onUpdate: ({ translationY }) => panAmount.set(Math.max(0, translationY)), + onDeactivate: ({ velocityY }) => { + //? Resetting the playback store is based off pan velocity. + const metThreshold = velocityY > 500; + panAmount.set(withSpring(metThreshold ? insets.bottom + 256 : 0)); + }, + }); useAnimatedReaction( () => panAmount.get(), diff --git a/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts b/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts index 174791a37..2987c63e7 100644 --- a/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts +++ b/mobile/src/navigation/screens/now-playing/helpers/useVinylSeekbar.ts @@ -74,8 +74,7 @@ export function useVinylSeekbar() { enabled: gestureInBound, onActivate: ({ absoluteX, absoluteY }) => { if (!isWithinBound({ absoluteX, absoluteY })) { - gestureInBound.set(false); - return; + return gestureInBound.set(false); } scheduleOnRN(setIsSeeking, true); @@ -83,8 +82,7 @@ export function useVinylSeekbar() { }, onUpdate: ({ absoluteX, absoluteY }) => { if (!isWithinBound({ absoluteX, absoluteY })) { - gestureInBound.set(false); - return; + return gestureInBound.set(false); } let currAngle = getAngle({ absoluteX, absoluteY }); From 81b7915c6739b397072ef2cb1ce90e05e5a22839 Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Thu, 28 May 2026 22:25:49 -0400 Subject: [PATCH 4/9] chore: Also convert our Toast gestures --- mobile/modules/toast/src/components/Toast.tsx | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/mobile/modules/toast/src/components/Toast.tsx b/mobile/modules/toast/src/components/Toast.tsx index 76ca25ed4..9803bfe5a 100644 --- a/mobile/modules/toast/src/components/Toast.tsx +++ b/mobile/modules/toast/src/components/Toast.tsx @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import { StyleSheet, Text, useWindowDimensions } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { GestureDetector, usePanGesture } from "react-native-gesture-handler"; import Animated, { useAnimatedStyle, useSharedValue, @@ -36,28 +36,23 @@ export function Toast({ toast, exiting, theme }: Props) { //#region Dismiss Gesture const panAmount = useSharedValue(0); - const panGesture = useMemo( - () => - Gesture.Pan() - .enabled(!exiting) - .activeOffsetY([-10, 10]) - .onUpdate(({ translationY }) => - panAmount.set(Math.min(0, translationY)), - ) - .onEnd(({ velocityY }) => { - const metThreshold = velocityY < -500; - panAmount.set( - withSpring( - metThreshold ? -(topOffset + toastHeight.get() + 256) : 0, - undefined, - (finished) => { - if (finished && metThreshold) scheduleOnRN(onRemove); - }, - ), - ); - }), - [panAmount, toastHeight, topOffset, exiting, onRemove], - ); + const panGesture = usePanGesture({ + enabled: !exiting, + activeOffsetY: [-10, 10], + onUpdate: ({ translationY }) => panAmount.set(Math.min(0, translationY)), + onDeactivate: ({ velocityY }) => { + const metThreshold = velocityY < -500; + panAmount.set( + withSpring( + metThreshold ? -(topOffset + toastHeight.get() + 256) : 0, + undefined, + (finished) => { + if (finished && metThreshold) scheduleOnRN(onRemove); + }, + ), + ); + }, + }); //#endregion //#region Enter/Exit Animations From 4ddec37db94be27c83e9c4a28420bf45d0be4e42 Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Thu, 28 May 2026 22:27:42 -0400 Subject: [PATCH 5/9] chore: Remove unnecessary ts-expect-error --- mobile/src/components/Form/Input.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile/src/components/Form/Input.tsx b/mobile/src/components/Form/Input.tsx index c23ff307d..da6113d9a 100644 --- a/mobile/src/components/Form/Input.tsx +++ b/mobile/src/components/Form/Input.tsx @@ -34,7 +34,6 @@ export function NumericInput({ ); return ( - // @ts-expect-error - Ref is compatible. Date: Thu, 28 May 2026 22:37:08 -0400 Subject: [PATCH 6/9] chore: Fix comment --- mobile/src/components/DragList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/components/DragList.tsx b/mobile/src/components/DragList.tsx index 7dbed9b20..167ed1808 100644 --- a/mobile/src/components/DragList.tsx +++ b/mobile/src/components/DragList.tsx @@ -189,7 +189,7 @@ function DragListImpl({ }, ); - // `Gesture.Native()` allows for scroll to work. + // The "Native" gesture allows for scroll to work. const nativeGesture = useNativeGesture({}); const gestures = useSimultaneousGestures(nativeGesture, panListenerGesture); From d461c216c1c4fc90754d50e92f5f8eff27eebaf4 Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Fri, 29 May 2026 14:17:06 -0400 Subject: [PATCH 7/9] chore: Use stable version of RNGH v3 --- mobile/package.json | 2 +- mobile/pnpm-lock.yaml | 10 +++++----- mobile/src/resources/licenses.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mobile/package.json b/mobile/package.json index 06c45e229..109dd1202 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -59,7 +59,7 @@ "react-native-android-widget": "0.20.3", "react-native-audio-browser": "github:MissingCore/react-native-audio-browser#bb9da301fdc284e144a54488c83c1af87b0ba61f", "react-native-bootsplash": "7.3.1", - "react-native-gesture-handler": "3.0.0-beta.5", + "react-native-gesture-handler": "3.0.0", "react-native-keyboard-controller": "1.21.8", "react-native-markdown-renderer": "4.1.1", "react-native-nitro-modules": "0.35.7", diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 05f50c4fc..93c36e537 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -121,8 +121,8 @@ importers: specifier: 7.3.1 version: 7.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native-gesture-handler: - specifier: 3.0.0-beta.5 - version: 3.0.0-beta.5(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) + specifier: 3.0.0 + version: 3.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.8 version: 1.21.8(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3) @@ -4588,8 +4588,8 @@ packages: react: '*' react-native: '*' - react-native-gesture-handler@3.0.0-beta.5: - resolution: {integrity: sha512-tDoS9I5Jtf8tPDLwMR8VtiPT6K1+67+7D5wI06gz/HGiBvr8NpY17n3xwq4BFu9y4EASLAJQGVIfcHKexPrJLw==} + react-native-gesture-handler@3.0.0: + resolution: {integrity: sha512-6E8o9D2sHwhFGiU0c4aCweMdJwIbQeBV+dq3IQ3HcqKhVGzg7ccEycap6i0zGCtIYfs3V29Xd4OycwcRj5qxBQ==} peerDependencies: react: '*' react-native: '*' @@ -9983,7 +9983,7 @@ snapshots: - supports-color - typescript - react-native-gesture-handler@3.0.0-beta.5(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): + react-native-gesture-handler@3.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.3))(react@19.2.3): dependencies: '@types/react-test-renderer': 19.1.0 invariant: 2.2.4 diff --git a/mobile/src/resources/licenses.json b/mobile/src/resources/licenses.json index a441c1a57..02ff0dd29 100644 --- a/mobile/src/resources/licenses.json +++ b/mobile/src/resources/licenses.json @@ -288,7 +288,7 @@ }, "react-native-gesture-handler": { "name": "react-native-gesture-handler", - "version": "3.0.0-beta.5", + "version": "3.0.0", "source": "https://github.com/software-mansion/react-native-gesture-handler", "license": "MIT", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Software Mansion \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." From 0a53cbb2898ba32ce048d2d83a4741c0e0010a96 Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Fri, 29 May 2026 15:02:33 -0400 Subject: [PATCH 8/9] fix: Re-add some default styling to our DragList - Since to apply our "fake gaps", we add a "mb-2" style basically to every rendered item, this would result in a bigger gap at the bottom of the list. To counter this, we had a `-mb-2` style on the draglist. --- mobile/src/components/DragList.tsx | 10 ++++++++++ .../screens/settings/sheets/TabOrderSheet.tsx | 1 + 2 files changed, 11 insertions(+) diff --git a/mobile/src/components/DragList.tsx b/mobile/src/components/DragList.tsx index 167ed1808..8c2493d82 100644 --- a/mobile/src/components/DragList.tsx +++ b/mobile/src/components/DragList.tsx @@ -21,6 +21,7 @@ import { scheduleOnRN, scheduleOnUI } from "react-native-worklets"; import type { StoreApi } from "zustand"; import { createStore, useStore } from "zustand"; +import { cn } from "~/lib/style"; import type { LegendListProps, ListRenderItemInfo } from "./Base/LegendList"; import { LegendList, useAnimatedLegendListRef } from "./Base/LegendList"; @@ -45,6 +46,9 @@ interface DragListProps extends Omit< onDragEnd?: VoidFunction; /** Called when an item is successfully moved. */ onReordered: (fromIndex: number, toIndex: number) => void; + + /** If default stylings to fix "fake gaps" will be applied. Defaults to `true`. */ + useDefaultStyles?: boolean; } const INACTIVE = -1; @@ -64,6 +68,7 @@ function DragListImpl({ data, renderItem, estimatedItemSize, + useDefaultStyles = true, onDragBegin: _, onDragEnd, onReordered, @@ -226,6 +231,11 @@ function DragListImpl({ scrollEnabled={reactiveActiveIndex === INACTIVE} // Fixes some issues caused by recycling. extraData={reactiveActiveIndex} + className={cn({ "-mb-2": useDefaultStyles }, props.className)} + contentContainerClassName={cn( + { "py-4": useDefaultStyles }, + props.contentContainerClassName, + )} /> ); diff --git a/mobile/src/navigation/screens/settings/sheets/TabOrderSheet.tsx b/mobile/src/navigation/screens/settings/sheets/TabOrderSheet.tsx index 345118811..ab4e8ed21 100644 --- a/mobile/src/navigation/screens/settings/sheets/TabOrderSheet.tsx +++ b/mobile/src/navigation/screens/settings/sheets/TabOrderSheet.tsx @@ -33,6 +33,7 @@ export function TabOrderSheet(props: { ref: TrueSheetRef }) { onDragBegin={() => setDraggable(false)} onDragEnd={() => setDraggable(true)} onReordered={Tabs.move} + useDefaultStyles={false} style={{ height: 440 }} contentContainerClassName="gap-2" /> From 3189db3065e7a884eb967480b3d7ebaeeaa7e16e Mon Sep 17 00:00:00 2001 From: cyanChill <83375816+cyanChill@users.noreply.github.com> Date: Fri, 29 May 2026 15:54:34 -0400 Subject: [PATCH 9/9] fix: Issues switching over to RNGH hook base APIs - We get have worklet warnings. - One of the annoying ones is "Tried to modify key `current` of an object which has been already passed to a worklet". - From this, we found our that our `Swipeable` component broke, which was due to the value we assigned to `ref.current` not being available in the current callback. --- mobile/src/components/DragList.tsx | 2 +- mobile/src/components/Swipeable.tsx | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mobile/src/components/DragList.tsx b/mobile/src/components/DragList.tsx index 8c2493d82..0d4843da6 100644 --- a/mobile/src/components/DragList.tsx +++ b/mobile/src/components/DragList.tsx @@ -125,8 +125,8 @@ function DragListImpl({ autoScrollAmount.set(0); pan.set(0); shifted.set(0); + enabled.set(true); }); - enabled.set(true); }, [ enabled, setReactiveActiveIndex, diff --git a/mobile/src/components/Swipeable.tsx b/mobile/src/components/Swipeable.tsx index d9ff7bac7..5bb48a84e 100644 --- a/mobile/src/components/Swipeable.tsx +++ b/mobile/src/components/Swipeable.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { LayoutChangeEvent } from "react-native"; import { Animated, View } from "react-native"; import { GestureDetector, usePanGesture } from "react-native-gesture-handler"; +import { useSharedValue } from "react-native-reanimated"; import { NothingArrowRight } from "~/resources/icons/NothingArrowRight"; @@ -59,7 +60,7 @@ export function Swipeable({ const [swipeAmount, setSwipeAmount] = useState(0); const dragX = useRef(new Animated.Value(0)).current; - const animationRef = useRef(null); + const animationRef = useSharedValue(null); useEffect(() => { const listener = dragX.addListener(({ value }) => setSwipeAmount(value)); @@ -82,7 +83,7 @@ export function Swipeable({ // Allows scrolling to work without triggering gesture. activeOffsetX: [-10, 10], enabled: !disabled, - onActivate: () => animationRef.current?.stop(), + onActivate: () => animationRef.get()?.stop(), onUpdate: ({ translationX }) => dragX.setValue(clampSwipeAmount(translationX)), onDeactivate: ({ translationX, velocityX }) => { @@ -104,7 +105,7 @@ export function Swipeable({ if (clampSwipeAmount(translationX) === 0) return; // Create animation the swiped item will translate to. - animationRef.current = overshootSwipe + const pendingAnimation = overshootSwipe ? Animated.spring(dragX, { toValue: metThreshold ? (swipedLeft ? -1 : 1) * rowWidth.current @@ -128,7 +129,11 @@ export function Swipeable({ else props.onSwipeRight!(); } - animationRef.current.start(async ({ finished }) => { + //! With RNGH's hook-based API, setting a ref value and accessing it + //! later in the same callback will not result in returning that set + //! value. + animationRef.set(pendingAnimation); + pendingAnimation.start(async ({ finished }) => { // Run callback after the animation finishes successfully and if // we met the threshold. if (!fireCallbackBeforeCompletion && finished && metThreshold) {