From 702c0b1b992432719c15051d76c805d4e9995999 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 10 Apr 2024 18:15:22 +0200 Subject: [PATCH] Fix layout issues on Android on the new architecture (#268) Co-authored-by: Tomek Zawadzki --- RNLiveMarkdown.podspec | 8 +- android/build.gradle | 12 + android/proguard-rules.pro | 1 + .../livemarkdown/CustomFabricUIManager.java | 47 ++++ .../livemarkdown/CustomMountingManager.java | 248 ++++++++++++++++++ .../livemarkdown/LiveMarkdownModule.java | 27 ++ .../livemarkdown/LiveMarkdownPackage.java | 33 ++- .../livemarkdown/MarkdownFontFamilySpan.java | 8 +- .../livemarkdown/MarkdownFontSizeSpan.java | 4 +- .../expensify/livemarkdown/MarkdownUtils.java | 8 +- android/src/main/new_arch/CMakeLists.txt | 82 ++++++ .../src/main/new_arch/MarkdownCommitHook.cpp | 173 ++++++++++++ .../src/main/new_arch/MarkdownCommitHook.h | 52 ++++ android/src/main/new_arch/NativeProxy.cpp | 38 +++ android/src/main/new_arch/NativeProxy.h | 37 +++ android/src/main/new_arch/OnLoad.cpp | 8 + .../src/main/new_arch/RNLiveMarkdownSpec.cpp | 44 ++++ .../src/main/new_arch/RNLiveMarkdownSpec.h | 37 +++ android/src/newarch/NativeProxy.java | 24 ++ .../oldarch/NativeLiveMarkdownModuleSpec.java | 37 +++ android/src/oldarch/NativeProxy.java | 9 + .../MarkdownShadowFamilyRegistry.cpp | 2 +- .../MarkdownShadowFamilyRegistry.h | 4 +- .../MarkdownTextInputDecoratorShadowNode.cpp | 2 +- .../MarkdownTextInputDecoratorShadowNode.h | 3 +- .../MarkdownTextInputDecoratorState.h | 13 + ...extInputDecoratorViewComponentDescriptor.h | 2 +- example/android/settings.gradle | 8 + example/package.json | 2 + .../@react-native+gradle-plugin+0.73.4.patch | 39 +++ example/patches/react-native+0.73.4.patch | 95 +++++++ ...MarkdownTextInputDecoratorComponentView.mm | 4 +- package.json | 1 + react-native.config.js | 12 + ...wnTextInputDecoratorViewNativeComponent.ts | 4 +- yarn.lock | 130 ++++++++- 36 files changed, 1233 insertions(+), 25 deletions(-) create mode 100644 android/proguard-rules.pro create mode 100644 android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java create mode 100644 android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java create mode 100644 android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java create mode 100644 android/src/main/new_arch/CMakeLists.txt create mode 100644 android/src/main/new_arch/MarkdownCommitHook.cpp create mode 100644 android/src/main/new_arch/MarkdownCommitHook.h create mode 100644 android/src/main/new_arch/NativeProxy.cpp create mode 100644 android/src/main/new_arch/NativeProxy.h create mode 100644 android/src/main/new_arch/OnLoad.cpp create mode 100644 android/src/main/new_arch/RNLiveMarkdownSpec.cpp create mode 100644 android/src/main/new_arch/RNLiveMarkdownSpec.h create mode 100644 android/src/newarch/NativeProxy.java create mode 100644 android/src/oldarch/NativeLiveMarkdownModuleSpec.java create mode 100644 android/src/oldarch/NativeProxy.java rename {ios => cpp/react/renderer/components/RNLiveMarkdownSpec}/MarkdownShadowFamilyRegistry.cpp (97%) rename {ios => cpp/react/renderer/components/RNLiveMarkdownSpec}/MarkdownShadowFamilyRegistry.h (91%) rename {ios => cpp/react/renderer/components/RNLiveMarkdownSpec}/MarkdownTextInputDecoratorShadowNode.cpp (95%) rename {ios => cpp/react/renderer/components/RNLiveMarkdownSpec}/MarkdownTextInputDecoratorShadowNode.h (95%) rename {ios => cpp/react/renderer/components/RNLiveMarkdownSpec}/MarkdownTextInputDecoratorState.h (56%) rename ios/MarkdownTextInputDecoratorComponentDescriptor.h => cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorViewComponentDescriptor.h (87%) create mode 100644 example/patches/@react-native+gradle-plugin+0.73.4.patch create mode 100644 example/patches/react-native+0.73.4.patch create mode 100644 react-native.config.js diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index a892205c1..a2d52c80d 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/expensify/react-native-live-markdown.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,cpp}" + s.source_files = "ios/**/*.{h,m,mm}" s.resources = "parser/react-native-live-markdown-parser.js" @@ -26,4 +26,10 @@ Pod::Spec.new do |s| "react/renderer/components/textinput/iostextinput", ]) end + + s.subspec "common" do |ss| + ss.source_files = "cpp/**/*.{cpp,h}" + ss.header_dir = "RNLiveMarkdown" + ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\"" } + end end diff --git a/android/build.gradle b/android/build.gradle index 86dcfd2e2..c1fe5935e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,6 +53,9 @@ android { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + consumerProguardFiles "proguard-rules.pro" + externalNativeBuild { cmake { arguments "-DANDROID_STL=c++_shared", "-DANDROID_TOOLCHAIN=clang" @@ -112,6 +115,15 @@ android { "**/libreactnativejni.so", ] } + + packagingOptions { + // For some reason gradle only complains about the duplicated version of librrc_root and libreact_render libraries + // while there are more libraries copied in intermediates folder of the lib build directory, we exclude + // only the ones that make the build fail (ideally we should only include libreanimated but we + // are only allowed to specify exlude patterns) + exclude "**/libreact_render*.so" + exclude "**/librrc_root.so" + } } repositories { diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 000000000..5b27a3935 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.expensify.livemarkdown.** { *; } diff --git a/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java b/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java new file mode 100644 index 000000000..c55b6c5c0 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java @@ -0,0 +1,47 @@ +package com.expensify.livemarkdown; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.react.fabric.mounting.MountingManager; +import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.uimanager.events.BatchEventDispatchedListener; + +import java.lang.reflect.Field; + +public class CustomFabricUIManager { + + public static FabricUIManager create(FabricUIManager source, ReadableMap markdownProps) { + Class uiManagerClass = source.getClass(); + + try { + Field mountingManagerField = uiManagerClass.getDeclaredField("mMountingManager"); + mountingManagerField.setAccessible(true); + + ReactApplicationContext reactContext = readPrivateField(source, "mReactApplicationContext"); + ViewManagerRegistry viewManagerRegistry = readPrivateField(source, "mViewManagerRegistry"); + BatchEventDispatchedListener batchEventDispatchedListener = readPrivateField(source, "mBatchEventDispatchedListener"); + MountingManager.MountItemExecutor mountItemExecutor = readPrivateField(source, "mMountItemExecutor"); + + FabricUIManager customFabricUIManager = new FabricUIManager(reactContext, viewManagerRegistry, batchEventDispatchedListener); + + mountingManagerField.set(customFabricUIManager, new CustomMountingManager(viewManagerRegistry, mountItemExecutor, reactContext, markdownProps)); + + return customFabricUIManager; + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("[LiveMarkdown] Cannot read data from FabricUIManager"); + } + } + + private static T readPrivateField(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + Class clazz = obj.getClass(); + + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + T value = (T) field.get(obj); + + return value; + } +} diff --git a/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java b/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java new file mode 100644 index 000000000..1b4381bc2 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java @@ -0,0 +1,248 @@ +package com.expensify.livemarkdown; + +import static com.facebook.react.views.text.TextAttributeProps.UNSET; + +import android.content.Context; +import android.content.res.AssetManager; +import android.text.BoringLayout; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextPaint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.mapbuffer.MapBuffer; +import com.facebook.react.fabric.mounting.MountingManager; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.views.text.TextAttributeProps; +import com.facebook.react.views.text.TextInlineViewPlaceholderSpan; +import com.facebook.react.views.text.TextLayoutManagerMapBuffer; +import com.facebook.yoga.YogaMeasureMode; +import com.facebook.yoga.YogaMeasureOutput; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class CustomMountingManager extends MountingManager { + private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true; + private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + + private MarkdownUtils markdownUtils; + + public CustomMountingManager( + @NonNull ViewManagerRegistry viewManagerRegistry, + @NonNull MountItemExecutor mountItemExecutor, + @NonNull Context context, + @NonNull ReadableMap decoratorProps) { + super(viewManagerRegistry, mountItemExecutor); + + AssetManager assetManager = context.getAssets(); + MarkdownUtils.maybeInitializeRuntime(assetManager); + + this.markdownUtils = new MarkdownUtils(assetManager); + this.markdownUtils.setMarkdownStyle(new MarkdownStyle(decoratorProps, context)); + } + + @Override + public long measureMapBuffer( + @NonNull ReactContext context, + @NonNull String componentName, + @NonNull MapBuffer attributedString, + @NonNull MapBuffer paragraphAttributes, + @Nullable MapBuffer state, + float width, + @NonNull YogaMeasureMode widthYogaMeasureMode, + float height, + @NonNull YogaMeasureMode heightYogaMeasureMode, + @Nullable float[] attachmentsPositions) { + + Spannable text = + TextLayoutManagerMapBuffer.getOrCreateSpannableForText(context, attributedString, null); + + if (text == null) { + return 0; + } + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY)); + boolean includeFontPadding = + paragraphAttributes.contains(TextLayoutManagerMapBuffer.PA_KEY_INCLUDE_FONT_PADDING) + ? paragraphAttributes.getBoolean(TextLayoutManagerMapBuffer.PA_KEY_INCLUDE_FONT_PADDING) + : DEFAULT_INCLUDE_FONT_PADDING; + int hyphenationFrequency = + TextAttributeProps.getHyphenationFrequency( + paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_HYPHENATION_FREQUENCY)); + + // StaticLayout returns wrong metrics for the last line if it's empty, add something to the + // last line so it's measured correctly + if (text.toString().endsWith("\n")) { + SpannableStringBuilder sb = new SpannableStringBuilder(text); + sb.append("I"); + + text = sb; + } + + markdownUtils.applyMarkdownFormatting((SpannableStringBuilder)text); + + BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance); + + Class mapBufferClass = TextLayoutManagerMapBuffer.class; + try { + Method createLayoutMethod = mapBufferClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class); + createLayoutMethod.setAccessible(true); + + Layout layout = (Layout)createLayoutMethod.invoke( + null, + text, + boring, + width, + widthYogaMeasureMode, + includeFontPadding, + textBreakStrategy, + hyphenationFrequency); + + int maximumNumberOfLines = + paragraphAttributes.contains(TextLayoutManagerMapBuffer.PA_KEY_MAX_NUMBER_OF_LINES) + ? paragraphAttributes.getInt(TextLayoutManagerMapBuffer.PA_KEY_MAX_NUMBER_OF_LINES) + : UNSET; + + int calculatedLineCount = + maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + ? layout.getLineCount() + : Math.min(maximumNumberOfLines, layout.getLineCount()); + + // Instead of using `layout.getWidth()` (which may yield a significantly larger width for + // text that is wrapping), compute width using the longest line. + float calculatedWidth = 0; + if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) { + calculatedWidth = width; + } else { + for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) { + boolean endsWithNewLine = + text.length() > 0 && text.charAt(layout.getLineEnd(lineIndex) - 1) == '\n'; + float lineWidth = + endsWithNewLine ? layout.getLineMax(lineIndex) : layout.getLineWidth(lineIndex); + if (lineWidth > calculatedWidth) { + calculatedWidth = lineWidth; + } + } + if (widthYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedWidth > width) { + calculatedWidth = width; + } + } + + // Android 11+ introduces changes in text width calculation which leads to cases + // where the container is measured smaller than text. Math.ceil prevents it + // See T136756103 for investigation + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.Q) { + calculatedWidth = (float) Math.ceil(calculatedWidth); + } + + float calculatedHeight = height; + if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) { + calculatedHeight = layout.getLineBottom(calculatedLineCount - 1); + if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) { + calculatedHeight = height; + } + } + + // Calculate the positions of the attachments (views) that will be rendered inside the + // Spanned Text. The following logic is only executed when a text contains views inside. + // This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method). + int attachmentIndex = 0; + int lastAttachmentFoundInSpan; + for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) { + lastAttachmentFoundInSpan = + text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class); + TextInlineViewPlaceholderSpan[] placeholders = + text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class); + for (TextInlineViewPlaceholderSpan placeholder : placeholders) { + int start = text.getSpanStart(placeholder); + int line = layout.getLineForOffset(start); + boolean isLineTruncated = layout.getEllipsisCount(line) > 0; + // This truncation check works well on recent versions of Android (tested on 5.1.1 and + // 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on + // Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the + // first thing to be truncated. + if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line)) + || start >= layout.getLineEnd(line)) { + float placeholderWidth = placeholder.getWidth(); + float placeholderHeight = placeholder.getHeight(); + // Calculate if the direction of the placeholder character is Right-To-Left. + boolean isRtlChar = layout.isRtlCharAt(start); + boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT; + float placeholderLeftPosition; + // There's a bug on Samsung devices where calling getPrimaryHorizontal on + // the last offset in the layout will result in an endless loop. Work around + // this bug by avoiding getPrimaryHorizontal in that case. + if (start == text.length() - 1) { + boolean endsWithNewLine = + text.length() > 0 && text.charAt(layout.getLineEnd(line) - 1) == '\n'; + float lineWidth = endsWithNewLine ? layout.getLineMax(line) : layout.getLineWidth(line); + placeholderLeftPosition = + isRtlParagraph + // Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns + // incorrect + // values when the paragraph is RTL and `setSingleLine(true)`. + ? calculatedWidth - lineWidth + : layout.getLineRight(line) - placeholderWidth; + } else { + // The direction of the paragraph may not be exactly the direction the string is + // heading + // in at the + // position of the placeholder. So, if the direction of the character is the same + // as the + // paragraph + // use primary, secondary otherwise. + boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar; + placeholderLeftPosition = + characterAndParagraphDirectionMatch + ? layout.getPrimaryHorizontal(start) + : layout.getSecondaryHorizontal(start); + if (isRtlParagraph) { + // Adjust `placeholderLeftPosition` to work around an Android bug. + // The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout + // methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and + // `getLineRight` return incorrect values. Their return values seem to be off + // by the same number of pixels so subtracting these values cancels out the + // error. + // + // The result is equivalent to bugless versions of + // `getPrimaryHorizontal`/`getSecondaryHorizontal`. + placeholderLeftPosition = + calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition); + } + if (isRtlChar) { + placeholderLeftPosition -= placeholderWidth; + } + } + // Vertically align the inline view to the baseline of the line of text. + float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight; + int attachmentPosition = attachmentIndex * 2; + + // The attachment array returns the positions of each of the attachments as + attachmentsPositions[attachmentPosition] = + PixelUtil.toDIPFromPixel(placeholderTopPosition); + attachmentsPositions[attachmentPosition + 1] = + PixelUtil.toDIPFromPixel(placeholderLeftPosition); + attachmentIndex++; + } + } + } + + float widthInSP = PixelUtil.toDIPFromPixel(calculatedWidth); + float heightInSP = PixelUtil.toDIPFromPixel(calculatedHeight); + + return YogaMeasureOutput.make(widthInSP, heightInSP); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java new file mode 100644 index 000000000..ed1428b82 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java @@ -0,0 +1,27 @@ +package com.expensify.livemarkdown; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.UIManager; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.common.UIManagerType; + +public class LiveMarkdownModule extends NativeLiveMarkdownModuleSpec { + private NativeProxy mNativeProxy; + public LiveMarkdownModule(ReactApplicationContext reactContext) { + super(reactContext); + + this.mNativeProxy = new NativeProxy(); + } + + @Override + public boolean install() { + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + FabricUIManager uiManager = + (FabricUIManager) UIManagerHelper.getUIManager(getReactApplicationContext(), UIManagerType.FABRIC); + mNativeProxy.createCommitHook(uiManager); + } + + return true; + } +} diff --git a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java index c1c8fece8..9f55fc873 100644 --- a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java +++ b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java @@ -1,15 +1,22 @@ package com.expensify.livemarkdown; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.facebook.react.ReactPackage; +import com.facebook.react.TurboReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; -public class LiveMarkdownPackage implements ReactPackage { +public class LiveMarkdownPackage extends TurboReactPackage { @Override public List createViewManagers(ReactApplicationContext reactContext) { List viewManagers = new ArrayList<>(); @@ -18,7 +25,27 @@ public List createViewManagers(ReactApplicationContext reactContext } @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.emptyList(); + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return new ReactModuleInfoProvider() { + @Override + public Map getReactModuleInfos() { + return Map.of(LiveMarkdownModule.NAME, new ReactModuleInfo( + LiveMarkdownModule.NAME, + LiveMarkdownModule.class.getName(), + false, // canOverrideExistingModule + false, // needsEagerInit + false, // isCxxModule + true // isTurboModule + )); + } + }; + } + @Nullable + @Override + public NativeModule getModule(@NonNull String s, @NonNull ReactApplicationContext reactApplicationContext) { + if (s.equals(LiveMarkdownModule.NAME)) { + return new LiveMarkdownModule(reactApplicationContext); + } + return null; } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFontFamilySpan.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFontFamilySpan.java index facecb82b..b9e71dd06 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFontFamilySpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFontFamilySpan.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; +import com.facebook.react.common.assets.ReactFontManager.TypefaceStyle; import com.facebook.react.views.text.ReactFontManager; public class MarkdownFontFamilySpan extends MetricAffectingSpan implements MarkdownSpan { @@ -31,7 +32,12 @@ public void updateDrawState(TextPaint tp) { } private void apply(@NonNull TextPaint textPaint) { - int style = textPaint.getTypeface().getStyle(); + int style = TypefaceStyle.NORMAL; + if (textPaint.getTypeface() != null) { + style = textPaint.getTypeface().getStyle(); + } else { + style = TypefaceStyle.NORMAL; + } Typeface typeface = ReactFontManager.getInstance().getTypeface(mFontFamily, style, mAssetManager); textPaint.setTypeface(typeface); textPaint.setFlags(textPaint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG); diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFontSizeSpan.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFontSizeSpan.java index 6e3c1b93f..25d4dadcb 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFontSizeSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFontSizeSpan.java @@ -2,8 +2,10 @@ import android.text.style.AbsoluteSizeSpan; +import com.facebook.react.uimanager.PixelUtil; + public class MarkdownFontSizeSpan extends AbsoluteSizeSpan implements MarkdownSpan { public MarkdownFontSizeSpan(float fontSize) { - super((int) fontSize, true); + super((int) PixelUtil.toPixelFromDIP(fontSize), false); } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index 971f8691a..69dea58a4 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -29,9 +29,7 @@ public class MarkdownUtils { private static boolean IS_RUNTIME_INITIALIZED = false; - @ThreadConfined(UI) - public static void maybeInitializeRuntime(AssetManager assetManager) { - UiThreadUtil.assertOnUiThread(); + public static synchronized void maybeInitializeRuntime(AssetManager assetManager) { if (IS_RUNTIME_INITIALIZED) { return; } @@ -50,9 +48,7 @@ public static void maybeInitializeRuntime(AssetManager assetManager) { private static native void nativeInitializeRuntime(String code); - @ThreadConfined(UI) - private static String parseMarkdown(String input) { - UiThreadUtil.assertOnUiThread(); + private synchronized static String parseMarkdown(String input) { return nativeParseMarkdown(input); } diff --git a/android/src/main/new_arch/CMakeLists.txt b/android/src/main/new_arch/CMakeLists.txt new file mode 100644 index 000000000..f5dfedf7d --- /dev/null +++ b/android/src/main/new_arch/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +set(LIB_LITERAL RNLiveMarkdownSpec) +set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL}) + +set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..) +set(LIB_CPP_DIR ${LIB_ANDROID_DIR}/../cpp) +set(LIB_CUSTOM_SOURCES_DIR ${LIB_CPP_DIR}/react/renderer/components/${LIB_LITERAL}) +set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) +set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL}) + +file(GLOB LIB_MODULE_SRCS CONFIGURE_DEPENDS *.cpp) +file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS ${LIB_CUSTOM_SOURCES_DIR}/*.cpp) +file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp) + +set(RN_DIR ${LIB_ANDROID_DIR}/../example/node_modules/react-native) + +add_library( + ${LIB_TARGET_NAME} + SHARED + ${LIB_MODULE_SRCS} + ${LIB_CUSTOM_SRCS} + ${LIB_CODEGEN_SRCS} +) + +target_include_directories( + ${LIB_TARGET_NAME} + PUBLIC + . + ${LIB_ANDROID_GENERATED_JNI_DIR} + ${LIB_ANDROID_GENERATED_COMPONENTS_DIR} + ${LIB_CPP_DIR} +) + +find_package(ReactAndroid REQUIRED CONFIG) + +target_link_libraries( + ${LIB_TARGET_NAME} + ReactAndroid::rrc_text + ReactAndroid::rrc_textinput + ReactAndroid::react_render_textlayoutmanager + ReactAndroid::react_render_imagemanager + fabricjni + fbjni + folly_runtime + glog + jsi + react_codegen_rncore + react_debug + react_nativemodule_core + react_render_core + react_render_debug + react_render_graphics + react_render_mapbuffer + ReactAndroid::react_render_uimanager + ReactAndroid::react_render_scheduler + react_utils + runtimeexecutor + rrc_view + turbomodulejsijni + yoga + android + log +) + +target_compile_options( + ${LIB_TARGET_NAME} + PRIVATE + -DLOG_TAG=\"ReactNative\" + -fexceptions + -frtti + -Wall + -std=c++20 +) + +target_include_directories( + ${CMAKE_PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) \ No newline at end of file diff --git a/android/src/main/new_arch/MarkdownCommitHook.cpp b/android/src/main/new_arch/MarkdownCommitHook.cpp new file mode 100644 index 000000000..9375bb48c --- /dev/null +++ b/android/src/main/new_arch/MarkdownCommitHook.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include + +#include "MarkdownCommitHook.h" +#include "react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.h" + +using namespace facebook; +using namespace react; + +namespace livemarkdown { + +MarkdownCommitHook::MarkdownCommitHook( + jni::global_ref + fabricUIManager) + : fabricUIManager_(fabricUIManager), + uiManager_( + fabricUIManager->getBinding()->getScheduler()->getUIManager()) { + uiManager_->registerCommitHook(*this); +} + +MarkdownCommitHook::~MarkdownCommitHook() noexcept { + uiManager_->unregisterCommitHook(*this); +} + +RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( + ShadowTree const &, RootShadowNode::Shared const &, + RootShadowNode::Unshared const &newRootShadowNode) noexcept { + auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); + + // A preface to why we do the weird thing below: + // On the new architecture there are two ways of measuring text on Android: by + // passing a cache key, or by passing a measured text with attributes by a map + // buffer. We could implement both, but that would increase the complexity of + // the code and duplication between here and RN core, so we implement + // measurement for map buffers since it's the independent one, and force RN to + // use this path every time. It shouldn't have a negative performance impact, + // since there is a cpp cache layer anyway. + // + // AndroidTextInputShadowNode is closed pretty tightly, but there's a one + // place where we can insert ourselves (well, there are two but we really + // shouldn't mess with vtable). The path to measurement looks like this: + // AndroidTextInputShadowNode::measureContent -> + // TextLayoutManager::measureAndroidComponentMapBuffer + // -> (jni) -> FabricUIManager::measureMapBuffer -> + // MountingManager::measureMapBuffer + // We cannot modify the shadow node directly, but we can replace its + // TextLayoutManager. Literally every method it has is linked statically so we + // cannot override anything, but we can replace its ContextContainer where a + // jni reference to the FabricUIManager is stored. Now `measureMapBuffer` is + // private in the ui manager, but the only thing it does is calling + // `measureMapBuffer` on MountingManager where it's public. At this point the + // path forward is clear: we make a custom MountingManager that will perform + // our measurement, then create a custom FabricUIManager which will direct + // measurement to our MountingManager. Then we only need to create the + // ContextContainer with our FabricUIManager, create the TextLayoutManager + // with the newly created ContextContainer and replace the pointer to + // TextLayoutManager inside the AndroidTextInputShadowNode. + + // In order to properly apply markdown formatting to the text input, we need + // to update the TextInputShadowNode's state to reset the cache key and update + // its TextLayoutManager reference, but we only have access to the + // ShadowNodeFamilies of the decorator components. We also know that a + // markdown decorator is always preceded with the TextInput to decorate, so we + // need to take the sibling. + std::vector nodesToUpdate; + MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate]( + ShadowNodeFamily::Shared + family) { + // get the path from the root to the node from the decorator family + const auto ancestors = family->getAncestors(*rootNode); + + if (!ancestors.empty()) { + auto &parentNode = ancestors.back().first.get(); + auto index = ancestors.back().second; + + // this is node represented by one of the registered families and since we + // only register markdown decorator shadow families, static casting should + // be safe here + const auto &decoratorNode = + std::static_pointer_cast( + parentNode.getChildren().at(index)); + // text input always precedes the decorator component + const auto &previousSibling = parentNode.getChildren().at(index - 1); + + if (const auto &textInputNode = + std::dynamic_pointer_cast( + previousSibling)) { + // store the pair of text input and decorator to update in the next step + // we need both, decorator to get markdown style and text input to + // update it + nodesToUpdate.push_back({ + textInputNode, + decoratorNode, + }); + } + } + }); + + for (const auto &nodes : nodesToUpdate) { + const auto &textInputState = + *std::static_pointer_cast>( + nodes.textInput->getState()); + const auto &stateData = textInputState.getData(); + + rootNode = rootNode->cloneTree( + nodes.textInput->getFamily(), + [this, &stateData, &textInputState, &nodes](ShadowNode const &node) { + auto newStateData = + std::make_shared(stateData); + // force measurement of a map buffer + newStateData->cachedAttributedStringId = 0; + + // clone the text input with the new state + auto newNode = node.clone({ + .state = + std::make_shared>( + newStateData, textInputState), + }); + + const auto currentDecoratorProps = + nodes.decorator->getProps()->rawProps["markdownStyle"]; + + // if it's the first time we encounter this particular input or the + // markdown styles have changed (in which case we need to reset the + // cpp cache, to which we don't have a direct access), create a new + // instance of TextLayoutManager that will be performing measurement + // for this particular input + if (!textLayoutManagers_.contains(nodes.textInput->getTag()) || + previousDecoratorProps_[nodes.textInput->getTag()] != + currentDecoratorProps) { + static auto customUIManagerClass = jni::findClassStatic( + "com/expensify/livemarkdown/CustomFabricUIManager"); + static auto createCustomUIManager = + customUIManagerClass + ->getStaticMethod( + "create"); + + auto const decoratorPropsRNM = + ReadableNativeMap::newObjectCxxArgs(currentDecoratorProps); + auto const decoratorPropsRM = + jni::make_local(reinterpret_cast( + decoratorPropsRNM.get())); + + const auto customUIManager = jni::make_global(createCustomUIManager( + customUIManagerClass, fabricUIManager_.get(), + decoratorPropsRM.get())); + const ContextContainer::Shared contextContainer = + std::make_shared(); + contextContainer->insert("FabricUIManager", customUIManager); + textLayoutManagers_[nodes.textInput->getTag()] = + std::make_shared(contextContainer); + previousDecoratorProps_[nodes.textInput->getTag()] = + currentDecoratorProps; + } + + // we need to replace the TextLayoutManager every time to make sure + // the correct measurement code is run + auto newTextInputShadowNode = + std::static_pointer_cast(newNode); + newTextInputShadowNode->setTextLayoutManager( + textLayoutManagers_[nodes.textInput->getTag()]); + + return newNode; + }); + } + + return std::static_pointer_cast(rootNode); +} + +} // namespace livemarkdown diff --git a/android/src/main/new_arch/MarkdownCommitHook.h b/android/src/main/new_arch/MarkdownCommitHook.h new file mode 100644 index 000000000..6e736d7c6 --- /dev/null +++ b/android/src/main/new_arch/MarkdownCommitHook.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include + +#include "react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.h" + +using namespace facebook; +using namespace react; + +namespace livemarkdown { + +struct MarkdownTextInputDecoratorPair { + const std::shared_ptr textInput; + const std::shared_ptr decorator; +}; + +class MarkdownCommitHook : public UIManagerCommitHook { +public: + MarkdownCommitHook( + jni::global_ref + fabricUIManager); + + ~MarkdownCommitHook() noexcept override; + + void commitHookWasRegistered(UIManager const &) noexcept override {} + + void commitHookWasUnregistered(UIManager const &) noexcept override {} + + RootShadowNode::Unshared shadowTreeWillCommit( + ShadowTree const &shadowTree, + RootShadowNode::Shared const &oldRootShadowNode, + RootShadowNode::Unshared const &newRootShadowNode) noexcept override; + +private: + const jni::global_ref + fabricUIManager_; + const std::shared_ptr uiManager_; + std::unordered_map + textLayoutManagers_; + std::unordered_map + previousDecoratorProps_; +}; + +} // namespace livemarkdown diff --git a/android/src/main/new_arch/NativeProxy.cpp b/android/src/main/new_arch/NativeProxy.cpp new file mode 100644 index 000000000..c46297c47 --- /dev/null +++ b/android/src/main/new_arch/NativeProxy.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +#include + +#include "NativeProxy.h" + +namespace livemarkdown { + +using namespace facebook; +using namespace react; + +NativeProxy::NativeProxy(jni::alias_ref jThis) + : javaPart_(jni::make_global(jThis)) {} + +NativeProxy::~NativeProxy() {} + +void NativeProxy::registerNatives() { + registerHybrid( + {makeNativeMethod("initHybrid", NativeProxy::initHybrid), + makeNativeMethod("createCommitHook", NativeProxy::createCommitHook)}); +} + +void NativeProxy::createCommitHook( + jni::alias_ref + fabricUIManager) { + const auto &globalUIManager = jni::make_global(fabricUIManager); + + this->commitHook_ = std::make_shared(globalUIManager); +} + +jni::local_ref +NativeProxy::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); +} + +} // namespace livemarkdown diff --git a/android/src/main/new_arch/NativeProxy.h b/android/src/main/new_arch/NativeProxy.h new file mode 100644 index 000000000..a8b5f51d6 --- /dev/null +++ b/android/src/main/new_arch/NativeProxy.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include + +#include "MarkdownCommitHook.h" + +namespace livemarkdown { + +using namespace facebook; +using namespace facebook::jni; + +class NativeProxy : public jni::HybridClass { +public: + static auto constexpr kJavaDescriptor = + "Lcom/expensify/livemarkdown/NativeProxy;"; + static jni::local_ref + initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + ~NativeProxy(); + +private: + friend HybridBase; + jni::global_ref javaPart_; + std::shared_ptr commitHook_; + + explicit NativeProxy(jni::alias_ref jThis); + + void + createCommitHook(jni::alias_ref + fabricUIManager); +}; + +} // namespace livemarkdown diff --git a/android/src/main/new_arch/OnLoad.cpp b/android/src/main/new_arch/OnLoad.cpp new file mode 100644 index 000000000..4ebace0b2 --- /dev/null +++ b/android/src/main/new_arch/OnLoad.cpp @@ -0,0 +1,8 @@ +#include + +#include "NativeProxy.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize( + vm, [] { livemarkdown::NativeProxy::registerNatives(); }); +} diff --git a/android/src/main/new_arch/RNLiveMarkdownSpec.cpp b/android/src/main/new_arch/RNLiveMarkdownSpec.cpp new file mode 100644 index 000000000..2680f9934 --- /dev/null +++ b/android/src/main/new_arch/RNLiveMarkdownSpec.cpp @@ -0,0 +1,44 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJniCpp.js + */ + +#include "RNLiveMarkdownSpec.h" + +namespace facebook { +namespace react { + +static facebook::jsi::Value +__hostFunction_NativeLiveMarkdownModuleSpecJSI_install( + facebook::jsi::Runtime &rt, TurboModule &turboModule, + const facebook::jsi::Value *args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule) + .invokeJavaMethod(rt, BooleanKind, "install", "()Z", args, count, + cachedMethodId); +} + +NativeLiveMarkdownModuleSpecJSI::NativeLiveMarkdownModuleSpecJSI( + const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + methodMap_["install"] = + MethodMetadata{0, __hostFunction_NativeLiveMarkdownModuleSpecJSI_install}; +} + +std::shared_ptr +RNLiveMarkdownSpec_ModuleProvider(const std::string &moduleName, + const JavaTurboModule::InitParams ¶ms) { + if (moduleName == "LiveMarkdownModule") { + return std::make_shared(params); + } + return nullptr; +} + +} // namespace react +} // namespace facebook diff --git a/android/src/main/new_arch/RNLiveMarkdownSpec.h b/android/src/main/new_arch/RNLiveMarkdownSpec.h new file mode 100644 index 000000000..bd63e8aed --- /dev/null +++ b/android/src/main/new_arch/RNLiveMarkdownSpec.h @@ -0,0 +1,37 @@ + +/** + * This code was generated by + * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be + * lost once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +/** + * JNI C++ class for module 'NativeLiveMarkdownModule' + */ +class JSI_EXPORT NativeLiveMarkdownModuleSpecJSI : public JavaTurboModule { +public: + NativeLiveMarkdownModuleSpecJSI(const JavaTurboModule::InitParams ¶ms); +}; + +JSI_EXPORT +std::shared_ptr +RNLiveMarkdownSpec_ModuleProvider(const std::string &moduleName, + const JavaTurboModule::InitParams ¶ms); + +} // namespace react +} // namespace facebook diff --git a/android/src/newarch/NativeProxy.java b/android/src/newarch/NativeProxy.java new file mode 100644 index 000000000..8e307ed0c --- /dev/null +++ b/android/src/newarch/NativeProxy.java @@ -0,0 +1,24 @@ +package com.expensify.livemarkdown; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.fabric.FabricUIManager; +import com.facebook.soloader.SoLoader; + +public class NativeProxy { + static { + SoLoader.loadLibrary("react_codegen_RNLiveMarkdownSpec"); + } + + @DoNotStrip + @SuppressWarnings("unused") + private final HybridData mHybridData; + + public NativeProxy() { + mHybridData = initHybrid(); + } + + private native HybridData initHybrid(); + + public native void createCommitHook(FabricUIManager fabricUIManager); +} diff --git a/android/src/oldarch/NativeLiveMarkdownModuleSpec.java b/android/src/oldarch/NativeLiveMarkdownModuleSpec.java new file mode 100644 index 000000000..19fea0861 --- /dev/null +++ b/android/src/oldarch/NativeLiveMarkdownModuleSpec.java @@ -0,0 +1,37 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJavaSpec.js + * + * @nolint + */ + +package com.expensify.livemarkdown; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import javax.annotation.Nonnull; + +public abstract class NativeLiveMarkdownModuleSpec extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "LiveMarkdownModule"; + + public NativeLiveMarkdownModuleSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + + @ReactMethod(isBlockingSynchronousMethod = true) + @DoNotStrip + public abstract boolean install(); +} diff --git a/android/src/oldarch/NativeProxy.java b/android/src/oldarch/NativeProxy.java new file mode 100644 index 000000000..0a6f66ffc --- /dev/null +++ b/android/src/oldarch/NativeProxy.java @@ -0,0 +1,9 @@ +package com.expensify.livemarkdown; + +import com.facebook.react.bridge.UIManager; + +public class NativeProxy { + public void createCommitHook(UIManager uiManager) { + // no-op on the old arch + } +} diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.cpp similarity index 97% rename from ios/MarkdownShadowFamilyRegistry.cpp rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.cpp index d41d17267..1a0f4a746 100644 --- a/ios/MarkdownShadowFamilyRegistry.cpp +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.cpp @@ -1,4 +1,4 @@ -#ifdef RCT_NEW_ARCH_ENABLED +#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID) #include "MarkdownShadowFamilyRegistry.h" diff --git a/ios/MarkdownShadowFamilyRegistry.h b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.h similarity index 91% rename from ios/MarkdownShadowFamilyRegistry.h rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.h index 6d6195daa..f76c87d8b 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownShadowFamilyRegistry.h @@ -1,7 +1,7 @@ #pragma once -#ifdef RCT_NEW_ARCH_ENABLED +#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID) -#include +#include #include #include diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.cpp similarity index 95% rename from ios/MarkdownTextInputDecoratorShadowNode.cpp rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.cpp index 4ea2cc040..104363d3a 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.cpp @@ -1,4 +1,4 @@ -#ifdef RCT_NEW_ARCH_ENABLED +#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID) #include diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.h similarity index 95% rename from ios/MarkdownTextInputDecoratorShadowNode.h rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.h index 8dbca5256..294e0d3dc 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorShadowNode.h @@ -1,12 +1,11 @@ #pragma once -#ifdef RCT_NEW_ARCH_ENABLED +#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID) #include "MarkdownShadowFamilyRegistry.h" #include "MarkdownTextInputDecoratorState.h" #include #include #include -#include #include namespace facebook { diff --git a/ios/MarkdownTextInputDecoratorState.h b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h similarity index 56% rename from ios/MarkdownTextInputDecoratorState.h rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h index cf6b292fa..7590b9968 100644 --- a/ios/MarkdownTextInputDecoratorState.h +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h @@ -14,7 +14,20 @@ class JSI_EXPORT MarkdownTextInputDecoratorState final { const ShadowNodeFamily::Shared textInputFamily_) : decoratorFamily(textInputFamily_){}; +#ifdef ANDROID + MarkdownTextInputDecoratorState( + MarkdownTextInputDecoratorState const &previousState, folly::dynamic data) + : decoratorFamily(previousState.decoratorFamily){}; +#endif + const ShadowNodeFamily::Shared decoratorFamily; + +#ifdef ANDROID + folly::dynamic getDynamic() const { + return folly::dynamic::object("decoratorFamily", "pointer should be here?"); + } + MapBuffer getMapBuffer() const { return MapBufferBuilder::EMPTY(); }; +#endif }; } // namespace react diff --git a/ios/MarkdownTextInputDecoratorComponentDescriptor.h b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorViewComponentDescriptor.h similarity index 87% rename from ios/MarkdownTextInputDecoratorComponentDescriptor.h rename to cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorViewComponentDescriptor.h index 2b0266478..c145e45ce 100644 --- a/ios/MarkdownTextInputDecoratorComponentDescriptor.h +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorViewComponentDescriptor.h @@ -7,7 +7,7 @@ namespace facebook { namespace react { -class MarkdownTextInputDecoratorComponentDescriptor final +class MarkdownTextInputDecoratorViewComponentDescriptor final : public ConcreteComponentDescriptor { public: using ConcreteComponentDescriptor::ConcreteComponentDescriptor; diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 48643a6f2..a50e9a8ee 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -2,3 +2,11 @@ rootProject.name = 'LiveMarkdownExample' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') +includeBuild('../node_modules/react-native') { + dependencySubstitution { + substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) + substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) + substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) + substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) + } +} diff --git a/example/package.json b/example/package.json index 5d9b8a2c1..3015fa426 100644 --- a/example/package.json +++ b/example/package.json @@ -10,6 +10,8 @@ "build:ios": "cd ios && xcodebuild -workspace LiveMarkdownExample.xcworkspace -scheme LiveMarkdownExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { + "patch-package": "^8.0.0", + "postinstall-postinstall": "^2.1.0", "react": "18.2.0", "react-native": "0.73.4" }, diff --git a/example/patches/@react-native+gradle-plugin+0.73.4.patch b/example/patches/@react-native+gradle-plugin+0.73.4.patch new file mode 100644 index 000000000..e55be2d15 --- /dev/null +++ b/example/patches/@react-native+gradle-plugin+0.73.4.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/@react-native/gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PreparePrefabHeadersTask.kt b/node_modules/@react-native/gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PreparePrefabHeadersTask.kt +index f3b55e0..ede5c95 100644 +--- a/node_modules/@react-native/gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PreparePrefabHeadersTask.kt ++++ b/node_modules/@react-native/gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PreparePrefabHeadersTask.kt +@@ -45,15 +45,27 @@ abstract class PreparePrefabHeadersTask : DefaultTask() { + fs.copy { + it.from(headerPath) + it.include("**/*.h") ++ it.include("**/*.hpp") + it.exclude("**/*.cpp") + it.exclude("**/*.txt") +- // We don't want to copy all the boost headers as they are 250Mb+ +- it.include("boost/config.hpp") +- it.include("boost/config/**/*.hpp") +- it.include("boost/core/*.hpp") +- it.include("boost/detail/workaround.hpp") +- it.include("boost/operators.hpp") +- it.include("boost/preprocessor/**/*.hpp") ++// // We don't want to copy all the boost headers as they are 250Mb+ ++// it.include("boost/config.hpp") ++// it.include("boost/config/**/*.hpp") ++// it.include("boost/container_hash/**/*.hpp") ++// it.include("boost/core/*.hpp") ++// it.include("boost/intrusive/**/*.hpp") ++// it.include("boost/move/**/*.hpp") ++// it.include("boost/type_traits/**/*.hpp") ++// it.include("boost/describe/**/*.hpp") ++// it.include("boost/iterator/*.hpp") ++// it.include("boost/detail/workaround.hpp") ++// it.include("boost/assert.hpp") ++// it.include("boost/operators.hpp") ++// it.include("boost/utility.hpp") ++// it.include("boost/cstdint.hpp") ++// it.include("boost/version.hpp") ++// it.include("boost/static_assert.hpp") ++// it.include("boost/preprocessor/**/*.hpp") + it.into(File(outputFolder.asFile, headerPrefix)) + } + } diff --git a/example/patches/react-native+0.73.4.patch b/example/patches/react-native+0.73.4.patch new file mode 100644 index 000000000..001a2dec9 --- /dev/null +++ b/example/patches/react-native+0.73.4.patch @@ -0,0 +1,95 @@ +diff --git a/node_modules/react-native/ReactAndroid/build.gradle b/node_modules/react-native/ReactAndroid/build.gradle +index 78c57eb..ec147fd 100644 +--- a/node_modules/react-native/ReactAndroid/build.gradle ++++ b/node_modules/react-native/ReactAndroid/build.gradle +@@ -125,6 +125,19 @@ final def preparePrefab = tasks.register("preparePrefab", PreparePrefabHeadersTa + "rrc_root", + new Pair("../ReactCommon/react/renderer/components/root/", "react/renderer/components/root/") + ), ++ new PrefabPreprocessingEntry( ++ "rrc_text", ++ [ ++ new Pair("../ReactCommon/react/renderer/components/text/", "react/renderer/components/text/"), ++ new Pair("../ReactCommon/react/renderer/attributedstring", "react/renderer/attributedstring"), ++ ] ++ ), ++ new PrefabPreprocessingEntry( ++ "rrc_textinput", ++ [ ++ new Pair("../ReactCommon/react/renderer/components/textinput/androidtextinput", ""), ++ ] ++ ), + new PrefabPreprocessingEntry( + "rrc_view", + [ +@@ -132,6 +145,13 @@ final def preparePrefab = tasks.register("preparePrefab", PreparePrefabHeadersTa + new Pair("../ReactCommon/react/renderer/components/view/platform/android/", ""), + ] + ), ++ new PrefabPreprocessingEntry( ++ "react_render_textlayoutmanager", ++ [ ++ new Pair("../ReactCommon/react/renderer/textlayoutmanager/", "react/renderer/textlayoutmanager/"), ++ new Pair("../ReactCommon/react/renderer/textlayoutmanager/platform/android/", ""), ++ ] ++ ), + new PrefabPreprocessingEntry( + "rrc_legacyviewmanagerinterop", + new Pair("../ReactCommon/react/renderer/components/legacyviewmanagerinterop/", "react/renderer/components/legacyviewmanagerinterop/") +@@ -559,6 +579,9 @@ android { + "glog", + "fabricjni", + "react_render_mapbuffer", ++ "react_render_textlayoutmanager", ++ "rrc_textinput", ++ "rrc_text", + "yoga", + "folly_runtime", + "react_nativemodule_core", +@@ -683,6 +706,15 @@ android { + rrc_root { + headers(new File(prefabHeadersDir, "rrc_root").absolutePath) + } ++ rrc_text { ++ headers(new File(prefabHeadersDir, "rrc_text").absolutePath) ++ } ++ rrc_textinput { ++ headers(new File(prefabHeadersDir, "rrc_textinput").absolutePath) ++ } ++ react_render_textlayoutmanager { ++ headers(new File(prefabHeadersDir, "react_render_textlayoutmanager").absolutePath) ++ } + rrc_view { + headers(new File(prefabHeadersDir, "rrc_view").absolutePath) + } +diff --git a/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake b/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake +index d49fa9e..3607c69 100644 +--- a/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake ++++ b/node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake +@@ -78,11 +78,14 @@ add_library(jsi ALIAS ReactAndroid::jsi) + add_library(glog ALIAS ReactAndroid::glog) + add_library(fabricjni ALIAS ReactAndroid::fabricjni) + add_library(react_render_mapbuffer ALIAS ReactAndroid::react_render_mapbuffer) ++add_library(react_render_textlayoutmanager ALIAS ReactAndroid::react_render_textlayoutmanager) + add_library(yoga ALIAS ReactAndroid::yoga) + add_library(folly_runtime ALIAS ReactAndroid::folly_runtime) + add_library(react_nativemodule_core ALIAS ReactAndroid::react_nativemodule_core) + add_library(react_render_imagemanager ALIAS ReactAndroid::react_render_imagemanager) + add_library(rrc_image ALIAS ReactAndroid::rrc_image) ++add_library(rrc_text ALIAS ReactAndroid::rrc_text) ++add_library(rrc_textinput ALIAS ReactAndroid::rrc_textinput) + add_library(rrc_legacyviewmanagerinterop ALIAS ReactAndroid::rrc_legacyviewmanagerinterop) + + find_package(fbjni REQUIRED CONFIG) +@@ -105,8 +108,11 @@ target_link_libraries(${CMAKE_PROJECT_NAME} + react_render_graphics # prefab ready + react_render_imagemanager # prefab ready + react_render_mapbuffer # prefab ready ++ react_render_textlayoutmanager # prefab ready + rrc_image # prefab ready + rrc_view # prefab ready ++ rrc_text # prefab ready ++ rrc_textinput # prefab ready + rrc_legacyviewmanagerinterop # prefab ready + runtimeexecutor # prefab ready + turbomodulejsijni # prefab ready diff --git a/ios/MarkdownTextInputDecoratorComponentView.mm b/ios/MarkdownTextInputDecoratorComponentView.mm index 728a6e9ed..7fcfa9659 100644 --- a/ios/MarkdownTextInputDecoratorComponentView.mm +++ b/ios/MarkdownTextInputDecoratorComponentView.mm @@ -6,7 +6,7 @@ #import #import -#import "MarkdownTextInputDecoratorComponentDescriptor.h" +#import #import "MarkdownShadowFamilyRegistry.h" #import "RCTFabricComponentsPlugins.h" @@ -19,7 +19,7 @@ @implementation MarkdownTextInputDecoratorComponentView { + (ComponentDescriptorProvider)componentDescriptorProvider { - return concreteComponentDescriptorProvider(); + return concreteComponentDescriptorProvider(); } - (instancetype)initWithFrame:(CGRect)frame diff --git a/package.json b/package.json index 7b221a6f3..1864f185d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "ios", "cpp", "*.podspec", + "react-native.config.js", "!ios/build", "!android/build", "!android/gradle", diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 000000000..2151248f7 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,12 @@ +module.exports = { + dependency: { + platforms: { + android: { + componentDescriptors: [ + "MarkdownTextInputDecoratorViewComponentDescriptor", + ], + cmakeListsPath: "../android/src/main/new_arch/CMakeLists.txt" + }, + }, + }, +} diff --git a/src/MarkdownTextInputDecoratorViewNativeComponent.ts b/src/MarkdownTextInputDecoratorViewNativeComponent.ts index 90ce575a2..729d183c8 100644 --- a/src/MarkdownTextInputDecoratorViewNativeComponent.ts +++ b/src/MarkdownTextInputDecoratorViewNativeComponent.ts @@ -48,6 +48,8 @@ interface NativeProps extends ViewProps { markdownStyle: MarkdownStyle; } -export default codegenNativeComponent('MarkdownTextInputDecoratorView'); +export default codegenNativeComponent('MarkdownTextInputDecoratorView', { + interfaceOnly: true, +}); export type {MarkdownStyle}; diff --git a/yarn.lock b/yarn.lock index 7789afdfd..66d8bde44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1830,7 +1830,9 @@ __metadata: "@react-native/babel-preset": 0.73.21 "@react-native/metro-config": 0.73.5 babel-plugin-module-resolver: ^5.0.0 + patch-package: ^8.0.0 pod-install: ^0.1.0 + postinstall-postinstall: ^2.1.0 react: 18.2.0 react-native: 0.73.4 languageName: unknown @@ -3581,6 +3583,13 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/lockfile@npm:^1.1.0": + version: 1.1.0 + resolution: "@yarnpkg/lockfile@npm:1.1.0" + checksum: 05b881b4866a3546861fee756e6d3812776ea47fa6eb7098f983d6d0eefa02e12b66c3fff931574120f196286a7ad4879ce02743c8bb2be36c6a576c7852083a + languageName: node + linkType: hard + "JSONStream@npm:^1.0.4": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -4068,6 +4077,13 @@ __metadata: languageName: node linkType: hard +"at-least-node@npm:^1.0.0": + version: 1.0.0 + resolution: "at-least-node@npm:1.0.0" + checksum: 463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.5": version: 1.0.5 resolution: "available-typed-arrays@npm:1.0.5" @@ -4660,7 +4676,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0": +"ci-info@npm:^3.2.0, ci-info@npm:^3.7.0": version: 3.9.0 resolution: "ci-info@npm:3.9.0" checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 @@ -7074,6 +7090,15 @@ __metadata: languageName: node linkType: hard +"find-yarn-workspace-root@npm:^2.0.0": + version: 2.0.0 + resolution: "find-yarn-workspace-root@npm:2.0.0" + dependencies: + micromatch: ^4.0.2 + checksum: fa5ca8f9d08fe7a54ce7c0a5931ff9b7e36f9ee7b9475fb13752bcea80ec6b5f180fa5102d60b376d5526ce924ea3fc6b19301262efa0a5d248dd710f3644242 + languageName: node + linkType: hard + "flat-cache@npm:^2.0.1": version: 2.0.1 resolution: "flat-cache@npm:2.0.1" @@ -7206,6 +7231,18 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^9.0.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: ^1.0.0 + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -9536,6 +9573,18 @@ __metadata: languageName: node linkType: hard +"json-stable-stringify@npm:^1.0.2": + version: 1.1.1 + resolution: "json-stable-stringify@npm:1.1.1" + dependencies: + call-bind: ^1.0.5 + isarray: ^2.0.5 + jsonify: ^0.0.1 + object-keys: ^1.1.1 + checksum: e1ba06600fd278767eeff53f28e408e29c867e79abf564e7aadc3ce8f31f667258f8db278ef28831e45884dd687388fa1910f46e599fc19fb94c9afbbe3a4de8 + languageName: node + linkType: hard + "json-stringify-safe@npm:^5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" @@ -9588,6 +9637,13 @@ __metadata: languageName: node linkType: hard +"jsonify@npm:^0.0.1": + version: 0.0.1 + resolution: "jsonify@npm:0.0.1" + checksum: 027287e1c0294fce15f18c0ff990cfc2318e7f01fb76515f784d5cd0784abfec6fc5c2355c3a2f2cb0ad7f4aa2f5b74ebbfe4e80476c35b2d13cabdb572e1134 + languageName: node + linkType: hard + "jsonparse@npm:^1.2.0": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" @@ -9633,6 +9689,15 @@ __metadata: languageName: node linkType: hard +"klaw-sync@npm:^6.0.0": + version: 6.0.0 + resolution: "klaw-sync@npm:6.0.0" + dependencies: + graceful-fs: ^4.1.11 + checksum: 0da397f8961313c3ef8f79fb63af9002cde5a8fb2aeb1a37351feff0dd6006129c790400c3f5c3b4e757bedcabb13d21ec0a5eaef5a593d59515d4f2c291e475 + languageName: node + linkType: hard + "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -10307,7 +10372,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" dependencies: @@ -10973,7 +11038,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^7.0.3": +"open@npm:^7.0.3, open@npm:^7.4.2": version: 7.4.2 resolution: "open@npm:7.4.2" dependencies: @@ -11267,6 +11332,31 @@ __metadata: languageName: node linkType: hard +"patch-package@npm:^8.0.0": + version: 8.0.0 + resolution: "patch-package@npm:8.0.0" + dependencies: + "@yarnpkg/lockfile": ^1.1.0 + chalk: ^4.1.2 + ci-info: ^3.7.0 + cross-spawn: ^7.0.3 + find-yarn-workspace-root: ^2.0.0 + fs-extra: ^9.0.0 + json-stable-stringify: ^1.0.2 + klaw-sync: ^6.0.0 + minimist: ^1.2.6 + open: ^7.4.2 + rimraf: ^2.6.3 + semver: ^7.5.3 + slash: ^2.0.0 + tmp: ^0.0.33 + yaml: ^2.2.2 + bin: + patch-package: index.js + checksum: d23cddc4d1622e2d8c7ca31b145c6eddb24bd271f69905e766de5e1f199f0b9a5479a6a6939ea857288399d4ed249285639d539a2c00fbddb7daa39934b007a2 + languageName: node + linkType: hard + "path-exists@npm:^3.0.0": version: 3.0.0 resolution: "path-exists@npm:3.0.0" @@ -11427,6 +11517,13 @@ __metadata: languageName: node linkType: hard +"postinstall-postinstall@npm:^2.1.0": + version: 2.1.0 + resolution: "postinstall-postinstall@npm:2.1.0" + checksum: e1d34252cf8d2c5641c7d2db7426ec96e3d7a975f01c174c68f09ef5b8327bc8d5a9aa2001a45e693db2cdbf69577094d3fe6597b564ad2d2202b65fba76134b + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -12385,6 +12482,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^2.6.3": + version: 2.7.1 + resolution: "rimraf@npm:2.7.1" + dependencies: + glob: ^7.1.3 + bin: + rimraf: ./bin.js + checksum: cdc7f6eacb17927f2a075117a823e1c5951792c6498ebcce81ca8203454a811d4cf8900314154d3259bb8f0b42ab17f67396a8694a54cae3283326e57ad250cd + languageName: node + linkType: hard + "rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -12746,6 +12854,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^2.0.0": + version: 2.0.0 + resolution: "slash@npm:2.0.0" + checksum: 512d4350735375bd11647233cb0e2f93beca6f53441015eea241fe784d8068281c3987fbaa93e7ef1c38df68d9c60013045c92837423c69115297d6169aa85e6 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -14413,6 +14528,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.2": + version: 2.4.1 + resolution: "yaml@npm:2.4.1" + bin: + yaml: bin.mjs + checksum: 4c391d07a5d5e935e058babb71026c9cdc9a6fd889e35dd91b53cfb0a12691b67c6c5c740858e71345fef18cd9c13c554a6dda9196f59820d769d94041badb0b + languageName: node + linkType: hard + "yargs-parser@npm:21.1.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1"