diff --git a/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch new file mode 100644 index 000000000000..272bd313faaa --- /dev/null +++ b/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch @@ -0,0 +1,686 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index 55b770d..6d86715 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -464,6 +464,21 @@ export type NativeProps = $ReadOnly<{| + |}>, + >, + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ?DirectEventHandler< ++ $ReadOnly<{| ++ target: Int32, ++ items: $ReadOnlyArray< ++ $ReadOnly<{| ++ type: string, ++ data: string, ++ |}>, ++ >, ++ |}>, ++ >, ++ + /** + * The string that will be rendered before text input has been entered. + */ +@@ -668,6 +683,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + topScroll: { + registrationName: 'onScroll', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + maxFontSizeMultiplier: true, +@@ -719,6 +737,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + textBreakStrategy: true, + onScroll: true, + onContentSizeChange: true, ++ onPaste: true, + disableFullscreenUI: true, + includeFontPadding: true, + fontWeight: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index 88d3cc8..615c11f 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -97,6 +97,9 @@ const RCTTextInputViewConfig = { + topChangeSync: { + registrationName: 'onChangeSync', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + fontSize: true, +@@ -162,6 +165,7 @@ const RCTTextInputViewConfig = { + onSelectionChange: true, + onContentSizeChange: true, + onScroll: true, ++ onPaste: true, + onChangeSync: true, + onKeyPressSync: true, + onTextInput: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +index 2c0c099..2bf9d5a 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -483,6 +483,16 @@ export interface TextInputTextInputEventData { + range: {start: number; end: number}; + } + ++/** ++ * @see TextInputProps.onPaste ++ */ ++export interface TextInputPasteEventData extends TargetedEvent { ++ items: Array<{ ++ type: string; ++ data: string; ++ }>; ++} ++ + /** + * @see https://reactnative.dev/docs/textinput#props + */ +@@ -804,6 +814,13 @@ export interface TextInputProps + | ((e: NativeSyntheticEvent) => void) + | undefined; + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ++ | ((e: NativeSyntheticEvent) => void) ++ | undefined; ++ + /** + * The string that will be rendered before text input has been entered + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +index 9adbfe9..b46437d 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -94,6 +94,18 @@ export type EditingEvent = SyntheticEvent< + |}>, + >; + ++export type PasteEvent = SyntheticEvent< ++ $ReadOnly<{| ++ target: number, ++ items: $ReadOnlyArray< ++ $ReadOnly<{| ++ type: string, ++ data: string, ++ |}>, ++ >, ++ |}>, ++>; ++ + type DataDetectorTypesType = + | 'phoneNumber' + | 'link' +@@ -796,6 +808,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ?(e: PasteEvent) => mixed, ++ + /** + * The string that will be rendered before text input has been entered. + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 481938f..a74fda7 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -132,6 +132,18 @@ export type EditingEvent = SyntheticEvent< + |}>, + >; + ++export type PasteEvent = SyntheticEvent< ++ $ReadOnly<{| ++ target: number, ++ items: $ReadOnlyArray< ++ $ReadOnly<{| ++ type: string, ++ data: string, ++ |}>, ++ >, ++ |}>, ++>; ++ + type DataDetectorTypesType = + | 'phoneNumber' + | 'link' +@@ -838,6 +850,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Invoked when the user performs the paste action. ++ */ ++ onPaste?: ?(e: PasteEvent) => mixed, ++ + /** + * The string that will be rendered before text input has been entered. + */ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +index 582b49c..ac31cb1 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +@@ -13,6 +13,9 @@ + #import + #import + ++#import ++#import ++ + @implementation RCTUITextView { + UILabel *_placeholderView; + UITextView *_detachedTextView; +@@ -166,7 +169,30 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ for (NSItemProvider *itemProvider in [clipboard itemProviders]) { ++ if ([itemProvider canLoadObjectOfClass:[UIImage class]]) { ++ NSString *identifier = itemProvider.registeredTypeIdentifiers.firstObject; ++ if (identifier != nil) { ++ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); ++ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); ++ NSString *fileName = [NSString stringWithFormat:@"%@.%@", itemProvider.suggestedName ?: @"file", fileExtension]; ++ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; ++ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; ++ NSData *fileData = [clipboard dataForPasteboardType:identifier]; ++ [fileData writeToFile:filePath atomically:YES]; ++ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; ++ } ++ break; ++ } ++ } ++ } else { ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } ++ [super paste:sender]; ++ } + } + + // Turn off scroll animation to fix flaky scrolling. +@@ -258,6 +284,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +index 7187177..748c4cc 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN + - (void)textInputDidChange; + + - (void)textInputDidChangeSelection; ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data; + + @optional + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +index f1c32e6..0ce9dfe 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +@@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; + - (void)selectedTextRangeWasSet; ++- (void)didPaste:(NSString *)type withData:(NSString *)data; + + @end + +@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN + - (instancetype)initWithTextView:(UITextView *)backedTextInputView; + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; ++- (void)didPaste:(NSString *)type withData:(NSString *)data; + + @end + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +index 9dca6a5..b2c6b53 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +@@ -147,6 +147,11 @@ - (void)selectedTextRangeWasSet + [self textFieldProbablyDidChangeSelection]; + } + ++- (void)didPaste:(NSString *)type withData:(NSString *)data ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++} ++ + #pragma mark - Generalization + + - (void)textFieldProbablyDidChangeSelection +@@ -290,6 +295,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex + _previousSelectedTextRange = textRange; + } + ++- (void)didPaste:(NSString *)type withData:(NSString *)data ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++} ++ + #pragma mark - Generalization + + - (void)textViewProbablyDidChangeSelection +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +index 209947d..5092dbd 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +@@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; ++@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; + + @property (nonatomic, assign) NSInteger mostRecentEventCount; + @property (nonatomic, assign, readonly) NSInteger nativeEventCount; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +index b0d71dc..2e42fc9 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +@@ -562,6 +562,26 @@ - (void)textInputDidChangeSelection + }); + } + ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++{ ++ if (!_onPaste) { ++ return; ++ } ++ ++ NSMutableArray *items = [NSMutableArray new]; ++ [items addObject:@{ ++ @"type" : type, ++ @"data" : data, ++ }]; ++ ++ NSDictionary *payload = @{ ++ @"target" : self.reactTag, ++ @"items" : items, ++ }; ++ ++ _onPaste(payload); ++} ++ + - (void)updateLocalData + { + [self enforceTextAttributesIfNeeded]; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index a19b555..8146c0d 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -66,6 +66,7 @@ @implementation RCTBaseTextInputViewManager { + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) + + RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +index 4d0afd9..a6afc7b 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +@@ -12,6 +12,9 @@ + #import + #import + ++#import ++#import ++ + @implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; + NSDictionary *_defaultTextAttributes; +@@ -139,6 +142,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +@@ -204,7 +211,30 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ for (NSItemProvider *itemProvider in [clipboard itemProviders]) { ++ if ([itemProvider canLoadObjectOfClass:[UIImage class]]) { ++ NSString *identifier = itemProvider.registeredTypeIdentifiers.firstObject; ++ if (identifier != nil) { ++ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); ++ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); ++ NSString *fileName = [NSString stringWithFormat:@"%@.%@", itemProvider.suggestedName ?: @"file", fileExtension]; ++ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; ++ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; ++ NSData *fileData = [clipboard dataForPasteboardType:identifier]; ++ [fileData writeToFile:filePath atomically:YES]; ++ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; ++ } ++ break; ++ } ++ } ++ } else { ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } ++ [super paste:sender]; ++ } + } + + #pragma mark - Layout +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 123968f..9626d49 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -426,6 +426,13 @@ - (void)textInputDidChangeSelection + } + } + ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++{ ++ if (_eventEmitter) { ++ static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); ++ } ++} ++ + #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) + + - (void)scrollViewDidScroll:(UIScrollView *)scrollView +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java +new file mode 100644 +index 0000000..bfb5819 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java +@@ -0,0 +1,17 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput; ++ ++/** ++ * Implement this interface to be informed of paste event in the ++ * ReactTextEdit This is used by the ReactTextInputManager to forward events ++ * from the EditText to JS ++ */ ++interface PasteWatcher { ++ public void onPaste(String type, String data); ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +index 081f2b8..ff91d47 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +@@ -9,14 +9,17 @@ package com.facebook.react.views.textinput; + + import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; + +-import android.content.ClipData; + import android.content.ClipboardManager; ++import android.content.ClipData; ++import android.content.ClipDescription; ++import android.content.ContentResolver; + import android.content.Context; + import android.graphics.Color; + import android.graphics.Paint; + import android.graphics.Rect; + import android.graphics.Typeface; + import android.graphics.drawable.Drawable; ++import android.net.Uri; + import android.os.Build; + import android.os.Bundle; + import android.text.Editable; +@@ -110,6 +113,7 @@ public class ReactEditText extends AppCompatEditText { + private @Nullable SelectionWatcher mSelectionWatcher; + private @Nullable ContentSizeWatcher mContentSizeWatcher; + private @Nullable ScrollWatcher mScrollWatcher; ++ private @Nullable PasteWatcher mPasteWatcher; + private InternalKeyListener mKeyListener; + private boolean mDetectScrollMovement = false; + private boolean mOnKeyPress = false; +@@ -153,6 +157,7 @@ public class ReactEditText extends AppCompatEditText { + mKeyListener = new InternalKeyListener(); + } + mScrollWatcher = null; ++ mPasteWatcher = null; + mTextAttributes = new TextAttributes(); + + applyTextAttributes(); +@@ -307,10 +312,31 @@ public class ReactEditText extends AppCompatEditText { + */ + @Override + public boolean onTextContextMenuItem(int id) { +- if (id == android.R.id.paste) { ++ if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + id = android.R.id.pasteAsPlainText; +- } else { ++ if (mPasteWatcher != null) { ++ ClipboardManager clipboardManager = ++ (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ++ ClipData clipData = clipboardManager.getPrimaryClip(); ++ String type = null; ++ String data = null; ++ if (clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { ++ type = ClipDescription.MIMETYPE_TEXT_PLAIN; ++ data = clipData.getItemAt(0).getText().toString(); ++ } else { ++ Uri itemUri = clipData.getItemAt(0).getUri(); ++ if (itemUri != null) { ++ ContentResolver cr = getReactContext(this).getContentResolver(); ++ type = cr.getType(itemUri); ++ data = itemUri.toString(); ++ } ++ } ++ if (type != null && data != null) { ++ mPasteWatcher.onPaste(type, data); ++ } ++ } ++ } else if (id == android.R.id.paste) { + ClipboardManager clipboard = + (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData previousClipData = clipboard.getPrimaryClip(); +@@ -389,6 +415,10 @@ public class ReactEditText extends AppCompatEditText { + mScrollWatcher = scrollWatcher; + } + ++ public void setPasteWatcher(PasteWatcher pasteWatcher) { ++ mPasteWatcher = pasteWatcher; ++ } ++ + /** + * Attempt to set a selection or fail silently. Intentionally meant to handle bad inputs. + * EventCounter is the same one used as with text. +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index e6bcfc4..902985d 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -273,6 +273,9 @@ public class ReactTextInputManager extends BaseViewManager { ++ ++ private static final String EVENT_NAME = "topPaste"; ++ ++ private String mType; ++ private String mData; ++ ++ @Deprecated ++ public ReactTextInputPasteEvent(int viewId, String type, String data) { ++ this(ViewUtil.NO_SURFACE_ID, viewId, type, data); ++ } ++ ++ public ReactTextInputPasteEvent(int surfaceId, int viewId, String type, String data) { ++ super(surfaceId, viewId); ++ mType = type; ++ mData = data; ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Override ++ public boolean canCoalesce() { ++ return false; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ ++ WritableArray items = Arguments.createArray(); ++ WritableMap item = Arguments.createMap(); ++ item.putString("type", mType); ++ item.putString("data", mData); ++ items.pushMap(item); ++ ++ eventData.putArray("items", items); ++ ++ return eventData; ++ } ++} +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +index 497569a..1a84b47 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +@@ -193,6 +193,19 @@ void TextInputEventEmitter::onScroll( + }); + } + ++void TextInputEventEmitter::onPaste(const std::string& type, const std::string& data) const { ++ dispatchEvent("onPaste", [type, data](jsi::Runtime& runtime) { ++ auto payload = jsi::Object(runtime); ++ auto items = jsi::Array(runtime, 1); ++ auto item = jsi::Object(runtime); ++ item.setProperty(runtime, "type", type); ++ item.setProperty(runtime, "data", data); ++ items.setValueAtIndex(runtime, 0, item); ++ payload.setProperty(runtime, "items", items); ++ return payload; ++ }); ++} ++ + void TextInputEventEmitter::dispatchTextInputEvent( + const std::string& name, + const TextInputMetrics& textInputMetrics, +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h +index 0ab2b18..781f083 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h +@@ -47,6 +47,7 @@ class TextInputEventEmitter : public ViewEventEmitter { + void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; + void onKeyPressSync(const KeyPressMetrics& keyPressMetrics) const; + void onScroll(const TextInputMetrics& textInputMetrics) const; ++ void onPaste(const std::string& type, const std::string& data) const; + + private: + void dispatchTextInputEvent( diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index d5dc4c12afc0..4b791e8d1034 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,8 +1,9 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import type {TextInput} from 'react-native'; +import type {NativeSyntheticEvent, TextInput, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {FileObject} from '@components/AttachmentModal'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; @@ -21,6 +22,7 @@ function Composer( { shouldClear = false, onClear = () => {}, + onPasteFile = () => {}, isDisabled = false, maxLines, isComposerFullSize = false, @@ -64,6 +66,20 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const pasteFile = useCallback( + (e: NativeSyntheticEvent) => { + const clipboardContent = e.nativeEvent.items[0]; + if (clipboardContent.type === 'text/plain') { + return; + } + const fileURI = clipboardContent.data; + const fileName = fileURI.split('/').pop(); + const file: FileObject = {uri: fileURI, name: fileName, type: clipboardContent.type}; + onPasteFile(file); + }, + [onPasteFile], + ); + useEffect(() => { if (!shouldClear) { return; @@ -92,6 +108,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} + onPaste={pasteFile} onBlur={(e) => { if (!isFocused) { // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9c7a5a215c1c..a2d062021205 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,5 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {FileObject} from '@components/AttachmentModal'; type TextSelection = { start: number; @@ -31,7 +32,7 @@ type ComposerProps = TextInputProps & { onChangeText?: (numberOfLines: string) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file: File) => void; + onPasteFile?: (file: FileObject) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/stories/Composer.stories.tsx b/src/stories/Composer.stories.tsx index 805f2b4c7448..a92dc0e789a0 100644 --- a/src/stories/Composer.stories.tsx +++ b/src/stories/Composer.stories.tsx @@ -3,6 +3,7 @@ import type {Meta} from '@storybook/react'; import {ExpensiMark} from 'expensify-common'; import React, {useState} from 'react'; import {Image, View} from 'react-native'; +import type {FileObject} from '@components/AttachmentModal'; import Composer from '@components/Composer'; import type {ComposerProps} from '@components/Composer/types'; import RenderHTML from '@components/RenderHTML'; @@ -29,7 +30,7 @@ const parser = new ExpensiMark(); function Default(props: ComposerProps) { const StyleUtils = useStyleUtils(); - const [pastedFile, setPastedFile] = useState(null); + const [pastedFile, setPastedFile] = useState(null); const [comment, setComment] = useState(props.defaultValue); const renderedHTML = parser.replace(comment ?? ''); @@ -53,7 +54,7 @@ function Default(props: ComposerProps) { Rendered Comment {!!renderedHTML && } - {!!pastedFile && ( + {!!pastedFile && pastedFile instanceof File && (