diff --git a/patches/react-native-reanimated@4.3.2.patch b/patches/react-native-reanimated@4.3.2.patch new file mode 100644 index 0000000000..baaed85216 --- /dev/null +++ b/patches/react-native-reanimated@4.3.2.patch @@ -0,0 +1,297 @@ +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea41a50585 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +@@ -57,11 +57,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele + const SharedComponentDescriptorRegistry &componentDescriptorRegistry, + const std::shared_ptr &contextContainer, + jsi::Runtime &uiRuntime, +- const std::shared_ptr &uiScheduler ++ const std::shared_ptr &uiScheduler, ++ const std::shared_ptr &uiManager + #ifdef ANDROID + , + const PreserveMountedTagsFunction &filterUnmountedTagsFunction, +- const std::shared_ptr &uiManager, + const std::shared_ptr &jsInvoker + #endif + ) +@@ -69,11 +69,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele + contextContainer_(contextContainer), + componentDescriptorRegistry_(componentDescriptorRegistry), + uiRuntime_(uiRuntime), +- uiScheduler_(uiScheduler) ++ uiScheduler_(uiScheduler), ++ uiManager_(uiManager) + #ifdef ANDROID + , + preserveMountedTags_(filterUnmountedTagsFunction), +- uiManager_(uiManager), + jsInvoker_(jsInvoker) + #endif + { +@@ -93,10 +93,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele + SharedComponentDescriptorRegistry componentDescriptorRegistry_; + jsi::Runtime &uiRuntime_; + const std::shared_ptr uiScheduler_; ++ std::shared_ptr uiManager_; + PreserveMountedTagsFunction preserveMountedTags_; + + #ifdef ANDROID +- std::shared_ptr uiManager_; + std::shared_ptr jsInvoker_; + + void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const; +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h +index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc048bf4787 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h +@@ -66,11 +66,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, + const SharedComponentDescriptorRegistry &componentDescriptorRegistry, + const std::shared_ptr &contextContainer, + jsi::Runtime &uiRuntime, +- const std::shared_ptr &uiScheduler ++ const std::shared_ptr &uiScheduler, ++ const std::shared_ptr &uiManager + #ifdef ANDROID + , + const PreserveMountedTagsFunction &filterUnmountedTagsFunction, +- const std::shared_ptr &uiManager, + const std::shared_ptr &jsInvoker + #endif + ) +@@ -79,11 +79,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, + componentDescriptorRegistry, + contextContainer, + uiRuntime, +- uiScheduler ++ uiScheduler, ++ uiManager + #ifdef ANDROID + , + filterUnmountedTagsFunction, +- uiManager, + jsInvoker + #endif + ), +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6bb0d43b7 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +@@ -2,6 +2,7 @@ + #include + + #include ++#include + #include + + #include +@@ -53,14 +54,37 @@ std::optional LayoutAnimationsProxy_Legacy::pullTransaction + + parseRemoveMutations(movedViews, mutations, roots); + +- auto shouldAnimate = !surfacesToRemove_.contains(surfaceId); +- surfacesToRemove_.erase(surfaceId); ++ // Consume the teardown mark only on the transaction that actually clears ++ // the root — pulls emitted for animation frames must not eat it early. ++ auto shouldAnimate = true; ++ const auto removesRootChildren = std::ranges::any_of(mutations, [surfaceId](const auto &mutation) { ++ return mutation.type == ShadowViewMutation::Remove && mutation.parentTag == surfaceId; ++ }); ++ if (removesRootChildren) { ++ shouldAnimate = surfacesToRemove_.erase(surfaceId) == 0; ++ } + handleRemovals(filteredMutations, roots, deadNodes, shouldAnimate); + + handleUpdatesAndEnterings(filteredMutations, movedViews, mutations, propsParserContext, surfaceId); + + addOngoingAnimations(surfaceId, filteredMutations); + ++ // The LayoutAnimationDriver can emit a final keyframe update in the same ++ // transaction as the deferred Remove/Delete it withheld for a delete ++ // animation. We emit removals before updates, so such an update would ++ // otherwise reach the mounting layer after its view was deleted. ++ std::unordered_set deletedTags; ++ for (const auto &mutation : filteredMutations) { ++ if (mutation.type == ShadowViewMutation::Delete) { ++ deletedTags.insert(mutation.oldChildShadowView.tag); ++ } ++ } ++ if (!deletedTags.empty()) { ++ std::erase_if(filteredMutations, [&deletedTags](const auto &mutation) { ++ return mutation.type == ShadowViewMutation::Update && deletedTags.contains(mutation.newChildShadowView.tag); ++ }); ++ } ++ + return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry}; + } + +@@ -947,23 +971,22 @@ inline bool MutationNode::isMutationNode() { + return true; + } + +-// UIManagerAnimationDelegate +- +-void LayoutAnimationsProxy_Legacy::uiManagerDidConfigureNextLayoutAnimation( +- jsi::Runtime &runtime, +- const RawValue &config, +- const jsi::Value &successCallbackValue, +- const jsi::Value &failureCallbackValue) const {} ++// UIManagerCommitHook + +-void LayoutAnimationsProxy_Legacy::setComponentDescriptorRegistry( +- const SharedComponentDescriptorRegistry &componentDescriptorRegistry) {} +- +-bool LayoutAnimationsProxy_Legacy::shouldAnimateFrame() const { +- return false; +-} +- +-void LayoutAnimationsProxy_Legacy::stopSurface(SurfaceId surfaceId) { +- surfacesToRemove_.insert(surfaceId); ++// Surface teardown commits an empty root (SurfaceHandler::stop) before the ++// teardown transaction is pulled — mark it so pullTransaction skips exit ++// animations. Reading the ShadowTreeRegistry here instead would deadlock (#8579). ++RootShadowNode::Unshared LayoutAnimationsProxy_Legacy::shadowTreeWillCommit( ++ const ShadowTree &shadowTree, ++ const RootShadowNode::Shared & /*oldRootShadowNode*/, ++ const RootShadowNode::Unshared &newRootShadowNode) noexcept { ++ auto lock = std::unique_lock(mutex); ++ if (newRootShadowNode->getChildren().empty()) { ++ surfacesToRemove_.insert(shadowTree.getSurfaceId()); ++ } else { ++ surfacesToRemove_.erase(shadowTree.getSurfaceId()); ++ } ++ return newRootShadowNode; + } + + } // namespace reanimated +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h +index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c0022d197c9b 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h +@@ -3,8 +3,8 @@ + #include + #include + #include +-#include + #include ++#include + #include + #include + #include +@@ -102,7 +102,7 @@ struct SurfaceContext { + }; + + struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, +- public UIManagerAnimationDelegate, ++ public UIManagerCommitHook, + public std::enable_shared_from_this { + mutable std::unordered_map> nodeForTag_; + mutable std::recursive_mutex mutex; +@@ -116,11 +116,11 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, + const SharedComponentDescriptorRegistry &componentDescriptorRegistry, + const std::shared_ptr &contextContainer, + jsi::Runtime &uiRuntime, +- const std::shared_ptr &uiScheduler ++ const std::shared_ptr &uiScheduler, ++ const std::shared_ptr &uiManager + #ifdef ANDROID + , + const PreserveMountedTagsFunction &filterUnmountedTagsFunction, +- const std::shared_ptr &uiManager, + const std::shared_ptr &jsInvoker + #endif + ) +@@ -129,14 +129,19 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, + componentDescriptorRegistry, + contextContainer, + uiRuntime, +- uiScheduler ++ uiScheduler, ++ uiManager + #ifdef ANDROID + , + filterUnmountedTagsFunction, +- uiManager, + jsInvoker + #endif + ) { ++ uiManager->registerCommitHook(*this); ++ } ++ ++ ~LayoutAnimationsProxy_Legacy() override { ++ uiManager_->unregisterCommitHook(*this); + } + + void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const; +@@ -202,19 +207,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const override; + +- // UIManagerAnimationDelegate +- +- void uiManagerDidConfigureNextLayoutAnimation( +- jsi::Runtime &runtime, +- const RawValue &config, +- const jsi::Value &successCallbackValue, +- const jsi::Value &failureCallbackValue) const override; +- +- void setComponentDescriptorRegistry(const SharedComponentDescriptorRegistry &componentDescriptorRegistry) override; ++ // UIManagerCommitHook + +- bool shouldAnimateFrame() const override; ++ void commitHookWasRegistered(const UIManager &uiManager) noexcept override {} ++ void commitHookWasUnregistered(const UIManager &uiManager) noexcept override {} + +- void stopSurface(SurfaceId surfaceId) override; ++ RootShadowNode::Unshared shadowTreeWillCommit( ++ const ShadowTree &shadowTree, ++ const RootShadowNode::Shared &oldRootShadowNode, ++ const RootShadowNode::Unshared &newRootShadowNode) noexcept override; + }; + + } // namespace reanimated +diff --git a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +index 9ade22bf773005613048a00c47b35767628e86c6..688fdfb82715261fd08a63126c8691b0095fbf56 100644 +--- a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp ++++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +@@ -1306,11 +1306,11 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { + componentDescriptorRegistry, + scheduler->getContextContainer(), + getJSIRuntimeFromWorkletRuntime(uiRuntime_), +- uiScheduler_ ++ uiScheduler_, ++ uiManager_ + #ifdef ANDROID + , + filterUnmountedTagsFunction_, +- uiManager_, + jsInvoker_ + #endif + ); +@@ -1319,22 +1319,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { + #endif + layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental); + } else { +- auto layoutAnimationsProxyLegacy = std::make_shared( ++ layoutAnimationsProxy_ = std::make_shared( + layoutAnimationsManager_, + componentDescriptorRegistry, + scheduler->getContextContainer(), + getJSIRuntimeFromWorkletRuntime(uiRuntime_), +- uiScheduler_ ++ uiScheduler_, ++ uiManager_ + #ifdef ANDROID + , + filterUnmountedTagsFunction_, +- uiManager_, + jsInvoker_ + #endif + ); +- // TODO (future): support in experimental +- uiManager_->setAnimationDelegate(layoutAnimationsProxyLegacy.get()); +- layoutAnimationsProxy_ = std::move(layoutAnimationsProxyLegacy); + } + } + } diff --git a/patches/react-native-reanimated@4.3.2.patch.md b/patches/react-native-reanimated@4.3.2.patch.md new file mode 100644 index 0000000000..c97309ca0b --- /dev/null +++ b/patches/react-native-reanimated@4.3.2.patch.md @@ -0,0 +1,22 @@ +# react-native-reanimated@4.3.2.patch + +Backport of https://github.com/software-mansion/react-native-reanimated/pull/9901 +("refactor(LayoutAnimations): stop taking over UIManagerAnimationDelegate"). + +Reanimated's legacy `LayoutAnimationsProxy_Legacy` registered itself as the +`UIManagerAnimationDelegate` only to receive `stopSurface`. Occupying that slot +overwrites the `LayoutAnimationDriver` that React Native installs there, which +silently breaks `LayoutAnimation.configureNext` for the whole app. + +The patch makes the proxy detect surface teardown itself via a +`UIManagerCommitHook` (a commit with an empty root marks the surface in +`surfacesToRemove_`), frees the animation-delegate slot, and drops final +keyframe `Update` mutations for views deleted in the same transaction (a +deterministic `configureNext` delete-animation crash found in this app). +`uiManager` moves from Android-only to shared constructor args since the hook +registration needs it on both platforms. + +Only the `packages/react-native-reanimated` part of the PR is included (the +`apps/fabric-example` hunk is not part of the published package), and the +include hunk in `LayoutAnimationsProxy_Legacy.cpp` was adjusted to the 4.3.2 +release sources. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06d758a4f0..52e94c9d89 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,6 +36,7 @@ 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@4.3.2': patches/react-native-reanimated@4.3.2.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