From 427f3a8bd7f21f14aef32af2f7ccf1f4b2731c29 Mon Sep 17 00:00:00 2001
From: Hailey <me@haileyok.com>
Date: Tue, 12 Nov 2024 11:18:53 -0800
Subject: Add email verification prompts throughout the app (#6174)

---
 src/screens/Messages/components/MessageInput.tsx | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

(limited to 'src/screens/Messages/components/MessageInput.tsx')

diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx
index 21d6e574e..8edad6272 100644
--- a/src/screens/Messages/components/MessageInput.tsx
+++ b/src/screens/Messages/components/MessageInput.tsx
@@ -18,6 +18,7 @@ import Graphemer from 'graphemer'
 
 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
 import {useHaptics} from '#/lib/haptics'
+import {useEmail} from '#/lib/hooks/useEmail'
 import {isIOS} from '#/platform/detection'
 import {
   useMessageDraft,
@@ -61,10 +62,15 @@ export function MessageInput({
   const [message, setMessage] = React.useState(getDraft)
   const inputRef = useAnimatedRef<TextInput>()
 
+  const {needsEmailVerification} = useEmail()
+
   useSaveMessageDraft(message)
   useExtractEmbedFromFacets(message, setEmbed)
 
   const onSubmit = React.useCallback(() => {
+    if (needsEmailVerification) {
+      return
+    }
     if (!hasEmbed && message.trim() === '') {
       return
     }
@@ -84,6 +90,7 @@ export function MessageInput({
       inputRef.current?.focus()
     }, 100)
   }, [
+    needsEmailVerification,
     hasEmbed,
     message,
     clearDraft,
@@ -159,6 +166,7 @@ export function MessageInput({
           ref={inputRef}
           hitSlop={HITSLOP_10}
           animatedProps={animatedProps}
+          editable={!needsEmailVerification}
         />
         <Pressable
           accessibilityRole="button"
@@ -171,7 +179,8 @@ export function MessageInput({
             a.justify_center,
             {height: 30, width: 30, backgroundColor: t.palette.primary_500},
           ]}
-          onPress={onSubmit}>
+          onPress={onSubmit}
+          disabled={needsEmailVerification}>
           <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
         </Pressable>
       </View>
-- 
cgit 1.4.1


From 474c4eff29b6a2454a20febf78a1edd5cf58663a Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Sun, 17 Nov 2024 15:06:28 +0000
Subject: Use compiler-safe Reanimated get/set APIs (#6391)

* Convert lightbox to get/set

* Work around software-mansion/react-native-reanimated#6613

* Use get/set in more places

* Port MainScrollProvider to get/set

* Port more to get/set

* Port composer to get/set

* Remove unnecessary thread hops in composer

* Port more things to get/set

* Convert more to get/set, remove redundant runOnJS

* Convert remaining cases to get/set
---
 src/Splash.tsx                                     | 72 ++++++++++---------
 src/components/Loader.tsx                          |  7 +-
 src/components/ProgressGuide/Toast.tsx             | 48 +++++++------
 src/components/anim/AnimatedCheck.tsx              | 20 +++---
 src/components/dms/ActionsWrapper.tsx              | 16 +++--
 src/components/dms/ChatEmptyPill.tsx               |  6 +-
 src/components/dms/NewMessagesPill.tsx             |  6 +-
 src/lib/custom-animations/GestureActionView.tsx    | 84 +++++++++++-----------
 src/lib/custom-animations/PressableScale.tsx       | 13 ++--
 src/lib/hooks/useMinimalShellTransform.ts          | 20 +++---
 src/screens/Messages/components/MessageInput.tsx   | 10 +--
 src/screens/Messages/components/MessagesList.tsx   | 36 +++++-----
 src/screens/Profile/Header/GrowableAvatar.tsx      |  2 +-
 src/screens/Profile/Header/GrowableBanner.tsx      | 15 ++--
 src/state/shell/minimal-mode.tsx                   | 16 +++--
 src/view/com/composer/Composer.tsx                 | 46 +++++++-----
 src/view/com/home/HomeHeaderLayout.web.tsx         |  2 +-
 src/view/com/home/HomeHeaderLayoutMobile.tsx       |  2 +-
 .../components/ImageItem/ImageItem.android.tsx     | 81 ++++++++++-----------
 .../components/ImageItem/ImageItem.ios.tsx         |  8 +--
 src/view/com/lightbox/ImageViewing/index.tsx       | 74 ++++++++++---------
 src/view/com/pager/PagerWithHeader.tsx             | 12 ++--
 src/view/com/util/BottomSheetCustomBackdrop.tsx    |  2 +-
 src/view/com/util/List.tsx                         |  4 +-
 src/view/com/util/MainScrollProvider.tsx           | 51 ++++++-------
 src/view/shell/bottom-bar/BottomBar.tsx            |  2 +-
 26 files changed, 351 insertions(+), 304 deletions(-)

(limited to 'src/screens/Messages/components/MessageInput.tsx')

diff --git a/src/Splash.tsx b/src/Splash.tsx
index 5a2b18445..a52b8837d 100644
--- a/src/Splash.tsx
+++ b/src/Splash.tsx
@@ -81,40 +81,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
     return {
       transform: [
         {
-          scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
+          scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
         },
         {
           scale: interpolate(
-            outroLogo.value,
+            outroLogo.get(),
             [0, 0.08, 1],
             [1, 0.8, 500],
             'clamp',
           ),
         },
       ],
-      opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
+      opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
     }
   })
   const bottomLogoAnimation = useAnimatedStyle(() => {
     return {
-      opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
+      opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
     }
   })
   const reducedLogoAnimation = useAnimatedStyle(() => {
     return {
       transform: [
         {
-          scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
+          scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
         },
       ],
-      opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
+      opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
     }
   })
 
   const logoWrapperAnimation = useAnimatedStyle(() => {
     return {
       opacity: interpolate(
-        outroAppOpacity.value,
+        outroAppOpacity.get(),
         [0, 0.1, 0.2, 1],
         [1, 1, 0, 0],
         'clamp',
@@ -126,11 +126,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
     return {
       transform: [
         {
-          scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
+          scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
         },
       ],
       opacity: interpolate(
-        outroAppOpacity.value,
+        outroAppOpacity.get(),
         [0, 0.1, 0.2, 1],
         [0, 0, 1, 1],
         'clamp',
@@ -146,29 +146,37 @@ export function Splash(props: React.PropsWithChildren<Props>) {
     if (isReady) {
       SplashScreen.hideAsync()
         .then(() => {
-          intro.value = withTiming(
-            1,
-            {duration: 400, easing: Easing.out(Easing.cubic)},
-            async () => {
-              // set these values to check animation at specific point
-              // outroLogo.value = 0.1
-              // outroApp.value = 0.1
-              outroLogo.value = withTiming(
-                1,
-                {duration: 1200, easing: Easing.in(Easing.cubic)},
-                () => {
-                  runOnJS(onFinish)()
-                },
-              )
-              outroApp.value = withTiming(1, {
-                duration: 1200,
-                easing: Easing.inOut(Easing.cubic),
-              })
-              outroAppOpacity.value = withTiming(1, {
-                duration: 1200,
-                easing: Easing.in(Easing.cubic),
-              })
-            },
+          intro.set(() =>
+            withTiming(
+              1,
+              {duration: 400, easing: Easing.out(Easing.cubic)},
+              async () => {
+                // set these values to check animation at specific point
+                // outroLogo.set(0.1)
+                // outroApp.set(0.1)
+                outroLogo.set(() =>
+                  withTiming(
+                    1,
+                    {duration: 1200, easing: Easing.in(Easing.cubic)},
+                    () => {
+                      runOnJS(onFinish)()
+                    },
+                  ),
+                )
+                outroApp.set(() =>
+                  withTiming(1, {
+                    duration: 1200,
+                    easing: Easing.inOut(Easing.cubic),
+                  }),
+                )
+                outroAppOpacity.set(() =>
+                  withTiming(1, {
+                    duration: 1200,
+                    easing: Easing.in(Easing.cubic),
+                  }),
+                )
+              },
+            ),
           )
         })
         .catch(() => {})
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
index e0b3be637..149554912 100644
--- a/src/components/Loader.tsx
+++ b/src/components/Loader.tsx
@@ -17,13 +17,12 @@ export function Loader(props: Props) {
   const rotation = useSharedValue(0)
 
   const animatedStyles = useAnimatedStyle(() => ({
-    transform: [{rotate: rotation.value + 'deg'}],
+    transform: [{rotate: rotation.get() + 'deg'}],
   }))
 
   React.useEffect(() => {
-    rotation.value = withRepeat(
-      withTiming(360, {duration: 500, easing: Easing.linear}),
-      -1,
+    rotation.set(() =>
+      withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
     )
   }, [rotation])
 
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
index 69e008260..b26c718f8 100644
--- a/src/components/ProgressGuide/Toast.tsx
+++ b/src/components/ProgressGuide/Toast.tsx
@@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef<
 
     // animate the opacity then set isOpen to false when done
     const setIsntOpen = () => setIsOpen(false)
-    opacity.value = withTiming(
-      0,
-      {
-        duration: 400,
-        easing: Easing.out(Easing.cubic),
-      },
-      () => runOnJS(setIsntOpen)(),
+    opacity.set(() =>
+      withTiming(
+        0,
+        {
+          duration: 400,
+          easing: Easing.out(Easing.cubic),
+        },
+        () => runOnJS(setIsntOpen)(),
+      ),
     )
   }, [setIsOpen, opacity])
 
@@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef<
 
     // animate the vertical translation, the opacity, and the checkmark
     const playCheckmark = () => animatedCheckRef.current?.play()
-    opacity.value = 0
-    opacity.value = withTiming(
-      1,
-      {
-        duration: 100,
+    opacity.set(0)
+    opacity.set(() =>
+      withTiming(
+        1,
+        {
+          duration: 100,
+          easing: Easing.out(Easing.cubic),
+        },
+        () => runOnJS(playCheckmark)(),
+      ),
+    )
+    translateY.set(0)
+    translateY.set(() =>
+      withTiming(insets.top + 10, {
+        duration: 500,
         easing: Easing.out(Easing.cubic),
-      },
-      () => runOnJS(playCheckmark)(),
+      }),
     )
-    translateY.value = 0
-    translateY.value = withTiming(insets.top + 10, {
-      duration: 500,
-      easing: Easing.out(Easing.cubic),
-    })
 
     // start the countdown timer to autoclose
     timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
@@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef<
   }, [winDim.width])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{translateY: translateY.value}],
-    opacity: opacity.value,
+    transform: [{translateY: translateY.get()}],
+    opacity: opacity.get(),
   }))
 
   return (
diff --git a/src/components/anim/AnimatedCheck.tsx b/src/components/anim/AnimatedCheck.tsx
index 7fdfc14cf..60407274e 100644
--- a/src/components/anim/AnimatedCheck.tsx
+++ b/src/components/anim/AnimatedCheck.tsx
@@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef<
   const checkAnim = useSharedValue(0)
 
   const circleAnimatedProps = useAnimatedProps(() => ({
-    strokeDashoffset: 166 - circleAnim.value * 166,
+    strokeDashoffset: 166 - circleAnim.get() * 166,
   }))
   const checkAnimatedProps = useAnimatedProps(() => ({
-    strokeDashoffset: 48 - 48 * checkAnim.value,
+    strokeDashoffset: 48 - 48 * checkAnim.get(),
   }))
 
   const play = React.useCallback(
     (cb?: () => void) => {
-      circleAnim.value = 0
-      checkAnim.value = 0
+      circleAnim.set(0)
+      checkAnim.set(0)
 
-      circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
-      checkAnim.value = withDelay(
-        500,
-        withTiming(1, {duration: 300, easing: Easing.linear}, cb),
+      circleAnim.set(() =>
+        withTiming(1, {duration: 500, easing: Easing.linear}),
+      )
+      checkAnim.set(() =>
+        withDelay(
+          500,
+          withTiming(1, {duration: 300, easing: Easing.linear}, cb),
+        ),
       )
     },
     [circleAnim, checkAnim],
diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx
index b77516e7b..a087fed3f 100644
--- a/src/components/dms/ActionsWrapper.tsx
+++ b/src/components/dms/ActionsWrapper.tsx
@@ -34,7 +34,7 @@ export function ActionsWrapper({
   const scale = useSharedValue(1)
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   const open = React.useCallback(() => {
@@ -46,7 +46,7 @@ export function ActionsWrapper({
   const shrink = React.useCallback(() => {
     'worklet'
     cancelAnimation(scale)
-    scale.value = withTiming(1, {duration: 200})
+    scale.set(() => withTiming(1, {duration: 200}))
   }, [scale])
 
   const doubleTapGesture = Gesture.Tap()
@@ -58,11 +58,13 @@ export function ActionsWrapper({
   const pressAndHoldGesture = Gesture.LongPress()
     .onStart(() => {
       'worklet'
-      scale.value = withTiming(1.05, {duration: 200}, finished => {
-        if (!finished) return
-        runOnJS(open)()
-        shrink()
-      })
+      scale.set(() =>
+        withTiming(1.05, {duration: 200}, finished => {
+          if (!finished) return
+          runOnJS(open)()
+          shrink()
+        }),
+      )
     })
     .onTouchesUp(shrink)
     .onTouchesMove(shrink)
diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx
index ffd022f56..042c3ad76 100644
--- a/src/components/dms/ChatEmptyPill.tsx
+++ b/src/components/dms/ChatEmptyPill.tsx
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
 
   const onPressIn = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1.075, {duration: 100})
+    scale.set(() => withTiming(1.075, {duration: 100}))
   }, [scale])
 
   const onPressOut = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1, {duration: 100})
+    scale.set(() => withTiming(1, {duration: 100}))
   }, [scale])
 
   const onPress = React.useCallback(() => {
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
   }, [playHaptic, prompts.length])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   return (
diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx
index 2f7ff8f4b..e3bc0c1f8 100644
--- a/src/components/dms/NewMessagesPill.tsx
+++ b/src/components/dms/NewMessagesPill.tsx
@@ -35,12 +35,12 @@ export function NewMessagesPill({
 
   const onPressIn = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1.075, {duration: 100})
+    scale.set(() => withTiming(1.075, {duration: 100}))
   }, [scale])
 
   const onPressOut = React.useCallback(() => {
     if (isWeb) return
-    scale.value = withTiming(1, {duration: 100})
+    scale.set(() => withTiming(1, {duration: 100}))
   }, [scale])
 
   const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
   }, [onPressInner, playHaptic])
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   return (
diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx
index 79e9db8a9..ba6952a81 100644
--- a/src/lib/custom-animations/GestureActionView.tsx
+++ b/src/lib/custom-animations/GestureActionView.tsx
@@ -61,7 +61,7 @@ export function GestureActionView({
   const clampedTransX = useDerivedValue(() => {
     const min = actions.leftFirst ? -MAX_WIDTH : 0
     const max = actions.rightFirst ? MAX_WIDTH : 0
-    return clamp(transX.value, min, max)
+    return clamp(transX.get(), min, max)
   })
 
   const iconScale = useSharedValue(1)
@@ -75,21 +75,23 @@ export function GestureActionView({
       return
     }
 
-    iconScale.value = withSequence(
-      withTiming(1.2, {duration: 175}),
-      withTiming(1, {duration: 100}),
+    iconScale.set(() =>
+      withSequence(
+        withTiming(1.2, {duration: 175}),
+        withTiming(1, {duration: 100}),
+      ),
     )
   }
 
   useAnimatedReaction(
     () => transX,
     () => {
-      if (transX.value === 0) {
+      if (transX.get() === 0) {
         runOnJS(setActiveAction)(null)
-      } else if (transX.value < 0) {
+      } else if (transX.get() < 0) {
         if (
           actions.leftSecond &&
-          transX.value <= -actions.leftSecond.threshold
+          transX.get() <= -actions.leftSecond.threshold
         ) {
           if (activeAction !== 'leftSecond') {
             runOnJS(setActiveAction)('leftSecond')
@@ -97,10 +99,10 @@ export function GestureActionView({
         } else if (activeAction !== 'leftFirst') {
           runOnJS(setActiveAction)('leftFirst')
         }
-      } else if (transX.value > 0) {
+      } else if (transX.get() > 0) {
         if (
           actions.rightSecond &&
-          transX.value > actions.rightSecond.threshold
+          transX.get() > actions.rightSecond.threshold
         ) {
           if (activeAction !== 'rightSecond') {
             runOnJS(setActiveAction)('rightSecond')
@@ -119,44 +121,44 @@ export function GestureActionView({
     .activeOffsetY([-200, 200])
     .onStart(() => {
       'worklet'
-      isActive.value = true
+      isActive.set(true)
     })
     .onChange(e => {
       'worklet'
-      transX.value = e.translationX
+      transX.set(e.translationX)
 
       if (e.translationX < 0) {
         // Left side
         if (actions.leftSecond) {
           if (
             e.translationX <= -actions.leftSecond.threshold &&
-            !hitSecond.value
+            !hitSecond.get()
           ) {
             runPopAnimation()
             runOnJS(haptic)()
-            hitSecond.value = true
+            hitSecond.set(true)
           } else if (
-            hitSecond.value &&
+            hitSecond.get() &&
             e.translationX > -actions.leftSecond.threshold
           ) {
             runPopAnimation()
-            hitSecond.value = false
+            hitSecond.set(false)
           }
         }
 
-        if (!hitSecond.value && actions.leftFirst) {
+        if (!hitSecond.get() && actions.leftFirst) {
           if (
             e.translationX <= -actions.leftFirst.threshold &&
-            !hitFirst.value
+            !hitFirst.get()
           ) {
             runPopAnimation()
             runOnJS(haptic)()
-            hitFirst.value = true
+            hitFirst.set(true)
           } else if (
-            hitFirst.value &&
+            hitFirst.get() &&
             e.translationX > -actions.leftFirst.threshold
           ) {
-            hitFirst.value = false
+            hitFirst.set(false)
           }
         }
       } else if (e.translationX > 0) {
@@ -164,33 +166,33 @@ export function GestureActionView({
         if (actions.rightSecond) {
           if (
             e.translationX >= actions.rightSecond.threshold &&
-            !hitSecond.value
+            !hitSecond.get()
           ) {
             runPopAnimation()
             runOnJS(haptic)()
-            hitSecond.value = true
+            hitSecond.set(true)
           } else if (
-            hitSecond.value &&
+            hitSecond.get() &&
             e.translationX < actions.rightSecond.threshold
           ) {
             runPopAnimation()
-            hitSecond.value = false
+            hitSecond.set(false)
           }
         }
 
-        if (!hitSecond.value && actions.rightFirst) {
+        if (!hitSecond.get() && actions.rightFirst) {
           if (
             e.translationX >= actions.rightFirst.threshold &&
-            !hitFirst.value
+            !hitFirst.get()
           ) {
             runPopAnimation()
             runOnJS(haptic)()
-            hitFirst.value = true
+            hitFirst.set(true)
           } else if (
-            hitFirst.value &&
+            hitFirst.get() &&
             e.translationX < actions.rightFirst.threshold
           ) {
-            hitFirst.value = false
+            hitFirst.set(false)
           }
         }
       }
@@ -198,29 +200,29 @@ export function GestureActionView({
     .onEnd(e => {
       'worklet'
       if (e.translationX < 0) {
-        if (hitSecond.value && actions.leftSecond) {
+        if (hitSecond.get() && actions.leftSecond) {
           runOnJS(actions.leftSecond.action)()
-        } else if (hitFirst.value && actions.leftFirst) {
+        } else if (hitFirst.get() && actions.leftFirst) {
           runOnJS(actions.leftFirst.action)()
         }
       } else if (e.translationX > 0) {
-        if (hitSecond.value && actions.rightSecond) {
+        if (hitSecond.get() && actions.rightSecond) {
           runOnJS(actions.rightSecond.action)()
-        } else if (hitSecond.value && actions.rightFirst) {
+        } else if (hitSecond.get() && actions.rightFirst) {
           runOnJS(actions.rightFirst.action)()
         }
       }
-      transX.value = withTiming(0, {duration: 200})
-      hitFirst.value = false
-      hitSecond.value = false
-      isActive.value = false
+      transX.set(() => withTiming(0, {duration: 200}))
+      hitFirst.set(false)
+      hitSecond.set(false)
+      isActive.set(false)
     })
 
   const composedGesture = Gesture.Simultaneous(panGesture)
 
   const animatedSliderStyle = useAnimatedStyle(() => {
     return {
-      transform: [{translateX: clampedTransX.value}],
+      transform: [{translateX: clampedTransX.get()}],
     }
   })
 
@@ -274,7 +276,7 @@ export function GestureActionView({
   const animatedBackgroundStyle = useAnimatedStyle(() => {
     return {
       backgroundColor: interpolateColor(
-        clampedTransX.value,
+        clampedTransX.get(),
         interpolation.inputRange,
         // @ts-expect-error - Weird type expected by reanimated, but this is okay
         interpolation.outputRange,
@@ -283,10 +285,10 @@ export function GestureActionView({
   })
 
   const animatedIconStyle = useAnimatedStyle(() => {
-    const absTransX = Math.abs(clampedTransX.value)
+    const absTransX = Math.abs(clampedTransX.get())
     return {
       opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
-      transform: [{scale: iconScale.value}],
+      transform: [{scale: iconScale.get()}],
     }
   })
 
diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx
index 4737b9ea3..1e776546d 100644
--- a/src/lib/custom-animations/PressableScale.tsx
+++ b/src/lib/custom-animations/PressableScale.tsx
@@ -2,7 +2,6 @@ import React from 'react'
 import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
 import Animated, {
   cancelAnimation,
-  runOnJS,
   useAnimatedStyle,
   useReducedMotion,
   useSharedValue,
@@ -32,27 +31,25 @@ export function PressableScale({
   const scale = useSharedValue(1)
 
   const animatedStyle = useAnimatedStyle(() => ({
-    transform: [{scale: scale.value}],
+    transform: [{scale: scale.get()}],
   }))
 
   return (
     <AnimatedPressable
       accessibilityRole="button"
       onPressIn={e => {
-        'worklet'
         if (onPressIn) {
-          runOnJS(onPressIn)(e)
+          onPressIn(e)
         }
         cancelAnimation(scale)
-        scale.value = withTiming(targetScale, {duration: 100})
+        scale.set(() => withTiming(targetScale, {duration: 100}))
       }}
       onPressOut={e => {
-        'worklet'
         if (onPressOut) {
-          runOnJS(onPressOut)(e)
+          onPressOut(e)
         }
         cancelAnimation(scale)
-        scale.value = withTiming(1, {duration: 100})
+        scale.set(() => withTiming(1, {duration: 100}))
       }}
       style={[!reducedMotion && animatedStyle, style]}
       {...rest}>
diff --git a/src/lib/hooks/useMinimalShellTransform.ts b/src/lib/hooks/useMinimalShellTransform.ts
index 678776755..6f16fa0f9 100644
--- a/src/lib/hooks/useMinimalShellTransform.ts
+++ b/src/lib/hooks/useMinimalShellTransform.ts
@@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() {
   const {headerHeight} = useShellLayout()
 
   const headerTransform = useAnimatedStyle(() => {
+    const headerModeValue = headerMode.get()
     return {
-      pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
-      opacity: Math.pow(1 - headerMode.value, 2),
+      pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
+      opacity: Math.pow(1 - headerModeValue, 2),
       transform: [
         {
           translateY: interpolate(
-            headerMode.value,
+            headerModeValue,
             [0, 1],
-            [0, -headerHeight.value],
+            [0, -headerHeight.get()],
           ),
         },
       ],
@@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() {
   const {footerHeight} = useShellLayout()
 
   const footerTransform = useAnimatedStyle(() => {
+    const footerModeValue = footerMode.get()
     return {
-      pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
-      opacity: Math.pow(1 - footerMode.value, 2),
+      pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
+      opacity: Math.pow(1 - footerModeValue, 2),
       transform: [
         {
           translateY: interpolate(
-            footerMode.value,
+            footerModeValue,
             [0, 1],
-            [0, footerHeight.value],
+            [0, footerHeight.get()],
           ),
         },
       ],
@@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() {
     return {
       transform: [
         {
-          translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
+          translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
         },
       ],
     }
diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx
index 8edad6272..85509211b 100644
--- a/src/screens/Messages/components/MessageInput.tsx
+++ b/src/screens/Messages/components/MessageInput.tsx
@@ -108,22 +108,22 @@ export function MessageInput({
         const measurement = measure(inputRef)
         if (!measurement) return
 
-        const max = windowHeight - -keyboardHeight.value - topInset - 150
+        const max = windowHeight - -keyboardHeight.get() - topInset - 150
         const availableSpace = max - measurement.height
 
-        maxHeight.value = max
-        isInputScrollable.value = availableSpace < 30
+        maxHeight.set(max)
+        isInputScrollable.set(availableSpace < 30)
       },
     },
     [windowHeight, topInset],
   )
 
   const animatedStyle = useAnimatedStyle(() => ({
-    maxHeight: maxHeight.value,
+    maxHeight: maxHeight.get(),
   }))
 
   const animatedProps = useAnimatedProps(() => ({
-    scrollEnabled: isInputScrollable.value,
+    scrollEnabled: isInputScrollable.get(),
   }))
 
   return (
diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx
index 9db4f07b6..9f67929a3 100644
--- a/src/screens/Messages/components/MessagesList.tsx
+++ b/src/screens/Messages/components/MessagesList.tsx
@@ -145,7 +145,7 @@ export function MessagesList({
     (_: number, height: number) => {
       // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
       // previous off whenever we add new content to the previous offset whenever we add new content to the list.
-      if (isWeb && isAtTop.value && hasScrolled) {
+      if (isWeb && isAtTop.get() && hasScrolled) {
         flatListRef.current?.scrollToOffset({
           offset: height - prevContentHeight.current,
           animated: false,
@@ -153,7 +153,7 @@ export function MessagesList({
       }
 
       // This number _must_ be the height of the MaybeLoader component
-      if (height > 50 && isAtBottom.value) {
+      if (height > 50 && isAtBottom.get()) {
         // If the size of the content is changing by more than the height of the screen, then we don't
         // want to scroll further than the start of all the new content. Since we are storing the previous offset,
         // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill
@@ -161,7 +161,7 @@ export function MessagesList({
         if (
           didBackground.current &&
           hasScrolled &&
-          height - prevContentHeight.current > layoutHeight.value - 50 &&
+          height - prevContentHeight.current > layoutHeight.get() - 50 &&
           convoState.items.length - prevItemCount.current > 1
         ) {
           flatListRef.current?.scrollToOffset({
@@ -209,7 +209,7 @@ export function MessagesList({
   )
 
   const onStartReached = useCallback(() => {
-    if (hasScrolled && prevContentHeight.current > layoutHeight.value) {
+    if (hasScrolled && prevContentHeight.current > layoutHeight.get()) {
       convoState.fetchMessageHistory()
     }
   }, [convoState, hasScrolled, layoutHeight])
@@ -217,18 +217,18 @@ export function MessagesList({
   const onScroll = React.useCallback(
     (e: ReanimatedScrollEvent) => {
       'worklet'
-      layoutHeight.value = e.layoutMeasurement.height
+      layoutHeight.set(e.layoutMeasurement.height)
       const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
 
       // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
       // when a new message is added, hence the 100 pixel offset
-      isAtBottom.value = e.contentSize.height - 100 < bottomOffset
-      isAtTop.value = e.contentOffset.y <= 1
+      isAtBottom.set(e.contentSize.height - 100 < bottomOffset)
+      isAtTop.set(e.contentOffset.y <= 1)
 
       if (
         newMessagesPill.show &&
         (e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
-          isAtBottom.value)
+          isAtBottom.get())
       ) {
         runOnJS(setNewMessagesPill)({
           show: false,
@@ -256,28 +256,28 @@ export function MessagesList({
       // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
       // just update the height here instead of having the `onMove` event do it (that event will not fire!)
       if (e.duration === 0) {
-        layoutScrollWithoutAnimation.value = true
-        keyboardHeight.value = e.height
+        layoutScrollWithoutAnimation.set(true)
+        keyboardHeight.set(e.height)
       } else {
-        keyboardIsOpening.value = true
+        keyboardIsOpening.set(true)
       }
     },
     onMove: e => {
       'worklet'
-      keyboardHeight.value = e.height
+      keyboardHeight.set(e.height)
       if (e.height > bottomOffset) {
         scrollTo(flatListRef, 0, 1e7, false)
       }
     },
     onEnd: () => {
       'worklet'
-      keyboardIsOpening.value = false
+      keyboardIsOpening.set(false)
     },
   })
 
   const animatedListStyle = useAnimatedStyle(() => ({
     marginBottom:
-      keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset,
+      keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset,
   }))
 
   // -- Message sending
@@ -363,13 +363,13 @@ export function MessagesList({
   // -- List layout changes (opening emoji keyboard, etc.)
   const onListLayout = React.useCallback(
     (e: LayoutChangeEvent) => {
-      layoutHeight.value = e.nativeEvent.layout.height
+      layoutHeight.set(e.nativeEvent.layout.height)
 
-      if (isWeb || !keyboardIsOpening.value) {
+      if (isWeb || !keyboardIsOpening.get()) {
         flatListRef.current?.scrollToEnd({
-          animated: !layoutScrollWithoutAnimation.value,
+          animated: !layoutScrollWithoutAnimation.get(),
         })
-        layoutScrollWithoutAnimation.value = false
+        layoutScrollWithoutAnimation.set(false)
       }
     },
     [
diff --git a/src/screens/Profile/Header/GrowableAvatar.tsx b/src/screens/Profile/Header/GrowableAvatar.tsx
index 20ac14892..dab69f955 100644
--- a/src/screens/Profile/Header/GrowableAvatar.tsx
+++ b/src/screens/Profile/Header/GrowableAvatar.tsx
@@ -45,7 +45,7 @@ function GrowableAvatarInner({
   const animatedStyle = useAnimatedStyle(() => ({
     transform: [
       {
-        scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], {
+        scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], {
           extrapolateRight: Extrapolation.CLAMP,
         }),
       },
diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx
index 144b7cd2d..7f5a3cd6e 100644
--- a/src/screens/Profile/Header/GrowableBanner.tsx
+++ b/src/screens/Profile/Header/GrowableBanner.tsx
@@ -66,7 +66,7 @@ function GrowableBannerInner({
   const animatedStyle = useAnimatedStyle(() => ({
     transform: [
       {
-        scale: interpolate(scrollY.value, [-150, 0], [2, 1], {
+        scale: interpolate(scrollY.get(), [-150, 0], [2, 1], {
           extrapolateRight: Extrapolation.CLAMP,
         }),
       },
@@ -76,7 +76,7 @@ function GrowableBannerInner({
   const animatedBlurViewProps = useAnimatedProps(() => {
     return {
       intensity: interpolate(
-        scrollY.value,
+        scrollY.get(),
         [-300, -65, -15],
         [50, 40, 0],
         Extrapolation.CLAMP,
@@ -85,16 +85,17 @@ function GrowableBannerInner({
   })
 
   const animatedSpinnerStyle = useAnimatedStyle(() => {
+    const scrollYValue = scrollY.get()
     return {
-      display: scrollY.value < 0 ? 'flex' : 'none',
+      display: scrollYValue < 0 ? 'flex' : 'none',
       opacity: interpolate(
-        scrollY.value,
+        scrollYValue,
         [-60, -15],
         [1, 0],
         Extrapolation.CLAMP,
       ),
       transform: [
-        {translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])},
+        {translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])},
         {rotate: '90deg'},
       ],
     }
@@ -103,7 +104,7 @@ function GrowableBannerInner({
   const animatedBackButtonStyle = useAnimatedStyle(() => ({
     transform: [
       {
-        translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
+        translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
           extrapolateRight: Extrapolation.CLAMP,
         }),
       },
@@ -168,7 +169,7 @@ function useShouldAnimateSpinner({
   const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
 
   useAnimatedReaction(
-    () => scrollY.value < -5,
+    () => scrollY.get() < -5,
     (value, prevValue) => {
       if (value !== prevValue) {
         runOnJS(setIsOverscrolled)(value)
diff --git a/src/state/shell/minimal-mode.tsx b/src/state/shell/minimal-mode.tsx
index 3f1cebdf0..00547ee3e 100644
--- a/src/state/shell/minimal-mode.tsx
+++ b/src/state/shell/minimal-mode.tsx
@@ -44,13 +44,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       'worklet'
       // Cancel any existing animation
       cancelAnimation(headerMode)
-      headerMode.value = withSpring(v ? 1 : 0, {
-        overshootClamping: true,
-      })
+      headerMode.set(() =>
+        withSpring(v ? 1 : 0, {
+          overshootClamping: true,
+        }),
+      )
       cancelAnimation(footerMode)
-      footerMode.value = withSpring(v ? 1 : 0, {
-        overshootClamping: true,
-      })
+      footerMode.set(() =>
+        withSpring(v ? 1 : 0, {
+          overshootClamping: true,
+        }),
+      )
     },
     [headerMode, footerMode],
   )
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 0a94827d5..5d9f60766 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -1267,12 +1267,12 @@ function useScrollTracker({
   const contentHeight = useSharedValue(0)
 
   const hasScrolledToTop = useDerivedValue(() =>
-    withTiming(contentOffset.value === 0 ? 1 : 0),
+    withTiming(contentOffset.get() === 0 ? 1 : 0),
   )
 
   const hasScrolledToBottom = useDerivedValue(() =>
     withTiming(
-      contentHeight.value - contentOffset.value - 5 <= scrollViewHeight.value
+      contentHeight.get() - contentOffset.get() - 5 <= scrollViewHeight.get()
         ? 1
         : 0,
     ),
@@ -1290,11 +1290,11 @@ function useScrollTracker({
     }) => {
       'worklet'
       if (typeof newContentHeight === 'number')
-        contentHeight.value = Math.floor(newContentHeight)
+        contentHeight.set(Math.floor(newContentHeight))
       if (typeof newContentOffset === 'number')
-        contentOffset.value = Math.floor(newContentOffset)
+        contentOffset.set(Math.floor(newContentOffset))
       if (typeof newScrollViewHeight === 'number')
-        scrollViewHeight.value = Math.floor(newScrollViewHeight)
+        scrollViewHeight.set(Math.floor(newScrollViewHeight))
     },
     [contentHeight, contentOffset, scrollViewHeight],
   )
@@ -1310,21 +1310,22 @@ function useScrollTracker({
     },
   })
 
-  const onScrollViewContentSizeChange = useCallback(
-    (_width: number, height: number) => {
-      if (stickyBottom && height > contentHeight.value) {
+  const onScrollViewContentSizeChangeUIThread = useCallback(
+    (newContentHeight: number) => {
+      'worklet'
+      const oldContentHeight = contentHeight.get()
+      let shouldScrollToBottom = false
+      if (stickyBottom && newContentHeight > oldContentHeight) {
         const isFairlyCloseToBottom =
-          contentHeight.value - contentOffset.value - 100 <=
-          scrollViewHeight.value
+          oldContentHeight - contentOffset.get() - 100 <= scrollViewHeight.get()
         if (isFairlyCloseToBottom) {
-          runOnUI(() => {
-            scrollTo(scrollViewRef, 0, contentHeight.value, true)
-          })()
+          shouldScrollToBottom = true
         }
       }
-      showHideBottomBorder({
-        newContentHeight: height,
-      })
+      showHideBottomBorder({newContentHeight})
+      if (shouldScrollToBottom) {
+        scrollTo(scrollViewRef, 0, newContentHeight, true)
+      }
     },
     [
       showHideBottomBorder,
@@ -1336,6 +1337,13 @@ function useScrollTracker({
     ],
   )
 
+  const onScrollViewContentSizeChange = useCallback(
+    (_width: number, height: number) => {
+      runOnUI(onScrollViewContentSizeChangeUIThread)(height)
+    },
+    [onScrollViewContentSizeChangeUIThread],
+  )
+
   const onScrollViewLayout = useCallback(
     (evt: LayoutChangeEvent) => {
       showHideBottomBorder({
@@ -1349,7 +1357,7 @@ function useScrollTracker({
     return {
       borderBottomWidth: StyleSheet.hairlineWidth,
       borderColor: interpolateColor(
-        hasScrolledToTop.value,
+        hasScrolledToTop.get(),
         [0, 1],
         [t.atoms.border_contrast_medium.borderColor, 'transparent'],
       ),
@@ -1359,7 +1367,7 @@ function useScrollTracker({
     return {
       borderTopWidth: StyleSheet.hairlineWidth,
       borderColor: interpolateColor(
-        hasScrolledToBottom.value,
+        hasScrolledToBottom.get(),
         [0, 1],
         [t.atoms.border_contrast_medium.borderColor, 'transparent'],
       ),
@@ -1604,7 +1612,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
 
   const animatedStyle = useAnimatedStyle(() => {
     return {
-      transform: [{rotateZ: `${rotate.value}deg`}],
+      transform: [{rotateZ: `${rotate.get()}deg`}],
     }
   })
 
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 7049306eb..bdfc2c7ff 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -93,7 +93,7 @@ function HomeHeaderLayoutDesktopAndTablet({
       {tabBarAnchor}
       <Animated.View
         onLayout={e => {
-          headerHeight.value = e.nativeEvent.layout.height
+          headerHeight.set(e.nativeEvent.layout.height)
         }}
         style={[
           t.atoms.bg,
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index f5397d717..98253ad74 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -43,7 +43,7 @@ export function HomeHeaderLayoutMobile({
     <Animated.View
       style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
       onLayout={e => {
-        headerHeight.value = e.nativeEvent.layout.height
+        headerHeight.set(e.nativeEvent.layout.height)
       }}>
       <View style={[pal.view, styles.topBar]}>
         <View style={[pal.view, {width: 100}]}>
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
index 260787d2f..7aca8721b 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -87,11 +87,11 @@ const ImageItem = ({
   // Note: DO NOT move any logic reading animated values outside this function.
   useAnimatedReaction(
     () => {
-      if (pinchScale.value !== 1) {
+      if (pinchScale.get() !== 1) {
         // We're currently pinching.
         return true
       }
-      const [, , committedScale] = readTransform(committedTransform.value)
+      const [, , committedScale] = readTransform(committedTransform.get())
       if (committedScale !== 1) {
         // We started from a pinched in state.
         return true
@@ -147,10 +147,10 @@ const ImageItem = ({
     .onStart(e => {
       'worklet'
       const screenSize = measureSafeArea()
-      pinchOrigin.value = {
+      pinchOrigin.set({
         x: e.focalX - screenSize.width / 2,
         y: e.focalY - screenSize.height / 2,
-      }
+      })
     })
     .onChange(e => {
       'worklet'
@@ -160,7 +160,7 @@ const ImageItem = ({
       }
       // Don't let the picture zoom in so close that it gets blurry.
       // Also, like in stock Android apps, don't let the user zoom out further than 1:1.
-      const [, , committedScale] = readTransform(committedTransform.value)
+      const [, , committedScale] = readTransform(committedTransform.get())
       const maxCommittedScale = Math.max(
         MIN_SCREEN_ZOOM,
         (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM,
@@ -171,20 +171,21 @@ const ImageItem = ({
         Math.max(minPinchScale, e.scale),
         maxPinchScale,
       )
-      pinchScale.value = nextPinchScale
+      pinchScale.set(nextPinchScale)
 
       // Zooming out close to the corner could push us out of bounds, which we don't want on Android.
       // Calculate where we'll end up so we know how much to translate back to stay in bounds.
       const t = createTransform()
-      prependPan(t, panTranslation.value)
-      prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value)
-      prependTransform(t, committedTransform.value)
+      prependPan(t, panTranslation.get())
+      prependPinch(t, nextPinchScale, pinchOrigin.get(), pinchTranslation.get())
+      prependTransform(t, committedTransform.get())
       const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
       if (dx !== 0 || dy !== 0) {
-        pinchTranslation.value = {
-          x: pinchTranslation.value.x + dx,
-          y: pinchTranslation.value.y + dy,
-        }
+        const pt = pinchTranslation.get()
+        pinchTranslation.set({
+          x: pt.x + dx,
+          y: pt.y + dy,
+        })
       }
     })
     .onEnd(() => {
@@ -193,18 +194,18 @@ const ImageItem = ({
       let t = createTransform()
       prependPinch(
         t,
-        pinchScale.value,
-        pinchOrigin.value,
-        pinchTranslation.value,
+        pinchScale.get(),
+        pinchOrigin.get(),
+        pinchTranslation.get(),
       )
-      prependTransform(t, committedTransform.value)
+      prependTransform(t, committedTransform.get())
       applyRounding(t)
-      committedTransform.value = t
+      committedTransform.set(t)
 
       // Reset just the pinch.
-      pinchScale.value = 1
-      pinchOrigin.value = {x: 0, y: 0}
-      pinchTranslation.value = {x: 0, y: 0}
+      pinchScale.set(1)
+      pinchOrigin.set({x: 0, y: 0})
+      pinchTranslation.set({x: 0, y: 0})
     })
 
   const pan = Gesture.Pan()
@@ -223,29 +224,29 @@ const ImageItem = ({
       prependPan(t, nextPanTranslation)
       prependPinch(
         t,
-        pinchScale.value,
-        pinchOrigin.value,
-        pinchTranslation.value,
+        pinchScale.get(),
+        pinchOrigin.get(),
+        pinchTranslation.get(),
       )
-      prependTransform(t, committedTransform.value)
+      prependTransform(t, committedTransform.get())
 
       // Prevent panning from going out of bounds.
       const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
       nextPanTranslation.x += dx
       nextPanTranslation.y += dy
-      panTranslation.value = nextPanTranslation
+      panTranslation.set(nextPanTranslation)
     })
     .onEnd(() => {
       'worklet'
       // Commit just the pan.
       let t = createTransform()
-      prependPan(t, panTranslation.value)
-      prependTransform(t, committedTransform.value)
+      prependPan(t, panTranslation.get())
+      prependTransform(t, committedTransform.get())
       applyRounding(t)
-      committedTransform.value = t
+      committedTransform.set(t)
 
       // Reset just the pan.
-      panTranslation.value = {x: 0, y: 0}
+      panTranslation.set({x: 0, y: 0})
     })
 
   const singleTap = Gesture.Tap().onEnd(() => {
@@ -261,11 +262,11 @@ const ImageItem = ({
       if (!imageDimensions || !imageAspect) {
         return
       }
-      const [, , committedScale] = readTransform(committedTransform.value)
+      const [, , committedScale] = readTransform(committedTransform.get())
       if (committedScale !== 1) {
         // Go back to 1:1 using the identity vector.
         let t = createTransform()
-        committedTransform.value = withClampedSpring(t)
+        committedTransform.set(withClampedSpring(t))
         return
       }
 
@@ -299,7 +300,7 @@ const ImageItem = ({
       )
       const finalTransform = createTransform()
       prependPinch(finalTransform, scale, origin, {x: dx, y: dy})
-      committedTransform.value = withClampedSpring(finalTransform)
+      committedTransform.set(withClampedSpring(finalTransform))
     })
 
   const composedGesture = isScrollViewBeingDragged
@@ -313,13 +314,13 @@ const ImageItem = ({
       )
 
   const containerStyle = useAnimatedStyle(() => {
-    const {scaleAndMoveTransform, isHidden} = transforms.value
+    const {scaleAndMoveTransform, isHidden} = transforms.get()
     // Apply the active adjustments on top of the committed transform before the gestures.
     // This is matrix multiplication, so operations are applied in the reverse order.
     let t = createTransform()
-    prependPan(t, panTranslation.value)
-    prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
-    prependTransform(t, committedTransform.value)
+    prependPan(t, panTranslation.get())
+    prependPinch(t, pinchScale.get(), pinchOrigin.get(), pinchTranslation.get())
+    prependTransform(t, committedTransform.get())
     const [translateX, translateY, scale] = readTransform(t)
     const manipulationTransform = [
       {translateX},
@@ -338,7 +339,7 @@ const ImageItem = ({
   })
 
   const imageCropStyle = useAnimatedStyle(() => {
-    const {cropFrameTransform} = transforms.value
+    const {cropFrameTransform} = transforms.get()
     return {
       flex: 1,
       overflow: 'hidden',
@@ -347,7 +348,7 @@ const ImageItem = ({
   })
 
   const imageStyle = useAnimatedStyle(() => {
-    const {cropContentTransform} = transforms.value
+    const {cropContentTransform} = transforms.get()
     return {
       flex: 1,
       transform: cropContentTransform,
@@ -359,7 +360,7 @@ const ImageItem = ({
   const [hasLoaded, setHasLoaded] = useState(false)
   useAnimatedReaction(
     () => {
-      return transforms.value.isResting && !hasLoaded
+      return transforms.get().isResting && !hasLoaded
     },
     (show, prevShow) => {
       if (show && !prevShow) {
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index f06a59ed6..c7be4f3e3 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -148,7 +148,7 @@ const ImageItem = ({
   )
 
   const containerStyle = useAnimatedStyle(() => {
-    const {scaleAndMoveTransform, isHidden} = transforms.value
+    const {scaleAndMoveTransform, isHidden} = transforms.get()
     return {
       flex: 1,
       transform: scaleAndMoveTransform,
@@ -158,7 +158,7 @@ const ImageItem = ({
 
   const imageCropStyle = useAnimatedStyle(() => {
     const screenSize = measureSafeArea()
-    const {cropFrameTransform} = transforms.value
+    const {cropFrameTransform} = transforms.get()
     return {
       overflow: 'hidden',
       transform: cropFrameTransform,
@@ -171,7 +171,7 @@ const ImageItem = ({
   })
 
   const imageStyle = useAnimatedStyle(() => {
-    const {cropContentTransform} = transforms.value
+    const {cropContentTransform} = transforms.get()
     return {
       transform: cropContentTransform,
       width: '100%',
@@ -184,7 +184,7 @@ const ImageItem = ({
   const [hasLoaded, setHasLoaded] = useState(false)
   useAnimatedReaction(
     () => {
-      return transforms.value.isResting && !hasLoaded
+      return transforms.get().isResting && !hasLoaded
     },
     (show, prevShow) => {
       if (show && !prevShow) {
diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx
index 68857f62d..4ba056eb0 100644
--- a/src/view/com/lightbox/ImageViewing/index.tsx
+++ b/src/view/com/lightbox/ImageViewing/index.tsx
@@ -109,18 +109,22 @@ export default function ImageViewRoot({
 
     // https://github.com/software-mansion/react-native-reanimated/issues/6677
     requestAnimationFrame(() => {
-      openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1
+      openProgress.set(() =>
+        canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1,
+      )
     })
     return () => {
       // https://github.com/software-mansion/react-native-reanimated/issues/6677
       requestAnimationFrame(() => {
-        openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0
+        openProgress.set(() =>
+          canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0,
+        )
       })
     }
   }, [nextLightbox, openProgress])
 
   useAnimatedReaction(
-    () => openProgress.value === 0,
+    () => openProgress.get() === 0,
     (isGone, wasGone) => {
       if (isGone && !wasGone) {
         runOnJS(setActiveLightbox)(null)
@@ -130,7 +134,7 @@ export default function ImageViewRoot({
 
   const onFlyAway = React.useCallback(() => {
     'worklet'
-    openProgress.value = 0
+    openProgress.set(0)
     runOnJS(onRequestClose)()
   }, [onRequestClose, openProgress])
 
@@ -187,7 +191,7 @@ function ImageView({
   const isFlyingAway = useSharedValue(false)
 
   const containerStyle = useAnimatedStyle(() => {
-    if (openProgress.value < 1 || isFlyingAway.value) {
+    if (openProgress.get() < 1 || isFlyingAway.get()) {
       return {pointerEvents: 'none'}
     }
     return {pointerEvents: 'auto'}
@@ -196,11 +200,12 @@ function ImageView({
   const backdropStyle = useAnimatedStyle(() => {
     const screenSize = measure(safeAreaRef)
     let opacity = 1
-    if (openProgress.value < 1) {
-      opacity = Math.sqrt(openProgress.value)
+    const openProgressValue = openProgress.get()
+    if (openProgressValue < 1) {
+      opacity = Math.sqrt(openProgressValue)
     } else if (screenSize) {
       const dragProgress = Math.min(
-        Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2),
+        Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2),
         1,
       )
       opacity -= dragProgress
@@ -212,11 +217,11 @@ function ImageView({
   })
 
   const animatedHeaderStyle = useAnimatedStyle(() => {
-    const show = showControls && dismissSwipeTranslateY.value === 0
+    const show = showControls && dismissSwipeTranslateY.get() === 0
     return {
       pointerEvents: show ? 'box-none' : 'none',
       opacity: withClampedSpring(
-        show && openProgress.value === 1 ? 1 : 0,
+        show && openProgress.get() === 1 ? 1 : 0,
         FAST_SPRING,
       ),
       transform: [
@@ -227,12 +232,12 @@ function ImageView({
     }
   })
   const animatedFooterStyle = useAnimatedStyle(() => {
-    const show = showControls && dismissSwipeTranslateY.value === 0
+    const show = showControls && dismissSwipeTranslateY.get() === 0
     return {
       flexGrow: 1,
       pointerEvents: show ? 'box-none' : 'none',
       opacity: withClampedSpring(
-        show && openProgress.value === 1 ? 1 : 0,
+        show && openProgress.get() === 1 ? 1 : 0,
         FAST_SPRING,
       ),
       transform: [
@@ -259,7 +264,7 @@ function ImageView({
       const screenSize = measure(safeAreaRef)
       return (
         !screenSize ||
-        Math.abs(dismissSwipeTranslateY.value) > screenSize.height
+        Math.abs(dismissSwipeTranslateY.get()) > screenSize.height
       )
     },
     (isOut, wasOut) => {
@@ -397,10 +402,11 @@ function LightboxImage({
   const transforms = useDerivedValue(() => {
     'worklet'
     const safeArea = measureSafeArea()
+    const openProgressValue = openProgress.get()
     const dismissTranslateY =
-      isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0
+      isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
 
-    if (openProgress.value === 0 && isFlyingAway.value) {
+    if (openProgressValue === 0 && isFlyingAway.get()) {
       return {
         isHidden: true,
         isResting: false,
@@ -410,9 +416,9 @@ function LightboxImage({
       }
     }
 
-    if (isActive && thumbRect && imageAspect && openProgress.value < 1) {
+    if (isActive && thumbRect && imageAspect && openProgressValue < 1) {
       return interpolateTransform(
-        openProgress.value,
+        openProgressValue,
         thumbRect,
         safeArea,
         imageAspect,
@@ -434,33 +440,37 @@ function LightboxImage({
     .maxPointers(1)
     .onUpdate(e => {
       'worklet'
-      if (openProgress.value !== 1 || isFlyingAway.value) {
+      if (openProgress.get() !== 1 || isFlyingAway.get()) {
         return
       }
-      dismissSwipeTranslateY.value = e.translationY
+      dismissSwipeTranslateY.set(e.translationY)
     })
     .onEnd(e => {
       'worklet'
-      if (openProgress.value !== 1 || isFlyingAway.value) {
+      if (openProgress.get() !== 1 || isFlyingAway.get()) {
         return
       }
       if (Math.abs(e.velocityY) > 200) {
-        isFlyingAway.value = true
-        if (dismissSwipeTranslateY.value === 0) {
+        isFlyingAway.set(true)
+        if (dismissSwipeTranslateY.get() === 0) {
           // HACK: If the initial value is 0, withDecay() animation doesn't start.
           // This is a bug in Reanimated, but for now we'll work around it like this.
-          dismissSwipeTranslateY.value = 1
+          dismissSwipeTranslateY.set(1)
         }
-        dismissSwipeTranslateY.value = withDecay({
-          velocity: e.velocityY,
-          velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
-          deceleration: 1, // Danger! This relies on the reaction below stopping it.
-        })
+        dismissSwipeTranslateY.set(() =>
+          withDecay({
+            velocity: e.velocityY,
+            velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
+            deceleration: 1, // Danger! This relies on the reaction below stopping it.
+          }),
+        )
       } else {
-        dismissSwipeTranslateY.value = withSpring(0, {
-          stiffness: 700,
-          damping: 50,
-        })
+        dismissSwipeTranslateY.set(() =>
+          withSpring(0, {
+            stiffness: 700,
+            damping: 50,
+          }),
+        )
       }
     })
 
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 6d601c289..92b98dc2e 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -131,11 +131,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     const lastForcedScrollY = useSharedValue(0)
     const adjustScrollForOtherPages = () => {
       'worklet'
-      const currentScrollY = scrollY.value
+      const currentScrollY = scrollY.get()
       const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
-      if (lastForcedScrollY.value !== forcedScrollY) {
-        lastForcedScrollY.value = forcedScrollY
-        const refs = scrollRefs.value
+      if (lastForcedScrollY.get() !== forcedScrollY) {
+        lastForcedScrollY.set(forcedScrollY)
+        const refs = scrollRefs.get()
         for (let i = 0; i < refs.length; i++) {
           const scollRef = refs[i]
           if (i !== currentPage && scollRef != null) {
@@ -167,7 +167,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
         const isPossiblyInvalid =
           headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
         if (!isPossiblyInvalid) {
-          scrollY.value = nextScrollY
+          scrollY.set(nextScrollY)
           runOnJS(queueThrottledOnScroll)()
         }
       },
@@ -246,7 +246,7 @@ let PagerTabBar = ({
   allowHeaderOverScroll?: boolean
 }): React.ReactNode => {
   const headerTransform = useAnimatedStyle(() => {
-    const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1
+    const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1
     return {
       transform: [
         {
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index 25e882e87..86751861f 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -18,7 +18,7 @@ export function createCustomBackdrop(
     // animated variables
     const opacity = useAnimatedStyle(() => ({
       opacity: interpolate(
-        animatedIndex.value, // current snap index
+        animatedIndex.get(), // current snap index
         [-1, 0], // input range
         [0, 0.5], // output range
         Extrapolation.CLAMP,
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 52314f954..4ee4d7d0b 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -79,8 +79,8 @@ function ListImpl<ItemT>(
       onScrollFromContext?.(e, ctx)
 
       const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
-      if (isScrolledDown.value !== didScrollDown) {
-        isScrolledDown.value = didScrollDown
+      if (isScrolledDown.get() !== didScrollDown) {
+        isScrolledDown.set(didScrollDown)
         if (onScrolledDownChange != null) {
           runOnJS(handleScrolledDownChange)(didScrollDown)
         }
diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx
index 193d07d72..0d084993b 100644
--- a/src/view/com/util/MainScrollProvider.tsx
+++ b/src/view/com/util/MainScrollProvider.tsx
@@ -44,7 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
     (v: boolean) => {
       'worklet'
       cancelAnimation(headerMode)
-      headerMode.value = v ? V1.value : V0.value
+      headerMode.set(v ? V1.get() : V0.get())
     },
     [headerMode],
   )
@@ -52,9 +52,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
   useEffect(() => {
     if (isWeb) {
       return listenToForcedWindowScroll(() => {
-        startDragOffset.value = null
-        startMode.value = null
-        didJustRestoreScroll.value = true
+        startDragOffset.set(null)
+        startMode.set(null)
+        didJustRestoreScroll.set(true)
       })
     }
   })
@@ -63,13 +63,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
     (e: NativeScrollEvent) => {
       'worklet'
       if (isNative) {
-        if (startDragOffset.value === null) {
+        const startDragOffsetValue = startDragOffset.get()
+        if (startDragOffsetValue === null) {
           return
         }
-        const didScrollDown = e.contentOffset.y > startDragOffset.value
-        startDragOffset.value = null
-        startMode.value = null
-        if (e.contentOffset.y < headerHeight.value) {
+        const didScrollDown = e.contentOffset.y > startDragOffsetValue
+        startDragOffset.set(null)
+        startMode.set(null)
+        if (e.contentOffset.y < headerHeight.get()) {
           // If we're close to the top, show the shell.
           setMode(false)
         } else if (didScrollDown) {
@@ -77,7 +78,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
           setMode(true)
         } else {
           // Snap to whichever state is the closest.
-          setMode(Math.round(headerMode.value) === 1)
+          setMode(Math.round(headerMode.get()) === 1)
         }
       }
     },
@@ -88,8 +89,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
     (e: NativeScrollEvent) => {
       'worklet'
       if (isNative) {
-        startDragOffset.value = e.contentOffset.y
-        startMode.value = headerMode.value
+        startDragOffset.set(e.contentOffset.y)
+        startMode.set(headerMode.get())
       }
     },
     [headerMode, startDragOffset, startMode],
@@ -123,10 +124,12 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
     (e: NativeScrollEvent) => {
       'worklet'
       if (isNative) {
-        if (startDragOffset.value === null || startMode.value === null) {
+        const startDragOffsetValue = startDragOffset.get()
+        const startModeValue = startMode.get()
+        if (startDragOffsetValue === null || startModeValue === null) {
           if (
-            headerMode.value !== 0 &&
-            e.contentOffset.y < headerHeight.value
+            headerMode.get() !== 0 &&
+            e.contentOffset.y < headerHeight.get()
           ) {
             // If we're close enough to the top, always show the shell.
             // Even if we're not dragging.
@@ -137,29 +140,29 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
 
         // The "mode" value is always between 0 and 1.
         // Figure out how much to move it based on the current dragged distance.
-        const dy = e.contentOffset.y - startDragOffset.value
+        const dy = e.contentOffset.y - startDragOffsetValue
         const dProgress = interpolate(
           dy,
-          [-headerHeight.value, headerHeight.value],
+          [-headerHeight.get(), headerHeight.get()],
           [-1, 1],
         )
-        const newValue = clamp(startMode.value + dProgress, 0, 1)
-        if (newValue !== headerMode.value) {
+        const newValue = clamp(startModeValue + dProgress, 0, 1)
+        if (newValue !== headerMode.get()) {
           // Manually adjust the value. This won't be (and shouldn't be) animated.
           // Cancel any any existing animation
           cancelAnimation(headerMode)
-          headerMode.value = newValue
+          headerMode.set(newValue)
         }
       } else {
-        if (didJustRestoreScroll.value) {
-          didJustRestoreScroll.value = false
+        if (didJustRestoreScroll.get()) {
+          didJustRestoreScroll.set(false)
           // Don't hide/show navbar based on scroll restoratoin.
           return
         }
         // On the web, we don't try to follow the drag because we don't know when it ends.
         // Instead, show/hide immediately based on whether we're scrolling up or down.
-        const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
-        startDragOffset.value = e.contentOffset.y
+        const dy = e.contentOffset.y - (startDragOffset.get() ?? 0)
+        startDragOffset.set(e.contentOffset.y)
 
         if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
           setMode(false)
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 855ba21b2..1d1023c2b 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -134,7 +134,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
           footerMinimalShellTransform,
         ]}
         onLayout={e => {
-          footerHeight.value = e.nativeEvent.layout.height
+          footerHeight.set(e.nativeEvent.layout.height)
         }}>
         {hasSession ? (
           <>
-- 
cgit 1.4.1

