From b13b722b13a348025e79ee2f339837c66b28c2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 20 Jan 2026 11:15:50 +0100 Subject: [PATCH 01/10] fix(android): pass full image object to native side for `defaultSource` & `loadingIndicatorSrc` --- packages/react-native/Libraries/Image/Image.android.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index 06cd6677bafd28..41d36222863e47 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -186,9 +186,9 @@ let BaseImage: AbstractImageAndroid = ({ /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found * when making Flow check .android.js files. */ headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}), - defaultSource: defaultSource ? defaultSource.uri : null, + defaultSource: defaultSource ? defaultSource : null, loadingIndicatorSrc: loadingIndicatorSource - ? loadingIndicatorSource.uri + ? loadingIndicatorSource : null, accessibilityLabel: props['aria-label'] ?? props.accessibilityLabel ?? props.alt, From a2a5844d652884f498840257b7045b3d284cecb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 16 Jan 2026 17:17:56 +0100 Subject: [PATCH 02/10] fix(android): image w&h are not to be wrapped in size this is needed on ios but not android --- .../ReactCommon/react/renderer/imagemanager/primitives.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h index a89807bcb21568..7866ee8d9ebd68 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h @@ -59,9 +59,8 @@ class ImageSource { imageSourceResult["scale"] = scale; folly::dynamic sizeResult = folly::dynamic::object(); - sizeResult["width"] = size.width; - sizeResult["height"] = size.height; - imageSourceResult["size"] = sizeResult; + imageSourceResult["width"] = size.width; + imageSourceResult["height"] = size.height; imageSourceResult["body"] = body; imageSourceResult["method"] = method; From 3777589c9fe48bb38ccf54da318d85a86b12904f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 16 Jan 2026 17:18:18 +0100 Subject: [PATCH 03/10] fix setting default source --- .../react/views/image/ReactImageManager.kt | 32 ++++++++-- .../react/views/image/ReactImageView.kt | 61 ++++++++----------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index 403583f205acf3..41640e7ed182df 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -12,8 +12,11 @@ import android.graphics.PorterDuff import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder +import com.facebook.react.BuildConfig +import com.facebook.react.bridge.ReactNoCrashSoftException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.ReactConstants import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -130,14 +133,35 @@ public constructor( } @ReactProp(name = "defaultSource") - public fun setDefaultSource(view: ReactImageView, source: String?) { - view.setDefaultSource(source) + public fun setDefaultSource(view: ReactImageView, source: ReadableMap?) { + if (source == null) { + view.setDefaultSource(null) + return + } + val imageSource = view.readableMapToImageSource(source, false) + if (!BuildConfig.DEBUG && !imageSource.isResource) { + throw ReactNoCrashSoftException( + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}", + ) + } + + view.setDefaultSource(imageSource.uri.toString()) } // In JS this is Image.props.loadingIndicatorSource.uri @ReactProp(name = "loadingIndicatorSrc") - public fun setLoadingIndicatorSource(view: ReactImageView, source: String?) { - view.setLoadingIndicatorSource(source) + public fun setLoadingIndicatorSource(view: ReactImageView, source: ReadableMap?) { + if (source == null) { + view.setLoadingIndicatorSource(null) + return + } + val imageSource = view.readableMapToImageSource(source, false) + if (!BuildConfig.DEBUG && !imageSource.isResource) { + throw ReactNoCrashSoftException( + "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}", + ) + } + view.setLoadingIndicatorSource(imageSource.uri.toString()) } @ReactProp(name = "borderColor", customType = "Color") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index db7a890ffdaa9d..29f677346d9ccc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -272,6 +272,30 @@ public class ReactImageView( } } + // todo: this must live elsewhere i think + public fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { + val cacheControl = computeCacheControl(source.getString("cache")) + val uri = source.getString("uri") + var imageSource = if (includeSize) { + ImageSource( + context, + uri, + source.getDouble("width"), + source.getDouble("height"), + cacheControl, + ) + } else { + ImageSource(context, uri, cacheControl = cacheControl) + } + + if (Uri.EMPTY == imageSource.uri) { + warnImageSource(uri) + imageSource = getTransparentBitmapImageSource(context) + } + + return imageSource + } + public fun setSource(sources: ReadableArray?) { val tmpSources = mutableListOf() @@ -280,45 +304,12 @@ public class ReactImageView( } else if (sources.size() == 1) { // Optimize for the case where we have just one uri, case in which we don't need the sizes val source = checkNotNull(sources.getMap(0)) - val cacheControl = computeCacheControl(source.getString("cache")) - var imageSource = ImageSource(context, source.getString("uri"), cacheControl = cacheControl) - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(source.getString("uri")) - imageSource = getTransparentBitmapImageSource(context) - } + val imageSource = readableMapToImageSource(source, includeSize = false) tmpSources.add(imageSource) } else { for (idx in 0 until sources.size()) { val source = sources.getMap(idx) ?: continue - val cacheControl = computeCacheControl(source.getString("cache")) - val uri = source.getString("uri") - val isForceCached = if (source.hasKey("isForceCached")) { - source.getBoolean("isForceCached") - } else { - false - } - val width = if (source.hasKey("width")) { - source.getDouble("width") - } else { - 0.0 - } - val height = if (source.hasKey("height")) { - source.getDouble("height") - } else { - 0.0 - } - var imageSource = - ImageSource( - context, - uri, - width, - height, - cacheControl, - isForceCached) - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(uri) - imageSource = getTransparentBitmapImageSource(context) - } + val imageSource = readableMapToImageSource(source, includeSize = true) tmpSources.add(imageSource) } } From 51e05332d9b59a894e3e756b3a5bd6dc9d225e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:10:27 +0100 Subject: [PATCH 04/10] change to soft assertion to not break current API --- .../react/views/image/ReactImageManager.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index 41640e7ed182df..30fd899bb82f1c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -139,13 +139,11 @@ public constructor( return } val imageSource = view.readableMapToImageSource(source, false) - if (!BuildConfig.DEBUG && !imageSource.isResource) { - throw ReactNoCrashSoftException( - "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}", - ) - } + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}") - view.setDefaultSource(imageSource.uri.toString()) + view.setDefaultSource(imageSource.source) } // In JS this is Image.props.loadingIndicatorSource.uri @@ -156,11 +154,10 @@ public constructor( return } val imageSource = view.readableMapToImageSource(source, false) - if (!BuildConfig.DEBUG && !imageSource.isResource) { - throw ReactNoCrashSoftException( - "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}", - ) - } + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}") + view.setLoadingIndicatorSource(imageSource.uri.toString()) } From 216af114067d6ca41d99657f70c32b200b604adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:19:55 +0100 Subject: [PATCH 05/10] cleanup --- .../react/views/image/ReactImageManager.kt | 25 +----- .../react/views/image/ReactImageView.kt | 77 ++++++++++++------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index 30fd899bb82f1c..34b708feda967a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -12,11 +12,8 @@ import android.graphics.PorterDuff import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder -import com.facebook.react.BuildConfig -import com.facebook.react.bridge.ReactNoCrashSoftException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.ReactConstants import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -134,31 +131,13 @@ public constructor( @ReactProp(name = "defaultSource") public fun setDefaultSource(view: ReactImageView, source: ReadableMap?) { - if (source == null) { - view.setDefaultSource(null) - return - } - val imageSource = view.readableMapToImageSource(source, false) - SoftAssertions.assertCondition( - !BuildConfig.DEBUG && !imageSource.isResource, - "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}") - - view.setDefaultSource(imageSource.source) + view.setDefaultSource(source) } // In JS this is Image.props.loadingIndicatorSource.uri @ReactProp(name = "loadingIndicatorSrc") public fun setLoadingIndicatorSource(view: ReactImageView, source: ReadableMap?) { - if (source == null) { - view.setLoadingIndicatorSource(null) - return - } - val imageSource = view.readableMapToImageSource(source, false) - SoftAssertions.assertCondition( - !BuildConfig.DEBUG && !imageSource.isResource, - "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}") - - view.setLoadingIndicatorSource(imageSource.uri.toString()) + view.setLoadingIndicatorSource(source) } @ReactProp(name = "borderColor", customType = "Color") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 29f677346d9ccc..da420580cac1d5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -45,9 +45,11 @@ import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.ImageRequest.RequestLevel import com.facebook.imagepipeline.request.ImageRequestBuilder import com.facebook.imagepipeline.request.Postprocessor +import com.facebook.react.BuildConfig import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.build.ReactBuildConfig @@ -272,30 +274,6 @@ public class ReactImageView( } } - // todo: this must live elsewhere i think - public fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { - val cacheControl = computeCacheControl(source.getString("cache")) - val uri = source.getString("uri") - var imageSource = if (includeSize) { - ImageSource( - context, - uri, - source.getDouble("width"), - source.getDouble("height"), - cacheControl, - ) - } else { - ImageSource(context, uri, cacheControl = cacheControl) - } - - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(uri) - imageSource = getTransparentBitmapImageSource(context) - } - - return imageSource - } - public fun setSource(sources: ReadableArray?) { val tmpSources = mutableListOf() @@ -342,16 +320,36 @@ public class ReactImageView( } } - public fun setDefaultSource(name: String?) { - val newDefaultDrawable = ResourceDrawableIdHelper.getResourceDrawable(context, name) + public fun setDefaultSource(source: ReadableMap?) { + var newDefaultDrawable: Drawable? = null + if (source != null) { + val imageSource = readableMapToImageSource(source, false) + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}" + ) + + newDefaultDrawable = ResourceDrawableIdHelper.getResourceDrawable(context, imageSource.source) + } + if (defaultImageDrawable != newDefaultDrawable) { defaultImageDrawable = newDefaultDrawable isDirty = true } } - public fun setLoadingIndicatorSource(name: String?) { - val drawable = ResourceDrawableIdHelper.getResourceDrawable(context, name) + public fun setLoadingIndicatorSource(source: ReadableMap?) { + var drawable: Drawable? = null + if (source != null) { + val imageSource = readableMapToImageSource(source, false) + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}" + ) + + drawable = ResourceDrawableIdHelper.getResourceDrawable(context, imageSource.source) + } + val newLoadingIndicatorSource = drawable?.let { AutoRotateDrawable(it, 1000) } if (loadingImageDrawable != newLoadingIndicatorSource) { loadingImageDrawable = newLoadingIndicatorSource @@ -568,6 +566,29 @@ public class ReactImageView( private val isTiled: Boolean get() = tileMode != TileMode.CLAMP + private fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { + val cacheControl = computeCacheControl(source.getString("cache")) + val uri = source.getString("uri") + var imageSource = if (includeSize) { + ImageSource( + context, + uri, + source.getDouble("width"), + source.getDouble("height"), + cacheControl, + ) + } else { + ImageSource(context, uri, cacheControl = cacheControl) + } + + if (Uri.EMPTY == imageSource.uri) { + warnImageSource(uri) + imageSource = getTransparentBitmapImageSource(context) + } + + return imageSource + } + private fun setSourceImage() { imageSource = null if (sources.isEmpty()) { From d5e491f5a9f93e6b4de25a7d3880fb21a1d70850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:43:58 +0100 Subject: [PATCH 06/10] update flow type for loadingIndicatorSrc --- .../react-native/Libraries/Image/ImageViewNativeComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js index 6ad7da9a8db642..f40dfd68c2af61 100644 --- a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js +++ b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js @@ -42,7 +42,7 @@ type ImageHostComponentProps = $ReadOnly<{ | ?$ReadOnlyArray>, headers?: ?{[string]: string}, defaultSource?: ?ImageSource | ?string, - loadingIndicatorSrc?: ?string, + loadingIndicatorSrc?: ?ImageSource | ?string, }>; interface NativeCommands { From 58a70d29699bdf7d5add917ed40b1542b82f99bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 21 Jan 2026 14:03:46 +0100 Subject: [PATCH 07/10] fix(android): fix crash with props 2.0 diffing when setting text props to null --- .../components/text/BaseTextProps.cpp | 28 +++++++++---------- .../components/text/ParagraphProps.cpp | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index ee1f1a8327aefb..2239af4a23a497 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -426,19 +426,19 @@ void BaseTextProps::appendTextAttributesProps( if (textAttributes.fontWeight != oldProps->textAttributes.fontWeight) { result["fontWeight"] = textAttributes.fontWeight.has_value() ? toString(textAttributes.fontWeight.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.fontStyle != oldProps->textAttributes.fontStyle) { result["fontStyle"] = textAttributes.fontStyle.has_value() ? toString(textAttributes.fontStyle.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.fontVariant != oldProps->textAttributes.fontVariant) { result["fontVariant"] = textAttributes.fontVariant.has_value() ? toString(textAttributes.fontVariant.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.allowFontScaling != @@ -458,7 +458,7 @@ void BaseTextProps::appendTextAttributesProps( oldProps->textAttributes.dynamicTypeRamp) { result["dynamicTypeRamp"] = textAttributes.dynamicTypeRamp.has_value() ? toString(textAttributes.dynamicTypeRamp.value()) - : nullptr; + : folly::dynamic(nullptr); } if (!floatEquality( @@ -470,7 +470,7 @@ void BaseTextProps::appendTextAttributesProps( if (textAttributes.textTransform != oldProps->textAttributes.textTransform) { result["textTransform"] = textAttributes.textTransform.has_value() ? toString(textAttributes.textTransform.value()) - : nullptr; + : folly::dynamic(nullptr); } if (!floatEquality( @@ -481,7 +481,7 @@ void BaseTextProps::appendTextAttributesProps( if (textAttributes.alignment != oldProps->textAttributes.alignment) { result["textAlign"] = textAttributes.alignment.has_value() ? toString(textAttributes.alignment.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.baseWritingDirection != @@ -489,7 +489,7 @@ void BaseTextProps::appendTextAttributesProps( result["baseWritingDirection"] = textAttributes.baseWritingDirection.has_value() ? toString(textAttributes.baseWritingDirection.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.lineBreakStrategy != @@ -497,13 +497,13 @@ void BaseTextProps::appendTextAttributesProps( result["lineBreakStrategyIOS"] = textAttributes.lineBreakStrategy.has_value() ? toString(textAttributes.lineBreakStrategy.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.lineBreakMode != oldProps->textAttributes.lineBreakMode) { result["lineBreakModeIOS"] = textAttributes.lineBreakMode.has_value() ? toString(textAttributes.lineBreakMode.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.textDecorationColor != @@ -516,7 +516,7 @@ void BaseTextProps::appendTextAttributesProps( result["textDecorationLine"] = textAttributes.textDecorationLineType.has_value() ? toString(textAttributes.textDecorationLineType.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.textDecorationStyle != @@ -524,14 +524,14 @@ void BaseTextProps::appendTextAttributesProps( result["textDecorationStyle"] = textAttributes.textDecorationStyle.has_value() ? toString(textAttributes.textDecorationStyle.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.textShadowOffset != oldProps->textAttributes.textShadowOffset) { result["textShadowOffset"] = textAttributes.textShadowOffset.has_value() ? toDynamic(textAttributes.textShadowOffset.value()) - : nullptr; + : folly::dynamic(nullptr); } if (!floatEquality( @@ -561,13 +561,13 @@ void BaseTextProps::appendTextAttributesProps( oldProps->textAttributes.accessibilityRole) { result["accessibilityRole"] = textAttributes.accessibilityRole.has_value() ? toString(textAttributes.accessibilityRole.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.role != oldProps->textAttributes.role) { result["role"] = textAttributes.role.has_value() ? toString(textAttributes.role.value()) - : nullptr; + : folly::dynamic(nullptr); } if (textAttributes.opacity != oldProps->textAttributes.opacity) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp index 2ae67fa33dfd76..3de86dfb2da0f5 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp @@ -230,7 +230,7 @@ folly::dynamic ParagraphProps::getDiffProps(const Props* prevProps) const { result["textAlignVertical"] = paragraphAttributes.textAlignVertical.has_value() ? toString(paragraphAttributes.textAlignVertical.value()) - : nullptr; + : folly::dynamic(nullptr);; } if (isSelectable != oldProps->isSelectable) { From 26965f9b1aa1d19fce33dafc31fcc2d50ed641fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 26 Jan 2026 20:15:46 +0100 Subject: [PATCH 08/10] wip --- .../react/fabric/FabricUIManager.java | 2 +- .../react/fabric/FabricUIManagerBinding.kt | 2 +- .../facebook/react/runtime/ReactHostImpl.kt | 4 +- .../facebook/react/uiapp/RNTesterActivity.kt | 103 ++++++------------ .../react/uiapp/RNTesterApplication.kt | 2 +- .../android/app/src/main/jni/OnLoad.cpp | 18 +++ 6 files changed, 56 insertions(+), 75 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 88b97458b90760..b72afb96e00e74 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -170,7 +170,7 @@ public class FabricUIManager FabricSoLoader.staticInit(); } - @Nullable private FabricUIManagerBinding mBinding; + @Nullable public FabricUIManagerBinding mBinding; private final ReactApplicationContext mReactApplicationContext; private final MountingManager mMountingManager; private final FabricEventDispatcher mEventDispatcher; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index f22661f1d7e15f..5f97a49b9aed4c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -18,7 +18,7 @@ import com.facebook.react.uimanager.PixelUtil.getDisplayMetricDensity @DoNotStrip @SuppressLint("MissingNativeLoadLibrary") -internal class FabricUIManagerBinding : HybridClassBase() { +public class FabricUIManagerBinding : HybridClassBase() { init { initHybrid() } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 39399a66744f39..ec3949e24aff8f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -955,8 +955,8 @@ public class ReactHostImpl( // as TurboModuleManager will handle any concurrent access instance.initializeEagerTurboModules() - log(method, "Loading JS Bundle") - instance.loadJSBundle(bundleLoader) +// log(method, "Loading JS Bundle") +// instance.loadJSBundle(bundleLoader) log(method, "Calling DevSupportManagerBase.onNewReactContextCreated(reactContext)") devSupportManager.onNewReactContextCreated(reactContext) diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt index a40994fd3ecf9d..b488da218ac810 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt @@ -10,6 +10,7 @@ package com.facebook.react.uiapp import android.content.res.Configuration import android.graphics.Color import android.os.Bundle +import android.util.Log import android.view.View import android.widget.FrameLayout import androidx.core.graphics.drawable.toDrawable @@ -17,91 +18,53 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.facebook.react.FBRNTesterEndToEndHelper import com.facebook.react.ReactActivity +import com.facebook.react.ReactInstanceEventListener +import com.facebook.react.bridge.ReactContext import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.fabric.FabricUIManager +import com.facebook.react.fabric.FabricUIManagerBinding +import com.facebook.react.uimanager.common.UIManagerType import java.io.FileDescriptor import java.io.PrintWriter -internal class RNTesterActivity : ReactActivity() { - class RNTesterActivityDelegate(val activity: ReactActivity, mainComponentName: String) : - DefaultReactActivityDelegate(activity, mainComponentName, fabricEnabled) { - private val PARAM_ROUTE = "route" - private lateinit var initialProps: Bundle - - override fun onCreate(savedInstanceState: Bundle?) { - // Get remote param before calling super which uses it - val bundle = activity.intent?.extras - - if (bundle != null && bundle.containsKey(PARAM_ROUTE)) { - val routeUri = "rntester://example/${bundle.getString(PARAM_ROUTE)}Example" - initialProps = Bundle().apply { putString("exampleFromAppetizeParams", routeUri) } - } - FBRNTesterEndToEndHelper.onCreate(activity.application) - super.onCreate(savedInstanceState) - } - - override fun getLaunchOptions() = - if (this::initialProps.isInitialized) initialProps else Bundle() - } - - // set background color so it will show below transparent system bars on forced edge-to-edge - private fun maybeUpdateBackgroundColor() { - val isDarkMode = - resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES - - val color = - if (isDarkMode) { - Color.rgb(11, 6, 0) - } else { - Color.rgb(243, 248, 255) - } - - window?.setBackgroundDrawable(color.toDrawable()) - } +/** + * Reproduction case: + * A-B-C commit/mount chain, but commit B gets skipped + * What does skipped mean? + * - Does it mean the shadow tree commit is skipped + * - Or does it mean that executing the mount gets skipped? + * + * What i basically need or want is manual access to the UIManager.cpp and control all operations from there 😈 + */ +internal class RNTesterActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener { - fullyDrawnReporter.addReporter() - maybeUpdateBackgroundColor() - - // register insets listener to update margins on the ReactRootView to avoid overlap w/ system - // bars - getReactDelegate()?.reactRootView?.let { rootView -> - val insetsType: Int = - WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + override fun onReactContextInitialized(context: ReactContext) { + // Now we should be able to get dirty! + val uiManager = UIManagerHelper.getUIManager(context, UIManagerType.FABRIC) + ?: throw IllegalStateException("Fabric UIManager not found") - val windowInsetsListener = { view: View, windowInsets: WindowInsetsCompat -> - val insets = windowInsets.getInsets(insetsType) + val fabricUIManager = uiManager as? FabricUIManager + ?: throw IllegalStateException("UIManager is not a FabricUIManager") - (view.layoutParams as FrameLayout.LayoutParams).apply { - setMargins(insets.left, insets.top, insets.right, insets.bottom) - } - - WindowInsetsCompat.CONSUMED + runTest(fabricUIManager) } - ViewCompat.setOnApplyWindowInsetsListener(rootView, windowInsetsListener) - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - // update background color on UI mode change - maybeUpdateBackgroundColor() + }) + + Log.d("HannoDebug", "RNTesterActivity onCreate"); } - override fun createReactActivityDelegate() = RNTesterActivityDelegate(this, mainComponentName) - override fun getMainComponentName() = "RNTesterApp" + override fun getMainComponentName() = "" - override fun dump( - prefix: String, - fd: FileDescriptor?, - writer: PrintWriter, - args: Array? - ) { - FBRNTesterEndToEndHelper.maybeDump(prefix, writer, args) + companion object { + @JvmStatic + external fun runTest(uiManager: FabricUIManager) } } diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index 0d9f7ff4146904..f4f7f603e7123d 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -49,7 +49,7 @@ internal class RNTesterApplication : Application(), ReactApplication { public override fun getBundleAssetName(): String = BuildConfig.BUNDLE_ASSET_NAME - override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + override fun getUseDeveloperSupport(): Boolean = false public override fun getPackages(): List { return listOf( diff --git a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp index 803d4bbc50c404..abe360a0d8b478 100644 --- a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp +++ b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #ifdef REACT_NATIVE_APP_CODEGEN_HEADER #include REACT_NATIVE_APP_CODEGEN_HEADER @@ -24,6 +25,22 @@ namespace facebook { namespace react { +class JRNtesterActivity : public jni::HybridClass { + public: + static constexpr const char* kJavaDescriptor = + "Lcom/facebook/react/uiapp/RNtesterActivity;"; + + void runTest(jni::alias_ref& javaUIManager) { + + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("runTest", JRNtesterActivity::runTest), + }); + } +}; + void registerComponents( std::shared_ptr registry) { #ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION @@ -75,5 +92,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { registerComponentDescriptorsFromEntryPoint = &facebook::react::registerComponents; facebook::react::SampleTurboModuleJSIBindings::registerNatives(); + facebook::react::JRNtesterActivity::registerNatives(); }); } From 09ac35c8c999f452767b395c6e4871d1c6a3af16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 30 Jan 2026 16:44:39 +0100 Subject: [PATCH 09/10] get ui mamnager --- .../react/fabric/FabricUIManagerBinding.kt | 2 +- .../com/facebook/react/uiapp/RNTesterActivity.kt | 15 +-------------- .../android/app/src/main/jni/CMakeLists.txt | 3 ++- .../rn-tester/android/app/src/main/jni/OnLoad.cpp | 13 ++++++++++++- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index 5f97a49b9aed4c..f22661f1d7e15f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -18,7 +18,7 @@ import com.facebook.react.uimanager.PixelUtil.getDisplayMetricDensity @DoNotStrip @SuppressLint("MissingNativeLoadLibrary") -public class FabricUIManagerBinding : HybridClassBase() { +internal class FabricUIManagerBinding : HybridClassBase() { init { initHybrid() } diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt index b488da218ac810..bde2a1b77d8ea3 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.kt @@ -7,27 +7,14 @@ package com.facebook.react.uiapp -import android.content.res.Configuration -import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.View -import android.widget.FrameLayout -import androidx.core.graphics.drawable.toDrawable -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import com.facebook.react.FBRNTesterEndToEndHelper import com.facebook.react.ReactActivity import com.facebook.react.ReactInstanceEventListener import com.facebook.react.bridge.ReactContext -import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled -import com.facebook.react.defaults.DefaultReactActivityDelegate -import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.fabric.FabricUIManager -import com.facebook.react.fabric.FabricUIManagerBinding +import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.common.UIManagerType -import java.io.FileDescriptor -import java.io.PrintWriter /** diff --git a/packages/rn-tester/android/app/src/main/jni/CMakeLists.txt b/packages/rn-tester/android/app/src/main/jni/CMakeLists.txt index 808c4ba3b7d60d..327603f0c97e3f 100644 --- a/packages/rn-tester/android/app/src/main/jni/CMakeLists.txt +++ b/packages/rn-tester/android/app/src/main/jni/CMakeLists.txt @@ -18,4 +18,5 @@ target_link_libraries(${CMAKE_PROJECT_NAME} sampleturbomodule) # RN Tester needs to link against the NativeCxxModuleExample target_link_libraries(${CMAKE_PROJECT_NAME} - nativecxxmoduleexample) + nativecxxmoduleexample + log) diff --git a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp index abe360a0d8b478..1427a4275fa924 100644 --- a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp +++ b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp @@ -14,6 +14,10 @@ #include #include #include +#include +#include + +#include #ifdef REACT_NATIVE_APP_CODEGEN_HEADER #include REACT_NATIVE_APP_CODEGEN_HEADER @@ -30,8 +34,15 @@ class JRNtesterActivity : public jni::HybridClass { static constexpr const char* kJavaDescriptor = "Lcom/facebook/react/uiapp/RNtesterActivity;"; - void runTest(jni::alias_ref& javaUIManager) { + void runTest(jni::alias_ref javaUIManager) { + FabricUIManagerBinding* uiManagerBinding = javaUIManager->getBinding(); + if (uiManagerBinding == nullptr) { + __android_log_print(ANDROID_LOG_ERROR, "RNtesterActivity", "FabricUIManagerBinding is null"); + return; + } + auto scheduler = uiManagerBinding->getScheduler(); + auto uiManager = scheduler->getUIManager(); } static void registerNatives() { From 5738d14458cd8a0cea31069ca7adf65b056940b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 30 Jan 2026 16:50:28 +0100 Subject: [PATCH 10/10] fix jni code --- .../android/app/src/main/jni/OnLoad.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp index 1427a4275fa924..e1c9dd8f5433e0 100644 --- a/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp +++ b/packages/rn-tester/android/app/src/main/jni/OnLoad.cpp @@ -29,25 +29,27 @@ namespace facebook { namespace react { -class JRNtesterActivity : public jni::HybridClass { - public: +struct JRNTesterActivity : public jni::JavaClass { static constexpr const char* kJavaDescriptor = - "Lcom/facebook/react/uiapp/RNtesterActivity;"; + "Lcom/facebook/react/uiapp/RNTesterActivity;"; - void runTest(jni::alias_ref javaUIManager) { + static void runTest( + jni::alias_ref, + jni::alias_ref javaUIManager) { FabricUIManagerBinding* uiManagerBinding = javaUIManager->getBinding(); if (uiManagerBinding == nullptr) { - __android_log_print(ANDROID_LOG_ERROR, "RNtesterActivity", "FabricUIManagerBinding is null"); + __android_log_print(ANDROID_LOG_ERROR, "RNTesterActivity", "FabricUIManagerBinding is null"); return; } auto scheduler = uiManagerBinding->getScheduler(); auto uiManager = scheduler->getUIManager(); + __android_log_print(ANDROID_LOG_INFO, "RNTesterActivity", "Successfully got UIManager!"); } static void registerNatives() { - registerHybrid({ - makeNativeMethod("runTest", JRNtesterActivity::runTest), + javaClassStatic()->registerNatives({ + makeNativeMethod("runTest", JRNTesterActivity::runTest), }); } }; @@ -103,6 +105,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { registerComponentDescriptorsFromEntryPoint = &facebook::react::registerComponents; facebook::react::SampleTurboModuleJSIBindings::registerNatives(); - facebook::react::JRNtesterActivity::registerNatives(); + facebook::react::JRNTesterActivity::registerNatives(); }); }