diff --git a/babel.config.js b/babel.config.js
index 24bdde26f2..1a11b2cb1f 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -29,7 +29,7 @@ module.exports = function (api) {
},
},
],
- 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
+ 'react-native-worklets/plugin', // NOTE: this plugin MUST be last
],
env: {
production: {
diff --git a/package.json b/package.json
index b0aed98b52..b5e6bed0c8 100644
--- a/package.json
+++ b/package.json
@@ -228,7 +228,7 @@
"react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
- "react-native-reanimated": "3.19.1",
+ "react-native-reanimated": "~4.3.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "4.24.0",
"react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder",
@@ -239,6 +239,7 @@
"react-native-web": "^0.21.0",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.15.0",
+ "react-native-worklets": "0.8.3",
"react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^10.0.1",
"react-textarea-autosize": "^8.5.3",
diff --git a/patches/react-native-reanimated@3.19.1.patch b/patches/react-native-reanimated@3.19.1.patch
deleted file mode 100644
index 8cc124d14b..0000000000
--- a/patches/react-native-reanimated@3.19.1.patch
+++ /dev/null
@@ -1,390 +0,0 @@
-diff --git a/lib/module/component/PerformanceMonitor.js b/lib/module/component/PerformanceMonitor.js
-index 9c98d6cc395419f50969753a4e4c7962b7be588f..3686a97280ac540efa0814ac4dea40358860a76f 100644
---- a/lib/module/component/PerformanceMonitor.js
-+++ b/lib/module/component/PerformanceMonitor.js
-@@ -1,125 +1,5 @@
- 'use strict';
-
--import React, { useEffect, useRef } from 'react';
--import { StyleSheet, TextInput, View } from 'react-native';
--import { addWhitelistedNativeProps } from "../ConfigHelper.js";
--import { createAnimatedComponent } from "../createAnimatedComponent/index.js";
--import { useAnimatedProps, useFrameCallback, useSharedValue } from "../hook/index.js";
--function createCircularDoublesBuffer(size) {
-- 'worklet';
--
-- return {
-- next: 0,
-- buffer: new Float32Array(size),
-- size,
-- count: 0,
-- push(value) {
-- const oldValue = this.buffer[this.next];
-- const oldCount = this.count;
-- this.buffer[this.next] = value;
-- this.next = (this.next + 1) % this.size;
-- this.count = Math.min(this.size, this.count + 1);
-- return oldCount === this.size ? oldValue : null;
-- },
-- front() {
-- const notEmpty = this.count > 0;
-- if (notEmpty) {
-- const current = this.next - 1;
-- const index = current < 0 ? this.size - 1 : current;
-- return this.buffer[index];
-- }
-- return null;
-- },
-- back() {
-- const notEmpty = this.count > 0;
-- return notEmpty ? this.buffer[this.next] : null;
-- }
-- };
--}
--const DEFAULT_BUFFER_SIZE = 20;
--addWhitelistedNativeProps({
-- text: true
--});
--const AnimatedTextInput = createAnimatedComponent(TextInput);
--function loopAnimationFrame(fn) {
-- let lastTime = 0;
-- function loop() {
-- requestAnimationFrame(time => {
-- if (lastTime > 0) {
-- fn(lastTime, time);
-- }
-- lastTime = time;
-- requestAnimationFrame(loop);
-- });
-- }
-- loop();
--}
--function getFps(renderTimeInMs) {
-- 'worklet';
--
-- return 1000 / renderTimeInMs;
--}
--function completeBufferRoutine(buffer, timestamp) {
-- 'worklet';
--
-- timestamp = Math.round(timestamp);
-- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
-- const measuredRangeDuration = timestamp - droppedTimestamp;
-- return getFps(measuredRangeDuration / buffer.count);
--}
--function JsPerformance({
-- smoothingFrames
--}) {
-- const jsFps = useSharedValue(null);
-- const totalRenderTime = useSharedValue(0);
-- const circularBuffer = useRef(createCircularDoublesBuffer(smoothingFrames));
-- useEffect(() => {
-- loopAnimationFrame((_, timestamp) => {
-- timestamp = Math.round(timestamp);
-- const currentFps = completeBufferRoutine(circularBuffer.current, timestamp);
--
-- // JS fps have to be measured every 2nd frame,
-- // thus 2x multiplication has to occur here
-- jsFps.value = (currentFps * 2).toFixed(0);
-- });
-- }, [jsFps, totalRenderTime]);
-- const animatedProps = useAnimatedProps(() => {
-- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
-- return {
-- text,
-- defaultValue: text
-- };
-- });
-- return
--
-- ;
--}
--function UiPerformance({
-- smoothingFrames
--}) {
-- const uiFps = useSharedValue(null);
-- const circularBuffer = useSharedValue(null);
-- useFrameCallback(({
-- timestamp
-- }) => {
-- if (circularBuffer.value === null) {
-- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
-- }
-- timestamp = Math.round(timestamp);
-- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
-- uiFps.value = currentFps.toFixed(0);
-- });
-- const animatedProps = useAnimatedProps(() => {
-- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
-- return {
-- text,
-- defaultValue: text
-- };
-- });
-- return
--
-- ;
--}
- /**
- * A component that lets you measure fps values on JS and UI threads on both the
- * Paper and Fabric architectures.
-@@ -127,38 +7,7 @@ function UiPerformance({
- * @param smoothingFrames - Determines amount of saved frames which will be used
- * for fps value smoothing.
- */
--export function PerformanceMonitor({
-- smoothingFrames = DEFAULT_BUFFER_SIZE
--}) {
-- return
--
--
-- ;
-+export function PerformanceMonitor() {
-+ return null;
- }
--const styles = StyleSheet.create({
-- monitor: {
-- flexDirection: 'row',
-- position: 'absolute',
-- backgroundColor: '#0006',
-- zIndex: 1000
-- },
-- header: {
-- fontSize: 14,
-- color: '#ffff',
-- paddingHorizontal: 5
-- },
-- text: {
-- fontSize: 13,
-- fontVariant: ['tabular-nums'],
-- color: '#ffff',
-- fontFamily: 'monospace',
-- paddingHorizontal: 3
-- },
-- container: {
-- alignItems: 'center',
-- justifyContent: 'center',
-- flexDirection: 'row',
-- flexWrap: 'wrap'
-- }
--});
- //# sourceMappingURL=PerformanceMonitor.js.map
-\ No newline at end of file
-diff --git a/src/component/PerformanceMonitor.tsx b/src/component/PerformanceMonitor.tsx
-index ff8fc8a947a0aeb959e21ec061882c3d190a2ce0..34dde79727765623df80dbcdab5016de6fd5d82c 100644
---- a/src/component/PerformanceMonitor.tsx
-+++ b/src/component/PerformanceMonitor.tsx
-@@ -1,170 +1,5 @@
- 'use strict';
-
--import React, { useEffect, useRef } from 'react';
--import { StyleSheet, TextInput, View } from 'react-native';
--
--import { addWhitelistedNativeProps } from '../ConfigHelper';
--import { createAnimatedComponent } from '../createAnimatedComponent';
--import type { FrameInfo } from '../frameCallback';
--import { useAnimatedProps, useFrameCallback, useSharedValue } from '../hook';
--
--type CircularBuffer = ReturnType;
--function createCircularDoublesBuffer(size: number) {
-- 'worklet';
--
-- return {
-- next: 0 as number,
-- buffer: new Float32Array(size),
-- size,
-- count: 0 as number,
--
-- push(value: number): number | null {
-- const oldValue = this.buffer[this.next];
-- const oldCount = this.count;
-- this.buffer[this.next] = value;
--
-- this.next = (this.next + 1) % this.size;
-- this.count = Math.min(this.size, this.count + 1);
-- return oldCount === this.size ? oldValue : null;
-- },
--
-- front(): number | null {
-- const notEmpty = this.count > 0;
-- if (notEmpty) {
-- const current = this.next - 1;
-- const index = current < 0 ? this.size - 1 : current;
-- return this.buffer[index];
-- }
-- return null;
-- },
--
-- back(): number | null {
-- const notEmpty = this.count > 0;
-- return notEmpty ? this.buffer[this.next] : null;
-- },
-- };
--}
--
--const DEFAULT_BUFFER_SIZE = 20;
--addWhitelistedNativeProps({ text: true });
--const AnimatedTextInput = createAnimatedComponent(TextInput);
--
--function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
-- let lastTime = 0;
--
-- function loop() {
-- requestAnimationFrame((time) => {
-- if (lastTime > 0) {
-- fn(lastTime, time);
-- }
-- lastTime = time;
-- requestAnimationFrame(loop);
-- });
-- }
--
-- loop();
--}
--
--function getFps(renderTimeInMs: number): number {
-- 'worklet';
-- return 1000 / renderTimeInMs;
--}
--
--function completeBufferRoutine(
-- buffer: CircularBuffer,
-- timestamp: number
--): number {
-- 'worklet';
-- timestamp = Math.round(timestamp);
--
-- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
--
-- const measuredRangeDuration = timestamp - droppedTimestamp;
--
-- return getFps(measuredRangeDuration / buffer.count);
--}
--
--function JsPerformance({ smoothingFrames }: { smoothingFrames: number }) {
-- const jsFps = useSharedValue(null);
-- const totalRenderTime = useSharedValue(0);
-- const circularBuffer = useRef(
-- createCircularDoublesBuffer(smoothingFrames)
-- );
--
-- useEffect(() => {
-- loopAnimationFrame((_, timestamp) => {
-- timestamp = Math.round(timestamp);
--
-- const currentFps = completeBufferRoutine(
-- circularBuffer.current,
-- timestamp
-- );
--
-- // JS fps have to be measured every 2nd frame,
-- // thus 2x multiplication has to occur here
-- jsFps.value = (currentFps * 2).toFixed(0);
-- });
-- }, [jsFps, totalRenderTime]);
--
-- const animatedProps = useAnimatedProps(() => {
-- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
-- return { text, defaultValue: text };
-- });
--
-- return (
--
--
--
-- );
--}
--
--function UiPerformance({ smoothingFrames }: { smoothingFrames: number }) {
-- const uiFps = useSharedValue(null);
-- const circularBuffer = useSharedValue(null);
--
-- useFrameCallback(({ timestamp }: FrameInfo) => {
-- if (circularBuffer.value === null) {
-- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
-- }
--
-- timestamp = Math.round(timestamp);
--
-- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
--
-- uiFps.value = currentFps.toFixed(0);
-- });
--
-- const animatedProps = useAnimatedProps(() => {
-- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
-- return { text, defaultValue: text };
-- });
--
-- return (
--
--
--
-- );
--}
--
--export type PerformanceMonitorProps = {
-- /**
-- * Sets amount of previous frames used for smoothing at highest expectedFps.
-- *
-- * Automatically scales down at lower frame rates.
-- *
-- * Affects jumpiness of the FPS measurements value.
-- */
-- smoothingFrames?: number;
--};
--
- /**
- * A component that lets you measure fps values on JS and UI threads on both the
- * Paper and Fabric architectures.
-@@ -172,40 +7,6 @@ export type PerformanceMonitorProps = {
- * @param smoothingFrames - Determines amount of saved frames which will be used
- * for fps value smoothing.
- */
--export function PerformanceMonitor({
-- smoothingFrames = DEFAULT_BUFFER_SIZE,
--}: PerformanceMonitorProps) {
-- return (
--
--
--
--
-- );
-+export function PerformanceMonitor() {
-+ return null;
- }
--
--const styles = StyleSheet.create({
-- monitor: {
-- flexDirection: 'row',
-- position: 'absolute',
-- backgroundColor: '#0006',
-- zIndex: 1000,
-- },
-- header: {
-- fontSize: 14,
-- color: '#ffff',
-- paddingHorizontal: 5,
-- },
-- text: {
-- fontSize: 13,
-- fontVariant: ['tabular-nums'],
-- color: '#ffff',
-- fontFamily: 'monospace',
-- paddingHorizontal: 3,
-- },
-- container: {
-- alignItems: 'center',
-- justifyContent: 'center',
-- flexDirection: 'row',
-- flexWrap: 'wrap',
-- },
--});
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 908b4b3314..19c937d66f 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -9,7 +9,8 @@ overrides:
'@expo/image-utils': '0.8.12'
'@types/estree': '1.0.6'
'react-native-compressor': '1.13.0'
- 'react-native-reanimated': '3.19.1'
+ 'react-native-reanimated': '4.3.1'
+ 'react-native-worklets': '0.8.3'
'psl': '1.9.0'
'@types/psl': '1.1.1'
'react-native-screens': '4.24.0'
@@ -35,7 +36,6 @@ patchedDependencies:
'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch
'react-native-keyboard-controller@1.21.8': patches/react-native-keyboard-controller@1.21.8.patch
'react-native-pager-view@6.8.0': patches/react-native-pager-view@6.8.0.patch
- 'react-native-reanimated@3.19.1': patches/react-native-reanimated@3.19.1.patch
'react-native-svg@15.12.1': patches/react-native-svg@15.12.1.patch
'react-native-view-shot@4.0.3': patches/react-native-view-shot@4.0.3.patch
'react-native@0.81.5': patches/react-native@0.81.5.patch
diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx
index b1eb873216..fdb4fa194d 100644
--- a/src/components/ContextMenu/index.tsx
+++ b/src/components/ContextMenu/index.tsx
@@ -89,14 +89,12 @@ const SPRING_IN: WithSpringConfig = {
mass: 0.75,
damping: 300,
stiffness: 1200,
- restDisplacementThreshold: 0.01,
}
const SPRING_OUT: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 150,
stiffness: 1000,
- restDisplacementThreshold: 0.01,
}
/**
diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx
index 08676bd172..78b893b698 100644
--- a/src/components/DraggableList/index.tsx
+++ b/src/components/DraggableList/index.tsx
@@ -237,8 +237,8 @@ function SortableItem({
itemKey: string
itemCount: number
itemHeight: number
- state: Animated.SharedValue
- dragY: Animated.SharedValue
+ state: SharedValue
+ dragY: SharedValue
scrollCompensation: SharedValue
isGestureActive: SharedValue
measureDone: SharedValue
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index c98dd7aed8..2564325935 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -1,4 +1,4 @@
-import {forwardRef, memo, useContext, useMemo} from 'react'
+import {memo, useContext, useMemo} from 'react'
import {
type StyleProp,
View,
@@ -6,6 +6,7 @@ import {
type ViewStyle,
} from 'react-native'
import Animated, {
+ type AnimatedRef,
type AnimatedScrollViewProps,
useAnimatedStyle,
} from 'react-native-reanimated'
@@ -73,67 +74,64 @@ export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp
contentContainerStyle?: StyleProp
ignoreTabletLayoutOffset?: boolean
+ ref?: AnimatedRef
}
/**
* Default scroll view for simple pages
*/
-export const Content = memo(
- forwardRef(function Content(
- {
- children,
- style,
- contentContainerStyle,
- ignoreTabletLayoutOffset,
- ...props
- },
- ref,
- ) {
- const t = useTheme()
- const {footerHeight} = useShellLayout()
- const {isWithinSplitView} = useIsWithinSplitView()
+export const Content = memo(function Content({
+ children,
+ style,
+ contentContainerStyle,
+ ignoreTabletLayoutOffset,
+ ref,
+ ...props
+}: ContentProps) {
+ const t = useTheme()
+ const {footerHeight} = useShellLayout()
+ const {isWithinSplitView} = useIsWithinSplitView()
- // note - if we ever make the footer transparent in any way,
- // we'll need to change this to use contentInsets/scrollIndicatorInsets
- // on iOS and contentContainerStyle padding on Android -sfn
- const animatedStyle = useAnimatedStyle(() => {
- return {
- marginBottom: footerHeight.get(),
- }
- })
+ // note - if we ever make the footer transparent in any way,
+ // we'll need to change this to use contentInsets/scrollIndicatorInsets
+ // on iOS and contentContainerStyle padding on Android -sfn
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ marginBottom: footerHeight.get(),
+ }
+ })
- return (
-
- {IS_WEB ? (
-
- {/* @ts-expect-error web only -esb */}
- {children}
-
- ) : (
- children
- )}
-
- )
- }),
-)
+ return (
+
+ {IS_WEB ? (
+
+ {/* @ts-expect-error web only -esb */}
+ {children}
+
+ ) : (
+ children
+ )}
+
+ )
+})
/**
* Utility component to center content within the screen
diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx
index c960490e91..94a39022e7 100644
--- a/src/components/Lightbox/pager/ImagePager.tsx
+++ b/src/components/Lightbox/pager/ImagePager.tsx
@@ -60,13 +60,11 @@ const SLOW_SPRING: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 300,
stiffness: 800,
- restDisplacementThreshold: 0.001,
}
const FAST_SPRING: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 150,
stiffness: 900,
- restDisplacementThreshold: 0.001,
}
function canAnimate(lightbox: Lightbox): boolean {
diff --git a/src/components/Lightbox/types.ts b/src/components/Lightbox/types.ts
index 19789fdc62..db68917452 100644
--- a/src/components/Lightbox/types.ts
+++ b/src/components/Lightbox/types.ts
@@ -6,7 +6,6 @@
*
*/
-import {type Component} from 'react'
import {type TransformsStyle} from 'react-native'
import {
type AnimatedRef,
@@ -29,7 +28,7 @@ export type ImageSource = {
thumbUri: string
thumbDimensions: Dimensions | null
thumbRect: MeasuredDimensions | null
- thumbRef?: AnimatedRef | null
+ thumbRef?: AnimatedRef | null
thumbBorderRadius?: number
alt?: string
type: 'image' | 'circle-avi' | 'rect-avi'
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
index e1e36e4502..5f509f89a1 100644
--- a/src/components/ProgressGuide/Toast.tsx
+++ b/src/components/ProgressGuide/Toast.tsx
@@ -119,7 +119,8 @@ export const ProgressGuideToast = forwardRef<
left = right = (winDim.width - 380) / 2
}
return {
- position: IS_WEB ? 'fixed' : 'absolute',
+ // position: fixed is web only
+ position: (IS_WEB ? 'fixed' : 'absolute') as 'absolute',
top: 0,
left,
right,
@@ -134,12 +135,7 @@ export const ProgressGuideToast = forwardRef<
return (
isOpen && (
-
+
- style?: StyleProp
+ style?: AnimatedStyle
}) {
const {t: l} = useLingui()
const t = useTheme()
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index a09aaab8c1..ce6b17b8cb 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -25,6 +25,7 @@ import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
import ProgressCircle from 'react-native-progress/Circle'
import Animated, {
type AnimatedRef,
+ type AnimatedStyle,
Easing,
FadeIn,
FadeOut,
@@ -1794,7 +1795,7 @@ function ComposerTopBar({
isEditingDraft: boolean
canSaveDraft: boolean
textLength: number
- topBarAnimatedStyle: StyleProp
+ topBarAnimatedStyle: AnimatedStyle
children?: React.ReactNode
}) {
const t = useTheme()
@@ -2029,7 +2030,7 @@ function ComposerPills({
thread: ThreadDraft
post: PostDraft
dispatch: (action: ComposerAction) => void
- bottomBarAnimatedStyle: StyleProp
+ bottomBarAnimatedStyle: AnimatedStyle
}) {
const t = useTheme()
const media = post.embed.media