react-native
                                
                                 react-native copied to clipboard
                                
                                    react-native copied to clipboard
                            
                            
                            
                        Over-riding onTextContextMenuItem in ReactEditText to copy/paste plain text instead of rich-text
Summary:
Text is copy pasted as rich text on Android TextInput instead of Plain Text.
Related PR Removing the Paste as plaintext option from the insert and selection context menu #38210
What is the root cause of that problem?
Android EditText and iOS UITextField/UITextView have different copy/paste behavior.
- Android TextInput copies/pastes rich text
- iOS UITextField copies/pastes plain text.
| iOS (react-native) | Android (react-native) | 
|---|---|
What changes do you think we should make in order to solve the problem?
The issue is a bug in react-native https://github.com/facebook/react-native/issues/31442:
- The JavaScript TextInput and ReactEditText Android state are not in sync
- The TextInput Android Native state over-rides the JavaScript state.
- onChangeText passes a plain text string to JavaScript, not rich text (text with spans and styles).
More info at https://github.com/Expensify/App/issues/21411#issuecomment-1611591137
The solution consists of:
- PR Over-riding onTextContextMenuItem in ReactEditText to copy/paste plain text instead of rich-text #38189 (https://stackoverflow.com/a/45319485/7295772).
- PR Removing the Paste as plaintextoption from the insert and selection context menu #38210
fixes https://github.com/facebook/react-native/issues/31442
Changelog:
[ANDROID] [FIXED] - Over-riding onTextContextMenuItem in ReactEditText to copy/paste plain text instead of rich-text
Test Plan:
Reproducing the issue on Android
https://user-images.githubusercontent.com/24992535/249185416-76f8a687-1aca-4dc9-9abe-3d73d6e2893c.mp4
Fixing the issue on Android
Sourcecode https://github.com/fabriziobertoglio1987/text-input-cursor-flickering-android/blob/fix-copy-paste/app/src/main/java/com/example/myapplication/CustomEditText.java
https://user-images.githubusercontent.com/24992535/249486339-95449bb9-71b6-430c-8207-f5672f034fa9.mp4
Testing the solution on React Native
https://github.com/Expensify/App/assets/24992535/b302237b-99e5-44a2-996d-8bc50bbbc95c
| Platform | Engine | Arch | Size (bytes) | Diff | 
|---|---|---|---|---|
| android | hermes | arm64-v8a | 8,843,572 | -39,854 | 
| android | hermes | armeabi-v7a | 8,152,772 | +218,558 | 
| android | hermes | x86 | 9,349,392 | +68,907 | 
| android | hermes | x86_64 | 9,192,090 | +8,476 | 
| android | jsc | arm64-v8a | 9,456,354 | -16,762 | 
| android | jsc | armeabi-v7a | 8,637,488 | +221,011 | 
| android | jsc | x86 | 9,539,440 | +83,566 | 
| android | jsc | x86_64 | 9,782,737 | +10,654 | 
Base commit: bae63d492fa8254547453229f28332f08e8b881c Branch: main
Building the main branch with head https://github.com/facebook/react-native/commit/06668fcbacd750771f1d53cce829dc55e86f3f3c triggers a runtime when opening the RNTester Text examples:
CLICK TO OPEN STACKTRACE
07-22 21:29:39.711 10861 10931 E AndroidRuntime: FATAL EXCEPTION: FrescoIoBoundExecutor-2
07-22 21:29:39.711 10861 10931 E AndroidRuntime: Process: com.facebook.react.uiapp, PID: 10861
07-22 21:29:39.711 10861 10931 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: library "libnative-imagetranscoder.so" not found
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.lang.Runtime.loadLibrary0(Runtime.java:998)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.lang.System.loadLibrary(System.java:1661)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.soloader.nativeloader.SystemDelegate.loadLibrary(SystemDelegate.java:24)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.soloader.nativeloader.NativeLoader.loadLibrary(NativeLoader.java:52)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.soloader.nativeloader.NativeLoader.loadLibrary(NativeLoader.java:30)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.nativecode.NativeJpegTranscoderSoLoader.ensure(NativeJpegTranscoderSoLoader.java:33)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.nativecode.NativeJpegTranscoder.<init>(NativeJpegTranscoder.java:59)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.nativecode.NativeJpegTranscoderFactory.createImageTranscoder(NativeJpegTranscoderFactory.java:43)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.transcoder.MultiImageTranscoderFactory.getNativeImageTranscoder(MultiImageTranscoderFactory.kt:59)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.transcoder.MultiImageTranscoderFactory.createImageTranscoder(MultiImageTranscoderFactory.kt:40)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.ResizeAndRotateProducer$TransformingConsumer.onNewResultImpl(ResizeAndRotateProducer.java:166)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.ResizeAndRotateProducer$TransformingConsumer.onNewResultImpl(ResizeAndRotateProducer.java:84)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.BaseConsumer.onNewResult(BaseConsumer.java:89)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.AddImageTransformMetaDataProducer$AddImageTransformMetaDataConsumer.onNewResultImpl(AddImageTransformMetaDataProducer.java:49)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.AddImageTransformMetaDataProducer$AddImageTransformMetaDataConsumer.onNewResultImpl(AddImageTransformMetaDataProducer.java:33)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.BaseConsumer.onNewResult(BaseConsumer.java:89)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.MultiplexProducer$Multiplexer.onNextResult(MultiplexProducer.java:510)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.MultiplexProducer$Multiplexer$ForwardingConsumer.onNewResultImpl(MultiplexProducer.java:569)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.MultiplexProducer$Multiplexer$ForwardingConsumer.onNewResultImpl(MultiplexProducer.java:562)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.BaseConsumer.onNewResult(BaseConsumer.java:89)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.EncodedMemoryCacheProducer$EncodedMemoryCacheConsumer.onNewResultImpl(EncodedMemoryCacheProducer.java:181)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.EncodedMemoryCacheProducer$EncodedMemoryCacheConsumer.onNewResultImpl(EncodedMemoryCacheProducer.java:123)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.BaseConsumer.onNewResult(BaseConsumer.java:89)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.DiskCacheReadProducer$1.then(DiskCacheReadProducer.java:113)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.producers.DiskCacheReadProducer$1.then(DiskCacheReadProducer.java:93)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task$14.run(Task.java:872)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.BoltsExecutors$ImmediateExecutor.execute(BoltsExecutors.java:105)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task.completeImmediately(Task.java:863)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task.access$000(Task.java:32)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task$10.then(Task.java:654)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task$10.then(Task.java:651)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task.runContinuations(Task.java:956)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task.trySetResult(Task.java:994)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.TaskCompletionSource.trySetResult(TaskCompletionSource.java:39)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.TaskCompletionSource.setResult(TaskCompletionSource.java:62)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at bolts.Task$4.run(Task.java:357)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.core.PriorityThreadFactory.newThread$lambda$0(PriorityThreadFactory.kt:37)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.core.PriorityThreadFactory.$r8$lambda$IPp7Vm9a1KDy8D4770JTjI9qOG4(Unknown Source:0)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at com.facebook.imagepipeline.core.PriorityThreadFactory$$ExternalSyntheticLambda0.run(Unknown Source:4)
07-22 21:29:39.711 10861 10931 E AndroidRuntime: 	at java.lang.Thread.run(Thread.java:1012)
https://github.com/facebook/react-native/assets/24992535/a8971f2b-f19a-4acc-a22e-fb7c1e34df86
Related https://github.com/facebook/react-native/commit/823839bcc13526a5a37a0d316f90a39f6bf283bd
Could you explain why you think the current behavior is a bug? At a glance, overriding a context menu command to paste rich text, to instead coerce to plain text, seems at odds with platform expectations.
Thanks @NickGerleman
Could you explain why you think the current behavior is a bug?
FIRST REASON: It was reported as a react-native TextInput Issue in different chat applications:
- Zulip Mobile https://github.com/zulip/zulip-mobile/issues/4660#issuecomment-1203296441
It's already the case that the only data TextInput actually exposes to the app is the plain text -- so the fact that it lets the user paste formatted text, and shows that formatting back, is basically a bug in RN because there's no way the app can end up respecting that formatting.
https://github.com/zulip/zulip-mobile/issues/4660#issuecomment-951338651
As described on this CZO thread, this issue can lead to a very awkward user experience.
- Expensify https://github.com/Expensify/App/issues/21411
- React Native Issue: https://github.com/facebook/react-native/issues/31442
SECOND REASON: React Native Controlled TextInput Component does not support paste with rich text.
- Native and JavaScript state are not in sync.
- The rich text pasted in the EditText over-rides the style of a JavaScript Controlled TextInput.
Example of a use case: The user pastes rich text in a JavaScript Controlled TextInput.
Expected behaviour:
- The JavaScript Controlled TextInput manages the TextInput state and over-rides the Native Android State. The JavaScript TextInput and Text style props set the native text style.
- onChangeText API is used to update the TextInput internal state and keep it sync with the updates from native.
Actual behaviour:
- The JavaScript TextInput and Text style props do not change the style of the text.
- onChangeText passes to JavaScript callback the plain text instead of rich text. It is not possible to keep the two states in sync.
https://user-images.githubusercontent.com/24992535/249502554-0f3a48c6-38ee-4847-be17-2aabebf52377.mp4
more info in comment https://github.com/Expensify/App/issues/21411#issuecomment-1611591137
At a glance, overriding a context menu command to paste rich text, to instead coerce to plain text, seems at odds with platform expectations.
The fix is published as two PRs:
- The first PR over-rides onTextContextMenuItem in ReactEditText to copy/paste plain text instead of rich-text#38189
- The second PR removes the option paste as plain text#38210
The final result is that the option paste as plain text is removed, while paste becomes paste as plain text.
| Before/After iOS | Before Android | 
|---|---|
| After Android | 
|---|
https://github.com/facebook/react-native/pull/38189#pullrequestreview-1542055466
Thanks a lot @NickGerleman. I combined #38210 with #38189. I tested the PR again and did not detect any issues.
@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.
@NickGerleman merged this pull request in facebook/react-native@b1ceea456d1cdc00c723582d00e5ae585f066b55.