@expensify/react-native-live-markdown 0.1.296 → 0.1.298

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -188,16 +188,17 @@ Currently, `react-native-live-markdown` supports only [ExpensiMark](https://gith
188
188
 
189
189
  `react-native-live-markdown` supports only latest React Native minor releases with the New Architecture enabled.
190
190
 
191
- | @expensify/react-native-live-markdown | 0.73 | 0.74 | 0.75 | 0.76 | 0.77 | 0.78 | 0.79 |
192
- | :-----------------------------------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
193
- | 0.1.260+ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
194
- | 0.1.256 – 0.1.259 | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
195
- | 0.1.248 – 0.1.255 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
196
- | 0.1.235 – 0.1.247 | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
197
- | 0.1.141 – 0.1.234 | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
198
- | 0.1.129 – 0.1.140 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
199
- | 0.1.122 – 0.1.128 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
200
- | 0.1.15 – 0.1.121 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
191
+ | @expensify/react-native-live-markdown | 0.73 | 0.74 | 0.75 | 0.76 | 0.77 | 0.78 | 0.79 | 0.80 |
192
+ | :-----------------------------------: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
193
+ | 0.1.297+ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
194
+ | 0.1.260 – 0.1.296 | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ |
195
+ | 0.1.256 – 0.1.259 | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
196
+ | 0.1.248 – 0.1.255 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
197
+ | 0.1.235 – 0.1.247 | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
198
+ | 0.1.141 – 0.1.234 | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
199
+ | 0.1.129 – 0.1.140 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
200
+ | 0.1.122 – 0.1.128 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
201
+ | 0.1.15 – 0.1.121 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
201
202
 
202
203
  ## License
203
204
 
@@ -128,8 +128,8 @@ android {
128
128
  ]
129
129
 
130
130
  // TextLayoutManager
131
- if (REACT_NATIVE_MINOR_VERSION <= 76 ) {
132
- java.srcDirs += "src/reactNativeVersionPatch/CustomMountingManager/76"
131
+ if (REACT_NATIVE_MINOR_VERSION <= 79 ) {
132
+ java.srcDirs += "src/reactNativeVersionPatch/CustomMountingManager/79"
133
133
  } else {
134
134
  java.srcDirs += "src/reactNativeVersionPatch/CustomMountingManager/latest"
135
135
  }
@@ -51,6 +51,7 @@ target_compile_options(
51
51
  -frtti
52
52
  -Wall
53
53
  -std=c++20
54
+ -DREACT_NATIVE_MINOR_VERSION=${ReactAndroid_VERSION_MINOR}
54
55
  )
55
56
 
56
57
  target_include_directories(
@@ -3,7 +3,9 @@
3
3
  #include <fbjni/fbjni.h>
4
4
  #include <react/fabric/JFabricUIManager.h>
5
5
  #include <react/jni/ReadableNativeMap.h>
6
+ #if REACT_NATIVE_MINOR_VERSION < 80
6
7
  #include <react/jni/SafeReleaseJniRef.h>
8
+ #endif // REACT_NATIVE_MINOR_VERSION < 80
7
9
  #include <react/renderer/components/view/conversions.h>
8
10
  #include <react/renderer/core/ComponentDescriptor.h>
9
11
  #include <yoga/Yoga.h>
@@ -3,7 +3,6 @@ package com.expensify.livemarkdown;
3
3
  import static com.facebook.react.views.text.TextAttributeProps.UNSET;
4
4
 
5
5
  import android.content.Context;
6
- import android.content.res.AssetManager;
7
6
  import android.text.BoringLayout;
8
7
  import android.text.Layout;
9
8
  import android.text.Spannable;
@@ -12,6 +11,7 @@ import android.text.TextPaint;
12
11
 
13
12
  import androidx.annotation.NonNull;
14
13
  import androidx.annotation.Nullable;
14
+ import androidx.core.util.Preconditions;
15
15
 
16
16
  import com.facebook.react.bridge.ReactContext;
17
17
  import com.facebook.react.bridge.ReadableMap;
@@ -21,6 +21,7 @@ import com.facebook.react.uimanager.PixelUtil;
21
21
  import com.facebook.react.uimanager.ViewManagerRegistry;
22
22
  import com.facebook.react.views.text.TextAttributeProps;
23
23
  import com.facebook.react.views.text.TextLayoutManager;
24
+ import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan;
24
25
  import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan;
25
26
  import com.facebook.yoga.YogaMeasureMode;
26
27
  import com.facebook.yoga.YogaMeasureOutput;
@@ -30,7 +31,13 @@ import java.lang.reflect.Method;
30
31
 
31
32
  public class CustomMountingManager extends MountingManager {
32
33
  private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
33
- private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
34
+ private static final ThreadLocal<TextPaint> sTextPaintInstance =
35
+ new ThreadLocal<TextPaint>() {
36
+ @Override
37
+ protected TextPaint initialValue() {
38
+ return new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
39
+ }
40
+ };
34
41
 
35
42
  private MarkdownUtils markdownUtils;
36
43
 
@@ -77,24 +84,48 @@ public class CustomMountingManager extends MountingManager {
77
84
  TextAttributeProps.getHyphenationFrequency(
78
85
  paragraphAttributes.getString(TextLayoutManager.PA_KEY_HYPHENATION_FREQUENCY));
79
86
 
80
- // StaticLayout returns wrong metrics for the last line if it's empty, add something to the
81
- // last line so it's measured correctly
82
- if (text.toString().endsWith("\n")) {
83
- SpannableStringBuilder sb = new SpannableStringBuilder(text);
84
- sb.append("I");
87
+ try {
88
+ Class<TextLayoutManager> textLayoutManagerClass = TextLayoutManager.class;
85
89
 
86
- text = sb;
87
- }
90
+ Method getTextAlignmentAttrMethod = textLayoutManagerClass.getDeclaredMethod("getTextAlignmentAttr", MapBuffer.class);
91
+ getTextAlignmentAttrMethod.setAccessible(true);
88
92
 
89
- Layout.Alignment alignment = TextLayoutManager.getTextAlignment(attributedString, text);
93
+ String textAlignmentAttr = (String)getTextAlignmentAttrMethod.invoke(null, attributedString);
90
94
 
91
- markdownUtils.applyMarkdownFormatting((SpannableStringBuilder)text);
95
+ Method getTextAlignmentMethod = textLayoutManagerClass.getDeclaredMethod("getTextAlignment", MapBuffer.class, Spannable.class, String.class);
96
+ getTextAlignmentMethod.setAccessible(true);
92
97
 
93
- BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
98
+ Layout.Alignment alignment = (Layout.Alignment)getTextAlignmentMethod.invoke(
99
+ null,
100
+ attributedString,
101
+ text,
102
+ textAlignmentAttr
103
+ );
94
104
 
95
- Class<TextLayoutManager> mapBufferClass = TextLayoutManager.class;
96
- try {
97
- Method createLayoutMethod = mapBufferClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class, Layout.Alignment.class);
105
+ Method getTextJustificationModeMethod = textLayoutManagerClass.getDeclaredMethod("getTextJustificationMode", String.class);
106
+ getTextJustificationModeMethod.setAccessible(true);
107
+
108
+ Integer justificationMode = (Integer) getTextJustificationModeMethod.invoke(null, textAlignmentAttr);
109
+
110
+
111
+ markdownUtils.applyMarkdownFormatting((SpannableStringBuilder)text);
112
+
113
+ TextPaint paint;
114
+ if (attributedString.contains(TextLayoutManager.AS_KEY_CACHE_ID)) {
115
+ paint = text.getSpans(0, 0, ReactTextPaintHolderSpan.class)[0].getTextPaint();
116
+ } else {
117
+ TextAttributeProps baseTextAttributes =
118
+ TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(TextLayoutManager.AS_KEY_BASE_ATTRIBUTES));
119
+ paint = Preconditions.checkNotNull(sTextPaintInstance.get());
120
+
121
+ Method updateTextPaintMethod = textLayoutManagerClass.getDeclaredMethod("updateTextPaint", TextPaint.class, TextAttributeProps.class, Context.class);
122
+ updateTextPaintMethod.setAccessible(true);
123
+ updateTextPaintMethod.invoke(null, paint, baseTextAttributes, context);
124
+ }
125
+
126
+ BoringLayout.Metrics boring = BoringLayout.isBoring(text, paint);
127
+
128
+ Method createLayoutMethod = textLayoutManagerClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class, Layout.Alignment.class, int.class, TextPaint.class);
98
129
  createLayoutMethod.setAccessible(true);
99
130
 
100
131
  Layout layout = (Layout)createLayoutMethod.invoke(
@@ -106,7 +137,9 @@ public class CustomMountingManager extends MountingManager {
106
137
  includeFontPadding,
107
138
  textBreakStrategy,
108
139
  hyphenationFrequency,
109
- alignment);
140
+ alignment,
141
+ justificationMode,
142
+ paint);
110
143
 
111
144
  int maximumNumberOfLines =
112
145
  paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
@@ -0,0 +1,47 @@
1
+ package com.expensify.livemarkdown;
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext;
4
+ import com.facebook.react.bridge.ReadableMap;
5
+ import com.facebook.react.fabric.FabricUIManager;
6
+ import com.facebook.react.fabric.mounting.MountingManager;
7
+ import com.facebook.react.uimanager.ViewManagerRegistry;
8
+ import com.facebook.react.uimanager.events.BatchEventDispatchedListener;
9
+
10
+ import java.lang.reflect.Field;
11
+
12
+ public class CustomFabricUIManager {
13
+
14
+ public static FabricUIManager create(FabricUIManager source, ReadableMap markdownProps, int parserId) {
15
+ Class<? extends FabricUIManager> uiManagerClass = source.getClass();
16
+
17
+ try {
18
+ Field mountingManagerField = uiManagerClass.getDeclaredField("mMountingManager");
19
+ mountingManagerField.setAccessible(true);
20
+
21
+ MountingManager sourceMountingManager = readPrivateField(source, "mMountingManager");
22
+ ReactApplicationContext reactContext = readPrivateField(source, "mReactApplicationContext");
23
+ ViewManagerRegistry viewManagerRegistry = readPrivateField(source, "mViewManagerRegistry");
24
+ BatchEventDispatchedListener batchEventDispatchedListener = readPrivateField(source, "mBatchEventDispatchedListener");
25
+ MountingManager.MountItemExecutor mountItemExecutor = readPrivateField(source, "mMountItemExecutor");
26
+
27
+ FabricUIManager customFabricUIManager = new FabricUIManager(reactContext, viewManagerRegistry, batchEventDispatchedListener);
28
+
29
+ mountingManagerField.set(customFabricUIManager, new CustomMountingManager(sourceMountingManager, viewManagerRegistry, mountItemExecutor, reactContext, markdownProps, parserId));
30
+
31
+ return customFabricUIManager;
32
+ } catch (NoSuchFieldException | IllegalAccessException e) {
33
+ throw new RuntimeException("[LiveMarkdown] Cannot read data from FabricUIManager");
34
+ }
35
+ }
36
+
37
+ @SuppressWarnings("unchecked")
38
+ private static <T> T readPrivateField(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
39
+ Class<?> clazz = obj.getClass();
40
+
41
+ Field field = clazz.getDeclaredField(name);
42
+ field.setAccessible(true);
43
+ T value = (T) field.get(obj);
44
+
45
+ return value;
46
+ }
47
+ }
@@ -8,6 +8,7 @@ import android.text.Layout;
8
8
  import android.text.Spannable;
9
9
  import android.text.SpannableStringBuilder;
10
10
  import android.text.TextPaint;
11
+ import android.text.TextUtils;
11
12
 
12
13
  import androidx.annotation.NonNull;
13
14
  import androidx.annotation.Nullable;
@@ -15,8 +16,10 @@ import androidx.core.util.Preconditions;
15
16
 
16
17
  import com.facebook.react.bridge.ReactContext;
17
18
  import com.facebook.react.bridge.ReadableMap;
19
+ import com.facebook.react.common.ReactConstants;
18
20
  import com.facebook.react.common.mapbuffer.MapBuffer;
19
21
  import com.facebook.react.fabric.mounting.MountingManager;
22
+ import com.facebook.react.fabric.mounting.SurfaceMountingManager;
20
23
  import com.facebook.react.uimanager.PixelUtil;
21
24
  import com.facebook.react.uimanager.ViewManagerRegistry;
22
25
  import com.facebook.react.views.text.TextAttributeProps;
@@ -39,20 +42,28 @@ public class CustomMountingManager extends MountingManager {
39
42
  }
40
43
  };
41
44
 
45
+ private MountingManager sourceMountingManager;
42
46
  private MarkdownUtils markdownUtils;
43
47
 
44
48
  public CustomMountingManager(
49
+ @NonNull MountingManager sourceMountingManager,
45
50
  @NonNull ViewManagerRegistry viewManagerRegistry,
46
51
  @NonNull MountItemExecutor mountItemExecutor,
47
52
  @NonNull Context context,
48
53
  @NonNull ReadableMap decoratorProps,
49
54
  int parserId) {
50
55
  super(viewManagerRegistry, mountItemExecutor);
56
+ this.sourceMountingManager = sourceMountingManager;
51
57
  this.markdownUtils = new MarkdownUtils((ReactContext) context);
52
58
  this.markdownUtils.setMarkdownStyle(new MarkdownStyle(decoratorProps, context));
53
59
  this.markdownUtils.setParserId(parserId);
54
60
  }
55
61
 
62
+ @Override
63
+ public SurfaceMountingManager getSurfaceManagerEnforced(int surfaceId, String context) {
64
+ return sourceMountingManager.getSurfaceManagerEnforced(surfaceId, context);
65
+ }
66
+
56
67
  @Override
57
68
  public long measureMapBuffer(
58
69
  @NonNull ReactContext context,
@@ -125,9 +136,20 @@ public class CustomMountingManager extends MountingManager {
125
136
 
126
137
  BoringLayout.Metrics boring = BoringLayout.isBoring(text, paint);
127
138
 
128
- Method createLayoutMethod = textLayoutManagerClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class, Layout.Alignment.class, int.class, TextPaint.class);
139
+ Method createLayoutMethod = textLayoutManagerClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class, Layout.Alignment.class, int.class, TextUtils.TruncateAt.class, int.class, TextPaint.class);
129
140
  createLayoutMethod.setAccessible(true);
130
141
 
142
+ int maximumNumberOfLines =
143
+ paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
144
+ ? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
145
+ : ReactConstants.UNSET;
146
+ @Nullable
147
+ TextUtils.TruncateAt ellipsizeMode =
148
+ paragraphAttributes.contains(TextLayoutManager.PA_KEY_ELLIPSIZE_MODE)
149
+ ? TextAttributeProps.getEllipsizeMode(
150
+ paragraphAttributes.getString(TextLayoutManager.PA_KEY_ELLIPSIZE_MODE))
151
+ : null;
152
+
131
153
  Layout layout = (Layout)createLayoutMethod.invoke(
132
154
  null,
133
155
  text,
@@ -139,13 +161,10 @@ public class CustomMountingManager extends MountingManager {
139
161
  hyphenationFrequency,
140
162
  alignment,
141
163
  justificationMode,
164
+ ellipsizeMode,
165
+ maximumNumberOfLines,
142
166
  paint);
143
167
 
144
- int maximumNumberOfLines =
145
- paragraphAttributes.contains(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
146
- ? paragraphAttributes.getInt(TextLayoutManager.PA_KEY_MAX_NUMBER_OF_LINES)
147
- : UNSET;
148
-
149
168
  int calculatedLineCount =
150
169
  maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
151
170
  ? layout.getLineCount()
@@ -14,6 +14,7 @@
14
14
  #import <RNLiveMarkdown/MarkdownTextInputDecoratorViewComponentDescriptor.h>
15
15
  #import <RNLiveMarkdown/MarkdownTextStorageDelegate.h>
16
16
  #import <RNLiveMarkdown/RCTMarkdownStyle.h>
17
+ #import <RNLiveMarkdown/RCTTextInput+AdaptiveImageGlyph.h>
17
18
 
18
19
  #import <objc/runtime.h>
19
20
 
@@ -85,6 +86,9 @@ using namespace facebook::react;
85
86
  // make sure `adjustsFontSizeToFitWidth` is disabled, otherwise formatting will be overwritten
86
87
  react_native_assert(_textField.adjustsFontSizeToFitWidth == NO);
87
88
 
89
+ // Enable TextField AdaptiveImageGlyph support for iOS 18.0+
90
+ [self enableAdaptiveImageGlyphSupport:_textField];
91
+
88
92
  _markdownTextFieldObserver = [[MarkdownTextFieldObserver alloc] initWithTextField:_textField markdownUtils:_markdownUtils];
89
93
 
90
94
  // register observers for future edits
@@ -101,6 +105,9 @@ using namespace facebook::react;
101
105
  } else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) {
102
106
  _textView = (RCTUITextView *)backedTextInputView;
103
107
 
108
+ // Enable TextView AdaptiveImageGlyph support for iOS 18.0+
109
+ [self enableAdaptiveImageGlyphSupport:_textView];
110
+
104
111
  // register delegate for future edits
105
112
  react_native_assert(_textView.textStorage.delegate == nil);
106
113
  _markdownTextStorageDelegate = [[MarkdownTextStorageDelegate alloc] initWithTextView:_textView markdownUtils:_markdownUtils];
@@ -140,6 +147,22 @@ using namespace facebook::react;
140
147
  }
141
148
  }
142
149
 
150
+ - (void)enableAdaptiveImageGlyphSupport:(UIView *)textInputView {
151
+ if (@available(iOS 18.0, *)) {
152
+ if ([textInputView respondsToSelector:@selector(setSupportsAdaptiveImageGlyph:)]) {
153
+ [textInputView setValue:@YES forKey:@"supportsAdaptiveImageGlyph"];
154
+ }
155
+ }
156
+ }
157
+
158
+ - (void)disableAdaptiveImageGlyphSupport:(UIView *)textInputView {
159
+ if (@available(iOS 18.0, *)) {
160
+ if ([textInputView respondsToSelector:@selector(setSupportsAdaptiveImageGlyph:)]) {
161
+ [textInputView setValue:@NO forKey:@"supportsAdaptiveImageGlyph"];
162
+ }
163
+ }
164
+ }
165
+
143
166
  - (void)removeTextInputObservers
144
167
  {
145
168
  react_native_assert(_observersAdded && "MarkdownTextInputDecoratorComponentView tried to remove TextInput observers while they were detached");
@@ -154,6 +177,7 @@ using namespace facebook::react;
154
177
  }
155
178
  _markdownBackedTextInputDelegate = nil;
156
179
  [_textView removeObserver:_markdownTextViewObserver forKeyPath:@"defaultTextAttributes" context:NULL];
180
+ [self disableAdaptiveImageGlyphSupport:_textView];
157
181
  _markdownTextViewObserver = nil;
158
182
  _markdownTextStorageDelegate = nil;
159
183
  _textView.textStorage.delegate = nil;
@@ -165,6 +189,7 @@ using namespace facebook::react;
165
189
  [_textField removeTarget:_markdownTextFieldObserver action:@selector(textFieldDidEndEditing:) forControlEvents:UIControlEventEditingDidEnd];
166
190
  [_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"text" context:NULL];
167
191
  [_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"attributedText" context:NULL];
192
+ [self disableAdaptiveImageGlyphSupport:_textField];
168
193
  _markdownTextFieldObserver = nil;
169
194
  _textField = nil;
170
195
  }
@@ -0,0 +1,14 @@
1
+ #import <React/RCTUITextField.h>
2
+ #import <React/RCTUITextView.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface RCTUITextField (AdaptiveImageGlyph)
7
+ - (void)liveMarkdown_insertAdaptiveImageGlyph:(id)glyph replacementRange:(NSRange)replacementRange;
8
+ @end
9
+
10
+ @interface RCTUITextView (AdaptiveImageGlyph)
11
+ - (void)liveMarkdown_insertAdaptiveImageGlyph:(id)glyph replacementRange:(NSRange)replacementRange;
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,128 @@
1
+ #import <RNLiveMarkdown/RCTTextInput+AdaptiveImageGlyph.h>
2
+ #import <UIKit/UIKit.h>
3
+ #import <objc/runtime.h>
4
+
5
+ // Helper method to setup method swizzling for adaptive image glyph
6
+ static void setupAdaptiveImageGlyphSwizzling(Class targetClass, SEL swizzledSelector) {
7
+ if (@available(iOS 18.0, *)) {
8
+ SEL originalSelector = @selector(insertAdaptiveImageGlyph:replacementRange:);
9
+
10
+ if ([targetClass instancesRespondToSelector:originalSelector]) {
11
+ Method originalMethod = class_getInstanceMethod(targetClass, originalSelector);
12
+ Method swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector);
13
+
14
+ if (originalMethod && swizzledMethod) {
15
+ BOOL didAddMethod = class_addMethod(targetClass,
16
+ originalSelector,
17
+ method_getImplementation(swizzledMethod),
18
+ method_getTypeEncoding(swizzledMethod));
19
+
20
+ if (didAddMethod) {
21
+ class_replaceMethod(targetClass,
22
+ swizzledSelector,
23
+ method_getImplementation(originalMethod),
24
+ method_getTypeEncoding(originalMethod));
25
+ } else {
26
+ method_exchangeImplementations(originalMethod, swizzledMethod);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ // Helper method to process genmoji and convert to pasteable image
34
+ static UIImage *processGenmojiImage(id glyph) {
35
+ if (!glyph) {
36
+ return nil;
37
+ }
38
+
39
+ NSData *imageData = nil;
40
+ if ([glyph respondsToSelector:@selector(imageContent)]) {
41
+ imageData = [glyph performSelector:@selector(imageContent)];
42
+ }
43
+
44
+ if (!imageData) {
45
+ return nil;
46
+ }
47
+
48
+ UIImage *originalImage = [UIImage imageWithData:imageData];
49
+ if (!originalImage) {
50
+ return nil;
51
+ }
52
+
53
+ CGSize targetSize = CGSizeMake(128, 128);
54
+ UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
55
+ [originalImage drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)];
56
+ UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
57
+ UIGraphicsEndImageContext();
58
+
59
+ if (!resizedImage) {
60
+ return nil;
61
+ }
62
+
63
+ NSData *pngData = UIImagePNGRepresentation(resizedImage);
64
+ if (!pngData) {
65
+ return nil;
66
+ }
67
+
68
+ return [UIImage imageWithData:pngData];
69
+ }
70
+
71
+ // Shared helper method to handle adaptive image glyph paste event
72
+ static void handleAdaptiveImageGlyphPasteEvent(UIView *textInputView, id glyph, NSRange replacementRange) {
73
+ UIImage *processedImage = processGenmojiImage(glyph);
74
+ if (!processedImage) {
75
+ return;
76
+ }
77
+
78
+ // Save current clipboard contents
79
+ UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
80
+ NSArray *savedItems = pasteboard.items;
81
+
82
+ // Set processed image in clipboard and trigger paste
83
+ pasteboard.image = processedImage;
84
+
85
+ if ([textInputView canPerformAction:@selector(paste:) withSender:nil]) {
86
+ [textInputView performSelector:@selector(paste:) withObject:nil];
87
+ }
88
+
89
+ // Restore previous clipboard contents
90
+ pasteboard.items = savedItems;
91
+
92
+ // Return without calling original method to prevent input genmoji insertion
93
+ return;
94
+ }
95
+
96
+ @implementation RCTUITextField (AdaptiveImageGlyph)
97
+
98
+ + (void)load {
99
+ static dispatch_once_t onceToken;
100
+ dispatch_once(&onceToken, ^{
101
+ setupAdaptiveImageGlyphSwizzling([self class], @selector(liveMarkdown_insertAdaptiveImageGlyph:replacementRange:));
102
+ });
103
+ }
104
+
105
+ - (void)liveMarkdown_insertAdaptiveImageGlyph:(id)glyph replacementRange:(NSRange)replacementRange {
106
+ if (@available(iOS 18.0, *)) {
107
+ handleAdaptiveImageGlyphPasteEvent(self, glyph, replacementRange);
108
+ }
109
+ }
110
+
111
+ @end
112
+
113
+ @implementation RCTUITextView (AdaptiveImageGlyph)
114
+
115
+ + (void)load {
116
+ static dispatch_once_t onceToken;
117
+ dispatch_once(&onceToken, ^{
118
+ setupAdaptiveImageGlyphSwizzling([self class], @selector(liveMarkdown_insertAdaptiveImageGlyph:replacementRange:));
119
+ });
120
+ }
121
+
122
+ - (void)liveMarkdown_insertAdaptiveImageGlyph:(id)glyph replacementRange:(NSRange)replacementRange {
123
+ if (@available(iOS 18.0, *)) {
124
+ handleAdaptiveImageGlyphPasteEvent(self, glyph, replacementRange);
125
+ }
126
+ }
127
+
128
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expensify/react-native-live-markdown",
3
- "version": "0.1.296",
3
+ "version": "0.1.298",
4
4
  "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -64,13 +64,13 @@
64
64
  "@commitlint/config-conventional": "^17.0.2",
65
65
  "@evilmartians/lefthook": "^1.5.0",
66
66
  "@lwc/eslint-plugin-lwc": "^1.7.2",
67
- "@react-native-community/cli": "18.0.0",
68
- "@react-native-community/cli-platform-android": "18.0.0",
69
- "@react-native-community/cli-platform-ios": "18.0.0",
70
- "@react-native/eslint-config": "0.79.2",
67
+ "@react-native-community/cli": "19.0.0",
68
+ "@react-native-community/cli-platform-android": "19.0.0",
69
+ "@react-native-community/cli-platform-ios": "19.0.0",
70
+ "@react-native/eslint-config": "0.80.1",
71
71
  "@release-it/conventional-changelog": "^5.0.0",
72
72
  "@types/jest": "^29.5.14",
73
- "@types/react": "^19.0.0",
73
+ "@types/react": "^19.1.0",
74
74
  "@typescript-eslint/eslint-plugin": "^6.15.0",
75
75
  "@typescript-eslint/parser": "^6.15.0",
76
76
  "del-cli": "^5.0.0",
@@ -93,10 +93,11 @@
93
93
  "nodemon": "^3.1.3",
94
94
  "patch-package": "^8.0.0",
95
95
  "prettier": "2.8.8",
96
- "react": "^19.0.0",
97
- "react-native": "0.79.2",
96
+ "react": "19.1.0",
97
+ "react-dom": "19.1.0",
98
+ "react-native": "0.80.1",
98
99
  "react-native-builder-bob": "^0.30.2",
99
- "react-native-reanimated": "3.17.3",
100
+ "react-native-reanimated": "3.18.0",
100
101
  "react-native-web": "^0.20.0",
101
102
  "release-it": "^15.0.0",
102
103
  "turbo": "^1.10.7",