diff --git a/src/Splash.tsx b/src/Splash.tsx index 8ef6b4cf7c..b450a2ddcd 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -9,13 +9,13 @@ import { import Animated, { Easing, interpolate, - runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import Svg, {Path, type SvgProps} from 'react-native-svg' +import {scheduleOnRN} from 'react-native-worklets' import {Image} from 'expo-image' import * as SplashScreen from 'expo-splash-screen' @@ -136,7 +136,7 @@ export function Splash(props: React.PropsWithChildren) { 1, {duration: 1200, easing: Easing.in(Easing.cubic)}, () => { - runOnJS(onFinish)() + scheduleOnRN(onFinish) }, ), ) diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index fdb4fa194d..d09e0e60ec 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -30,7 +30,6 @@ import {KeyboardEvents} from 'react-native-keyboard-controller' import Animated, { clamp, interpolate, - runOnJS, type SharedValue, useAnimatedReaction, useAnimatedStyle, @@ -44,6 +43,7 @@ import { useSafeAreaInsets, } from 'react-native-safe-area-context' import {captureRef} from 'react-native-view-shot' +import {scheduleOnRN} from 'react-native-worklets' import {Image, type ImageErrorEventData} from 'expo-image' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -166,7 +166,7 @@ export function Root({children}: {children: React.ReactNode}) { // note: return location has to be reset on open, // rather than on close, otherwise there's a flicker // where the reanimated update is faster than the react render - runOnJS(onCompletedClose)() + scheduleOnRN(onCompletedClose) } }), ) @@ -331,7 +331,7 @@ export function Trigger({ () => hoveredItemSV.get(), (hovered, prev) => { if (hovered !== prev) { - runOnJS(setHoveredMenuItem)(hovered) + scheduleOnRN(setHoveredMenuItem, hovered) } }, ) @@ -343,7 +343,7 @@ export function Trigger({ .averageTouches(true) .onStart(() => { 'worklet' - runOnJS(open)('full') + scheduleOnRN(open, 'full') }) .onUpdate(evt => { 'worklet' @@ -357,7 +357,7 @@ export function Trigger({ // as the menu may have slid into place beneath their finger const item = hoveredItemSV.get() if (item) { - runOnJS(onTouchUpMenuItem)(item) + scheduleOnRN(onTouchUpMenuItem, item) } }) }, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV]) diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 74b0c8a81c..4f48dfce8d 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -21,11 +21,11 @@ import { } from 'react-native' import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' import Animated, { - runOnJS, type ScrollEvent, useAnimatedStyle, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -299,9 +299,9 @@ export const InnerFlatList = forwardRef< } const {contentOffset} = e if (contentOffset.y > 0 && !disableDrag) { - runOnJS(setDisableDrag)(true) + scheduleOnRN(setDisableDrag, true) } else if (contentOffset.y <= 1 && disableDrag) { - runOnJS(setDisableDrag)(false) + scheduleOnRN(setDisableDrag, false) } } diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx index 7e70e2f07d..ebdf621b95 100644 --- a/src/components/DraggableList/index.tsx +++ b/src/components/DraggableList/index.tsx @@ -4,7 +4,6 @@ import Animated, { type AnimatedRef, measure, Reanimated3DefaultSpringConfig, - runOnJS, scrollTo, type SharedValue, useAnimatedRef, @@ -14,6 +13,7 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {useHaptics} from '#/lib/haptics' import {atoms as a, useTheme, web} from '#/alf' @@ -26,7 +26,7 @@ import {IS_IOS} from '#/env' * * All positioning is driven by a `slots` map (key → index) and translateY * (no discrete `top` changes). On drag end the new slot assignment is - * computed on the UI thread first, then React state is updated via runOnJS. + * computed on the UI thread first, then React state is updated via scheduleOnRN. * * See SortableList.web.tsx for the web implementation using pointer events. */ @@ -265,9 +265,9 @@ function SortableItem({ measureDone.set(false) lastHapticSlot.set(mySlot) if (onDragStart) { - runOnJS(onDragStart)() + scheduleOnRN(onDragStart) } - runOnJS(playHaptic)() + scheduleOnRN(playHaptic) }) .onChange(e => { 'worklet' @@ -285,7 +285,7 @@ function SortableItem({ const clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1)) if (IS_IOS && clampedSlot !== lastHapticSlot.get()) { lastHapticSlot.set(clampedSlot) - runOnJS(playHaptic)('Light') + scheduleOnRN(playHaptic, 'Light') } }) .onEnd(() => { @@ -326,13 +326,13 @@ function SortableItem({ dragStartSlot: -1, }) dragY.set(0) - runOnJS(onCommitReorder)(sorted) + scheduleOnRN(onCommitReorder, sorted) } else { const s = state.get() state.set({...s, activeKey: '', dragStartSlot: -1}) dragY.set(0) if (onDragEnd) { - runOnJS(onDragEnd)() + scheduleOnRN(onDragEnd) } } } @@ -347,7 +347,7 @@ function SortableItem({ const s = state.get() state.set({...s, activeKey: '', dragStartSlot: -1}) if (onDragEnd) { - runOnJS(onDragEnd)() + scheduleOnRN(onDragEnd) } } }) diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx index 632f5eb362..9088b9ded9 100644 --- a/src/components/Lightbox/chrome/ImageMenu.tsx +++ b/src/components/Lightbox/chrome/ImageMenu.tsx @@ -2,12 +2,12 @@ import {useRef, useState} from 'react' import {Modal, Pressable, StyleSheet, View} from 'react-native' import Animated, { interpolate, - runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {useLingui} from '@lingui/react/macro' import {atoms as a} from '#/alf' @@ -52,7 +52,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) { progress.set( withTiming(0, TIMING_OUT, finished => { if (finished) { - runOnJS(setIsMounted)(false) + scheduleOnRN(setIsMounted, false) } }), ) diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx index b27e2d9aba..72d39875ab 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx @@ -8,7 +8,6 @@ import { import Animated, { type AnimatableValue, Reanimated3DefaultSpringConfig, - runOnJS, type SharedValue, useAnimatedReaction, useAnimatedRef, @@ -16,6 +15,7 @@ import Animated, { useSharedValue, withSpring, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {Image} from 'expo-image' import { @@ -95,7 +95,7 @@ const ImageItem = ({ }, (nextIsScaled, prevIsScaled) => { if (nextIsScaled !== prevIsScaled) { - runOnJS(handleZoom)(nextIsScaled) + scheduleOnRN(handleZoom, nextIsScaled) } }, ) @@ -245,7 +245,7 @@ const ImageItem = ({ const singleTap = Gesture.Tap().onEnd(() => { 'worklet' - runOnJS(onTap)() + scheduleOnRN(onTap) }) const doubleTap = Gesture.Tap() @@ -359,9 +359,9 @@ const ImageItem = ({ }, (show, prevShow) => { if (!prevShow && show) { - runOnJS(setShowLoader)(true) + scheduleOnRN(setShowLoader, true) } else if (prevShow && !show) { - runOnJS(setShowLoader)(false) + scheduleOnRN(setShowLoader, false) } }, ) diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx index 216ac176c0..9cf088f864 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx @@ -14,7 +14,6 @@ import { type PanGesture, } from 'react-native-gesture-handler' import Animated, { - runOnJS, type SharedValue, useAnimatedProps, useAnimatedReaction, @@ -24,6 +23,7 @@ import Animated, { useSharedValue, } from 'react-native-reanimated' import {useSafeAreaFrame} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import {Image} from 'expo-image' import { @@ -84,7 +84,7 @@ const ImageItem = ({ 'worklet' const nextIsScaled = e.zoomScale > 1 if (scaled !== nextIsScaled) { - runOnJS(handleZoom)(nextIsScaled) + scheduleOnRN(handleZoom, nextIsScaled) } }, onBeginDrag() { @@ -118,7 +118,7 @@ const ImageItem = ({ const singleTap = Gesture.Tap().onEnd(() => { 'worklet' - runOnJS(onTap)() + scheduleOnRN(onTap) }) const doubleTap = Gesture.Tap() @@ -142,7 +142,7 @@ const ImageItem = ({ screenSize, ) } - runOnJS(zoomTo)(nextZoomRect) + scheduleOnRN(zoomTo, nextZoomRect) }) const composedGesture = Gesture.Exclusive( @@ -199,9 +199,9 @@ const ImageItem = ({ }, (show, prevShow) => { if (!prevShow && show) { - runOnJS(setShowLoader)(true) + scheduleOnRN(setShowLoader, true) } else if (prevShow && !show) { - runOnJS(setShowLoader)(false) + scheduleOnRN(setShowLoader, false) } }, ) diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx index 7b7548a0e3..78abbf9f77 100644 --- a/src/components/Lightbox/pager/ImagePager.tsx +++ b/src/components/Lightbox/pager/ImagePager.tsx @@ -20,8 +20,6 @@ import Animated, { measure, type MeasuredDimensions, ReduceMotion, - runOnJS, - runOnUI, type SharedValue, useAnimatedReaction, useAnimatedRef, @@ -32,6 +30,7 @@ import Animated, { withSpring, type WithSpringConfig, } from 'react-native-reanimated' +import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets' import {Image} from 'expo-image' import * as ScreenOrientation from 'expo-screen-orientation' @@ -136,10 +135,10 @@ export default function ImageViewRoot({ const onFullyClosed = useCallback(() => { setActiveLightbox(null) - runOnUI(() => { + scheduleOnUI(() => { 'worklet' thumbRects.set({}) - })() + }) requestIdleCallback(() => { void Image.clearMemoryCache() }) @@ -149,7 +148,7 @@ export default function ImageViewRoot({ () => openProgress.get() === 0, (isGone, wasGone) => { if (isGone && !wasGone) { - runOnJS(onFullyClosed)() + scheduleOnRN(onFullyClosed) } }, ) @@ -160,10 +159,10 @@ export default function ImageViewRoot({ () => openProgress.get() === 1, (isOpen, wasOpen) => { if (isOpen && !wasOpen) { - runOnJS(ScreenOrientation.unlockAsync)() + scheduleOnRN(ScreenOrientation.unlockAsync) } else if (!isOpen && wasOpen) { // default is PORTRAIT_UP - set via config plugin in app.config.js -sfn - runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP) + scheduleOnRN(ScreenOrientation.lockAsync, PORTRAIT_UP) } }, ) @@ -171,7 +170,7 @@ export default function ImageViewRoot({ const onFlyAway = useCallback(() => { 'worklet' openProgress.set(0) - runOnJS(onRequestClose)() + scheduleOnRN(onRequestClose) }, [onRequestClose, openProgress]) return ( @@ -320,7 +319,7 @@ function ImageView({ const handleRequestClose = useCallback(() => { const activeRef = images[imageIndex]?.thumbRef if (isAnimated && activeRef) { - runOnUI(() => { + scheduleOnUI(() => { 'worklet' const rect = measure(activeRef) thumbRects.modify(rects => { @@ -328,8 +327,8 @@ function ImageView({ rects[imageIndex] = rect return rects }) - runOnJS(onRequestClose)() - })() + scheduleOnRN(onRequestClose) + }) } else { onRequestClose() } diff --git a/src/components/Lightbox/state.tsx b/src/components/Lightbox/state.tsx index 6a8fdbe0b7..b5284a6d7c 100644 --- a/src/components/Lightbox/state.tsx +++ b/src/components/Lightbox/state.tsx @@ -1,10 +1,6 @@ import {createContext, useContext, useEffect, useMemo, useState} from 'react' -import { - measure, - type MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' +import {measure, type MeasuredDimensions} from 'react-native-reanimated' +import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets' import {nanoid} from 'nanoid/non-secure' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -73,7 +69,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { if (thumbRef) { // Measure the tapped image on the UI thread, then open with // the rect baked in so it's available from the first render. - // Only the rect (plain data) goes through runOnJS — AnimatedRef + // Only the rect (plain data) goes through scheduleOnRN — AnimatedRef // objects can't survive serialization across threads. const openWithRect = (rect: MeasuredDimensions | null) => { doOpen({ @@ -83,11 +79,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ), }) } - runOnUI(() => { + scheduleOnUI(() => { 'worklet' const rect = measure(thumbRef) - runOnJS(openWithRect)(rect) - })() + scheduleOnRN(openWithRect, rect) + }) } else { doOpen(lightbox) } diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index 56e84cfd4f..9decdea95b 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -8,12 +8,12 @@ import { } from 'react-native' import Animated, { measure, - runOnJS, useAnimatedRef, useFrameCallback, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {WebView} from 'react-native-webview' +import {scheduleOnRN} from 'react-native-worklets' import {Image} from 'expo-image' import {type AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/core/macro' @@ -164,7 +164,7 @@ export function ExternalPlayer({ const isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top if (!isVisible) { - runOnJS(setIsPlayerActive)(false) + scheduleOnRN(setIsPlayerActive, false) } }, false) // False here disables autostarting the callback diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx index 5f509f89a1..961f3ed231 100644 --- a/src/components/ProgressGuide/Toast.tsx +++ b/src/components/ProgressGuide/Toast.tsx @@ -9,12 +9,12 @@ import { import {Pressable, useWindowDimensions, View} from 'react-native' import Animated, { Easing, - runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -69,7 +69,7 @@ export const ProgressGuideToast = forwardRef< duration: 400, easing: Easing.out(Easing.cubic), }, - () => runOnJS(setIsntOpen)(), + () => scheduleOnRN(setIsntOpen), ), ) }, [setIsOpen, opacity]) @@ -88,7 +88,7 @@ export const ProgressGuideToast = forwardRef< duration: 100, easing: Easing.out(Easing.cubic), }, - () => runOnJS(playCheckmark)(), + () => scheduleOnRN(playCheckmark), ), ) translateY.set(0) diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx index fa9520286c..049dcac26a 100644 --- a/src/components/dms/NewMessagesPill.tsx +++ b/src/components/dms/NewMessagesPill.tsx @@ -1,12 +1,12 @@ import {useCallback} from 'react' import {Pressable, View} from 'react-native' import Animated, { - runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import { ScaleAndFadeIn, @@ -41,7 +41,7 @@ export function NewMessagesPill({ }, [scale]) const onPress = useCallback(() => { - runOnJS(playHaptic)() + scheduleOnRN(playHaptic) onPressInner?.() }, [onPressInner, playHaptic]) diff --git a/src/components/dms/SwipeToReply.tsx b/src/components/dms/SwipeToReply.tsx index 3ae0347374..7cdd256aa9 100644 --- a/src/components/dms/SwipeToReply.tsx +++ b/src/components/dms/SwipeToReply.tsx @@ -4,13 +4,13 @@ import {Gesture, type GestureType} from 'react-native-gesture-handler' import Animated, { clamp, interpolate, - runOnJS, useAnimatedStyle, useReducedMotion, useSharedValue, withSequence, withTiming, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {useHaptics} from '#/lib/haptics' import {atoms as a, tokens, useTheme} from '#/alf' @@ -111,7 +111,7 @@ export function SwipeToReply({ if (pastThreshold && !hit.get()) { hit.set(true) runPop() - runOnJS(playHaptic)('Medium') + scheduleOnRN(playHaptic, 'Medium') } else if (!pastThreshold && hit.get()) { hit.set(false) } @@ -120,7 +120,7 @@ export function SwipeToReply({ 'worklet' // Only a clean end (finger lifted past threshold) triggers the reply. if (hit.get()) { - runOnJS(onReply)() + scheduleOnRN(onReply) } }) .onFinalize(() => { diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx index 71b7864df2..d70a1546fc 100644 --- a/src/lib/custom-animations/GestureActionView.tsx +++ b/src/lib/custom-animations/GestureActionView.tsx @@ -5,7 +5,6 @@ import Animated, { clamp, interpolate, interpolateColor, - runOnJS, useAnimatedReaction, useAnimatedStyle, useDerivedValue, @@ -14,6 +13,7 @@ import Animated, { withSequence, withTiming, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {useHaptics} from '#/lib/haptics' import {type GestureActions} from './GestureActionView.shared' @@ -74,17 +74,17 @@ export function GestureActionView({ () => transX, () => { if (transX.get() === 0) { - runOnJS(setActiveAction)(null) + scheduleOnRN(setActiveAction, null) } else if (transX.get() < 0) { if ( actions.leftSecond && transX.get() <= -actions.leftSecond.threshold ) { if (activeAction !== 'leftSecond') { - runOnJS(setActiveAction)('leftSecond') + scheduleOnRN(setActiveAction, 'leftSecond') } } else if (activeAction !== 'leftFirst') { - runOnJS(setActiveAction)('leftFirst') + scheduleOnRN(setActiveAction, 'leftFirst') } } else if (transX.get() > 0) { if ( @@ -92,10 +92,10 @@ export function GestureActionView({ transX.get() > actions.rightSecond.threshold ) { if (activeAction !== 'rightSecond') { - runOnJS(setActiveAction)('rightSecond') + scheduleOnRN(setActiveAction, 'rightSecond') } } else if (activeAction !== 'rightFirst') { - runOnJS(setActiveAction)('rightFirst') + scheduleOnRN(setActiveAction, 'rightFirst') } } }, @@ -127,7 +127,7 @@ export function GestureActionView({ !hitSecond.get() ) { runPopAnimation() - runOnJS(haptic)() + scheduleOnRN(haptic) hitSecond.set(true) } else if ( hitSecond.get() && @@ -144,7 +144,7 @@ export function GestureActionView({ !hitFirst.get() ) { runPopAnimation() - runOnJS(haptic)() + scheduleOnRN(haptic) hitFirst.set(true) } else if ( hitFirst.get() && @@ -161,7 +161,7 @@ export function GestureActionView({ !hitSecond.get() ) { runPopAnimation() - runOnJS(haptic)() + scheduleOnRN(haptic) hitSecond.set(true) } else if ( hitSecond.get() && @@ -178,7 +178,7 @@ export function GestureActionView({ !hitFirst.get() ) { runPopAnimation() - runOnJS(haptic)() + scheduleOnRN(haptic) hitFirst.set(true) } else if ( hitFirst.get() && @@ -193,15 +193,15 @@ export function GestureActionView({ 'worklet' if (e.translationX < 0) { if (hitSecond.get() && actions.leftSecond) { - runOnJS(actions.leftSecond.action)() + scheduleOnRN(actions.leftSecond.action) } else if (hitFirst.get() && actions.leftFirst) { - runOnJS(actions.leftFirst.action)() + scheduleOnRN(actions.leftFirst.action) } } else if (e.translationX > 0) { if (hitSecond.get() && actions.rightSecond) { - runOnJS(actions.rightSecond.action)() + scheduleOnRN(actions.rightSecond.action) } else if (hitSecond.get() && actions.rightFirst) { - runOnJS(actions.rightFirst.action)() + scheduleOnRN(actions.rightFirst.action) } } transX.set(() => withTiming(0, {duration: 200})) diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index c5976fb688..1bc5e20e1e 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -7,10 +7,10 @@ import { import Animated, { Extrapolation, interpolate, - runOnJS, useAnimatedStyle, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import {GlassContainer} from 'expo-glass-effect' import {LinearGradient} from 'expo-linear-gradient' import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api' @@ -95,7 +95,7 @@ export function MessageComposer({ onEnd: evt => { 'worklet' if (IS_ANDROID && evt.progress === 0) { - runOnJS(blur)() + scheduleOnRN(blur) } }, }) diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 3890979543..312a2277ad 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -13,7 +13,6 @@ import { KeyboardGestureArea, } from 'react-native-keyboard-controller' import Animated, { - runOnJS, type ScrollEvent, type SharedValue, useAnimatedStyle, @@ -22,6 +21,7 @@ import Animated, { withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import { type $Typed, type AppBskyEmbedRecord, @@ -502,7 +502,7 @@ export function MessagesList({ (e.contentOffset.y > newMessagesPill.startContentOffset + 200 || isAtBottom.get()) ) { - runOnJS(setNewMessagesPill)({ + scheduleOnRN(setNewMessagesPill, { show: false, startContentOffset: 0, }) diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx index 70453f7ae5..ef152aa32f 100644 --- a/src/screens/Profile/Header/GrowableBanner.tsx +++ b/src/screens/Profile/Header/GrowableBanner.tsx @@ -4,13 +4,13 @@ import Animated, { type AnimatedRef, Extrapolation, interpolate, - runOnJS, type SharedValue, useAnimatedProps, useAnimatedReaction, useAnimatedStyle, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import {BlurView} from 'expo-blur' import {useIsFetching} from '@tanstack/react-query' @@ -226,7 +226,7 @@ function useShouldAnimateSpinner({ () => scrollY.get() < -5, (value, prevValue) => { if (value !== prevValue) { - runOnJS(setIsOverscrolled)(value) + scheduleOnRN(setIsOverscrolled, value) } }, [scrollY], diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index b064ea329a..0b7ab16ec4 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -1,12 +1,12 @@ import {memo, useMemo, useState} from 'react' import {type LayoutChangeEvent, StyleSheet, View} from 'react-native' import Animated, { - runOnJS, useAnimatedReaction, useAnimatedStyle, withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnRN} from 'react-native-worklets' import { type AppBskyActorDefs, type AppBskyLabelerDefs, @@ -151,7 +151,7 @@ const MinimalHeader = memo(function MinimalHeader({ () => scrollY.get() > 100, (value, prev) => { if (prev !== value) { - runOnJS(setVisible)(value) + scheduleOnRN(setVisible, value) } }, ) diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index acaf02453c..8eabe3e62a 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -8,8 +8,6 @@ import { import Animated, { clamp, interpolate, - runOnJS, - runOnUI, type SharedValue, useAnimatedReaction, useAnimatedStyle, @@ -20,6 +18,7 @@ import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context' +import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets' import {useEventListener} from 'expo' import {type VideoPlayer} from 'expo-video' @@ -66,7 +65,7 @@ export function Scrubber({ () => Math.round(seekProgressSV.get()), (progress, prevProgress) => { if (progress !== prevProgress) { - runOnJS(setCurrentSeekTime)(progress) + scheduleOnRN(setCurrentSeekTime, progress) } }, ) @@ -76,11 +75,11 @@ export function Scrubber({ player?.seekBy(time) setTimeout(() => { - runOnUI(() => { + scheduleOnUI(() => { 'worklet' isSeekingSV.set(false) seekingAnimationSV.set(withTiming(0, {duration: 500})) - })() + }) }, 50) }, [player, isSeekingSV, seekingAnimationSV], @@ -116,7 +115,7 @@ export function Scrubber({ // it's seek by, so offset by the current time // seekBy sets isSeekingSV back to false, so no need to do that here - runOnJS(seekBy)(newTime - currentTimeSV.get()) + scheduleOnRN(seekBy, newTime - currentTimeSV.get()) }) }, [ scrollGesture, @@ -253,7 +252,7 @@ function PlayerListener({ if (duration !== 0) { setDuration(Math.round(duration)) } - runOnUI(updateTime)(evt.currentTime, duration) + scheduleOnUI(updateTime, evt.currentTime, duration) }) return null diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 8a2f3b7162..23df77e06c 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -32,7 +32,6 @@ import Animated, { interpolateColor, LayoutAnimationConfig, LinearTransition, - runOnUI, scrollTo, useAnimatedRef, useAnimatedScrollHandler, @@ -45,6 +44,7 @@ import Animated, { ZoomOut, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {scheduleOnUI} from 'react-native-worklets' import * as FileSystem from 'expo-file-system' import {type ImagePickerAsset} from 'expo-image-picker' import { @@ -2382,7 +2382,7 @@ function useScrollTracker({ const onScrollViewContentSizeChange = useCallback( (_width: number, height: number) => { - runOnUI(onScrollViewContentSizeChangeUIThread)(height) + scheduleOnUI(onScrollViewContentSizeChangeUIThread, height) }, [onScrollViewContentSizeChangeUIThread], ) diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 6d0982ad8d..9aa709f7fe 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -18,12 +18,12 @@ import PagerView, { type PageScrollStateChangedNativeEventData, } from 'react-native-pager-view' import Animated, { - runOnJS, type SharedValue, useEvent, useHandler, useSharedValue, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {useFocusEffect} from '@react-navigation/native' import {useSetDrawerSwipeDisabled} from '#/state/shell' @@ -125,7 +125,7 @@ export function Pager({ }, onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) { 'worklet' - runOnJS(setIsIdle)(e.pageScrollState === 'idle') + scheduleOnRN(setIsIdle, e.pageScrollState === 'idle') if (dragState.get() === 'idle' && e.pageScrollState === 'settling') { // This is a programmatic scroll on Android. // Stay "idle" to match iOS and avoid confusing downstream code. @@ -137,7 +137,7 @@ export function Pager({ onPageSelected(e: PagerViewOnPageSelectedEventData) { 'worklet' didInit.set(true) - runOnJS(onPageSelectedJSThread)(e.position) + scheduleOnRN(onPageSelectedJSThread, e.position) }, }, [parentOnPageScrollStateChanged], diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 61a30de4a5..c2c52d72aa 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -8,13 +8,13 @@ import { } from 'react-native' import Animated, { type AnimatedRef, - runOnUI, scrollTo, type SharedValue, useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' +import {scheduleOnUI} from 'react-native-worklets' import {ScrollProvider} from '#/lib/ScrollContext' import { @@ -181,7 +181,7 @@ export function PagerWithHeader({ ) const onTabPressed = useCallback(() => { - runOnUI(adjustScrollForOtherPages)('dragging') + scheduleOnUI(adjustScrollForOtherPages, 'dragging') }, [adjustScrollForOtherPages]) return ( diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index c07f44ac8b..273e6f22de 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -7,8 +7,6 @@ import { } from 'react-native' import Animated, { interpolate, - runOnJS, - runOnUI, scrollTo, type SharedValue, useAnimatedReaction, @@ -16,6 +14,7 @@ import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' +import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets' import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' @@ -126,7 +125,7 @@ export function TabBar({ const progress = dragProgress.get() const offset = progressToOffset(progress) // It's unclear why we need to go back to JS here. It seems iOS-specific. - runOnJS(scrollToOffsetJS)(offset) + scheduleOnRN(scrollToOffsetJS, offset) } } }, @@ -303,7 +302,7 @@ export function TabBar({ const onPressItem = useCallback( (index: number) => { - runOnUI(onPressUIThread)(index) + scheduleOnUI(onPressUIThread, index) onSelect?.(index) if (index === selectedPage) { onPressSelected?.(index) @@ -409,14 +408,14 @@ function TabBarItem({ const handleLayout = useCallback( (e: LayoutChangeEvent) => { - runOnUI(onItemLayout)(index, e.nativeEvent.layout) + scheduleOnUI(onItemLayout, index, e.nativeEvent.layout) }, [index, onItemLayout], ) const handleTextLayout = useCallback( (e: LayoutChangeEvent) => { - runOnUI(onTextLayout)(index, e.nativeEvent.layout) + scheduleOnUI(onTextLayout, index, e.nativeEvent.layout) }, [index, onTextLayout], ) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index f8a537a657..0489f83fb9 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -2,10 +2,10 @@ import {forwardRef, memo, useDeferredValue, useMemo} from 'react' import {RefreshControl, type ViewToken} from 'react-native' import { type FlatListPropsWithLayout, - runOnJS, useAnimatedScrollHandler, useSharedValue, } from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {updateActiveVideoViewAsync} from '@bsky.app/video' import {useDedupe} from '#/lib/hooks/useDedupe' @@ -85,7 +85,7 @@ let List = forwardRef( onBeginDragFromContext?.(e, ctx) }, onEndDrag(e, ctx) { - runOnJS(updateActiveVideoViewAsync)() + scheduleOnRN(updateActiveVideoViewAsync) onEndDragFromContext?.(e, ctx) }, onScroll(e, ctx) { @@ -95,18 +95,18 @@ let List = forwardRef( if (isScrolledDown.get() !== didScrollDown) { isScrolledDown.set(didScrollDown) if (onScrolledDownChange != null) { - runOnJS(handleScrolledDownChange)(didScrollDown) + scheduleOnRN(handleScrolledDownChange, didScrollDown) } } if (IS_IOS) { - runOnJS(dedupe)(updateActiveVideoViewAsync) + scheduleOnRN(dedupe, updateActiveVideoViewAsync) } }, // Note: adding onMomentumBegin here makes simulator scroll // lag on Android. So either don't add it, or figure out why. onMomentumEnd(e, ctx) { - runOnJS(updateActiveVideoViewAsync)() + scheduleOnRN(updateActiveVideoViewAsync) onMomentumEndFromContext?.(e, ctx) }, })