From 93e3d4549d375cac711606fc5a537187b47c1868 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:31:46 +0100 Subject: [PATCH 1/8] Support image pasting --- ...+0.73.4+018+Add-onPaste-to-TextInput.patch | 458 ++++++++++++++++++ ...-native+0.73.4+019+iOS-Image-Pasting.patch | 70 +++ src/components/Composer/index.native.tsx | 12 + src/components/Composer/types.ts | 3 +- .../{index.native.ts => index.android.ts} | 12 +- src/libs/Clipboard/index.ios.ts | 36 ++ src/libs/Clipboard/index.ts | 5 +- src/libs/Clipboard/types.ts | 5 +- 8 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch create mode 100644 patches/react-native+0.73.4+019+iOS-Image-Pasting.patch rename src/libs/Clipboard/{index.native.ts => index.android.ts} (57%) create mode 100644 src/libs/Clipboard/index.ios.ts diff --git a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch new file mode 100644 index 000000000000..53acbd03e82e --- /dev/null +++ b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch @@ -0,0 +1,458 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index 55b770d..7677336 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -464,6 +464,11 @@ export type NativeProps = $ReadOnly<{| + |}>, + >, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?DirectEventHandler<$ReadOnly<{|target: Int32|}>>, ++ + /** + * The string that will be rendered before text input has been entered. + */ +@@ -668,6 +673,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + topScroll: { + registrationName: 'onScroll', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + maxFontSizeMultiplier: true, +@@ -719,6 +727,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..e5fbc79 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -804,6 +804,11 @@ export interface TextInputProps + | ((e: NativeSyntheticEvent) => void) + | undefined; + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ 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..69ac611 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -796,6 +796,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?(e: TargetEvent) => 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..3f92f75 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -838,6 +838,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?(e: TargetEvent) => 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..43c6c7d 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +@@ -167,6 +167,7 @@ - (void)paste:(id)sender + { + _textWasPasted = YES; + [super paste:sender]; ++ [_textInputDelegateAdapter didPaste]; + } + + // Turn off scroll animation to fix flaky scrolling. +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +index 7187177..aaa6a17 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; + + @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..2b91f03 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; + + @end + +@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN + - (instancetype)initWithTextView:(UITextView *)backedTextInputView; + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; ++- (void)didPaste; + + @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..6814c57 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 ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++} ++ + #pragma mark - Generalization + + - (void)textFieldProbablyDidChangeSelection +@@ -290,6 +295,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex + _previousSelectedTextRange = textRange; + } + ++- (void)didPaste ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++} ++ + #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..f64dd69 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +@@ -562,6 +562,14 @@ - (void)textInputDidChangeSelection + }); + } + ++- (void)textInputDidPaste ++{ ++ if (!_onPaste) { ++ return; ++ } ++ _onPaste(@{@"target" : self.reactTag}); ++} ++ + - (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..10009ff 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +@@ -205,6 +205,7 @@ - (void)paste:(id)sender + { + _textWasPasted = YES; + [super paste:sender]; ++ [_textInputDelegateAdapter didPaste]; + } + + #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 7ce04da..97c502d 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 ++{ ++ if (_eventEmitter) { ++ static_cast(*_eventEmitter).onPaste(); ++ } ++} ++ + #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..7eacc51 +--- /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(); ++} +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..98c0f0d 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 +@@ -110,6 +110,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 +154,7 @@ public class ReactEditText extends AppCompatEditText { + mKeyListener = new InternalKeyListener(); + } + mScrollWatcher = null; ++ mPasteWatcher = null; + mTextAttributes = new TextAttributes(); + + applyTextAttributes(); +@@ -307,10 +309,15 @@ 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 { ++ boolean actionPerformed = super.onTextContextMenuItem(id); ++ if (mPasteWatcher != null) { ++ mPasteWatcher.onPaste(); ++ } ++ return actionPerformed; ++ } else if (id == android.R.id.paste) { + ClipboardManager clipboard = + (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData previousClipData = clipboard.getPrimaryClip(); +@@ -389,6 +396,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 8496a7d..deb54d4 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"; ++ ++ @Deprecated ++ public ReactTextInputPasteEvent(int viewId) { ++ this(ViewUtil.NO_SURFACE_ID, viewId); ++ } ++ ++ public ReactTextInputPasteEvent(int surfaceId, int viewId) { ++ super(surfaceId, viewId); ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Override ++ public boolean canCoalesce() { ++ return false; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ 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..c6adbc3 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,10 @@ void TextInputEventEmitter::onScroll( + }); + } + ++void TextInputEventEmitter::onPaste() const { ++ dispatchEvent("onPaste"); ++} ++ + 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..6373012 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; + + private: + void dispatchTextInputEvent( diff --git a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch new file mode 100644 index 000000000000..3b681679c470 --- /dev/null +++ b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch @@ -0,0 +1,70 @@ +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 43c6c7d..5904762 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,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextView { + UILabel *_placeholderView; + UITextView *_detachedTextView; +@@ -166,7 +168,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ if (![UIPasteboard generalPasteboard].hasImages) { ++ [super paste:sender]; ++ } + [_textInputDelegateAdapter didPaste]; + } + +@@ -259,6 +263,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/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +index 10009ff..273bc0a 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,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; + NSDictionary *_defaultTextAttributes; +@@ -139,6 +141,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 +210,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ if (![UIPasteboard generalPasteboard].hasImages) { ++ [super paste:sender]; ++ } + [_textInputDelegateAdapter didPaste]; + } + diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c5f2e07eef80..13290a03bd5e 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -10,6 +10,7 @@ import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; import type {ComposerProps} from './types'; @@ -21,6 +22,7 @@ function Composer( { shouldClear = false, onClear = () => {}, + onPasteFile = () => {}, isDisabled = false, maxLines, isComposerFullSize = false, @@ -64,6 +66,15 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const pasteImage = useCallback(() => { + Clipboard.getImage().then((image) => { + if (!image) { + return; + } + onPasteFile(image); + }); + }, [onPasteFile]); + useEffect(() => { if (!shouldClear) { return; @@ -92,6 +103,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} + onPaste={pasteImage} 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..543649c2cca0 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 {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/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.android.ts similarity index 57% rename from src/libs/Clipboard/index.native.ts rename to src/libs/Clipboard/index.android.ts index 2c345327a106..bb3cb22ab66f 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.android.ts @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import type {CanSetHtml, SetHtml, SetString} from './types'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; /** * Sets a string on the Clipboard object via @react-native-clipboard/clipboard @@ -12,8 +12,18 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; +const getImage: GetImage = () => { + return Clipboard.getImage().then((imageb64) => { + if (!imageb64) { + return undefined; + } + return {uri: imageb64, name: 'image.png', type: 'image/png'}; + }); +}; + export default { setString, canSetHtml, setHtml, + getImage, }; diff --git a/src/libs/Clipboard/index.ios.ts b/src/libs/Clipboard/index.ios.ts new file mode 100644 index 000000000000..c214bdfc76e6 --- /dev/null +++ b/src/libs/Clipboard/index.ios.ts @@ -0,0 +1,36 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; + +/** + * Sets a string on the Clipboard object via @react-native-clipboard/clipboard + */ +const setString: SetString = (text) => { + Clipboard.setString(text); +}; + +// We don't want to set HTML on native platforms so noop them. +const canSetHtml: CanSetHtml = () => false; +const setHtml: SetHtml = () => {}; + +const getImage: GetImage = () => { + return Clipboard.hasImage() + .then((hasImage) => { + if (!hasImage) { + return undefined; + } + return Clipboard.getImagePNG(); + }) + .then((imageb64) => { + if (!imageb64) { + return undefined; + } + return {uri: imageb64, name: 'image.png', type: 'image/png'}; + }); +}; + +export default { + setString, + canSetHtml, + setHtml, + getImage, +}; diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 130aad270b92..a03e406ec9c0 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -import type {CanSetHtml, SetHtml, SetString} from './types'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; type ComposerSelection = { start: number; @@ -136,8 +136,11 @@ const setString: SetString = (text) => { Clipboard.setString(text); }; +const getImage: GetImage = () => Promise.reject('getImage not supported on web'); + export default { setString, canSetHtml, setHtml, + getImage, }; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index 1d899144a2ba..e54cf60df635 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,5 +1,8 @@ +import {FileObject} from '@components/AttachmentModal'; + type SetString = (text: string) => void; type SetHtml = (html: string, text: string) => void; type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); +type GetImage = () => Promise; -export type {SetString, CanSetHtml, SetHtml}; +export type {SetString, CanSetHtml, SetHtml, GetImage}; From a6dee6d29503dfed581092387ed56b1d97a6beb6 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:24:40 +0100 Subject: [PATCH 2/8] lint + ts --- src/components/Composer/types.ts | 2 +- src/libs/Clipboard/index.android.ts | 5 ++--- src/libs/Clipboard/index.ios.ts | 5 ++--- src/libs/Clipboard/index.ts | 2 +- src/libs/Clipboard/types.ts | 2 +- src/stories/Composer.stories.tsx | 5 +++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 543649c2cca0..a2d062021205 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,5 +1,5 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; -import {FileObject} from '@components/AttachmentModal'; +import type {FileObject} from '@components/AttachmentModal'; type TextSelection = { start: number; diff --git a/src/libs/Clipboard/index.android.ts b/src/libs/Clipboard/index.android.ts index bb3cb22ab66f..6e412441b194 100644 --- a/src/libs/Clipboard/index.android.ts +++ b/src/libs/Clipboard/index.android.ts @@ -12,14 +12,13 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; -const getImage: GetImage = () => { - return Clipboard.getImage().then((imageb64) => { +const getImage: GetImage = () => + Clipboard.getImage().then((imageb64) => { if (!imageb64) { return undefined; } return {uri: imageb64, name: 'image.png', type: 'image/png'}; }); -}; export default { setString, diff --git a/src/libs/Clipboard/index.ios.ts b/src/libs/Clipboard/index.ios.ts index c214bdfc76e6..4b2ba52b9672 100644 --- a/src/libs/Clipboard/index.ios.ts +++ b/src/libs/Clipboard/index.ios.ts @@ -12,8 +12,8 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; -const getImage: GetImage = () => { - return Clipboard.hasImage() +const getImage: GetImage = () => + Clipboard.hasImage() .then((hasImage) => { if (!hasImage) { return undefined; @@ -26,7 +26,6 @@ const getImage: GetImage = () => { } return {uri: imageb64, name: 'image.png', type: 'image/png'}; }); -}; export default { setString, diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index a03e406ec9c0..fb0f00e6eb84 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -136,7 +136,7 @@ const setString: SetString = (text) => { Clipboard.setString(text); }; -const getImage: GetImage = () => Promise.reject('getImage not supported on web'); +const getImage: GetImage = () => Promise.reject(new Error('getImage not supported on web')); export default { setString, diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index e54cf60df635..c04cf952f1f7 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,4 +1,4 @@ -import {FileObject} from '@components/AttachmentModal'; +import type {FileObject} from '@components/AttachmentModal'; type SetString = (text: string) => void; type SetHtml = (html: string, text: string) => void; 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 && ( Date: Mon, 22 Jul 2024 09:17:53 +0100 Subject: [PATCH 3/8] update patch --- ...+0.73.4+018+Add-onPaste-to-TextInput.patch | 48 +++++++++---------- ...-native+0.73.4+019+iOS-Image-Pasting.patch | 30 ++++++------ 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch index 53acbd03e82e..bba53a8c6da4 100644 --- a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch +++ b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..7677336 100644 +index 55b770d..1df5807 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -464,6 +464,11 @@ export type NativeProps = $ReadOnly<{| @@ -7,10 +7,10 @@ index 55b770d..7677336 100644 >, + /** -+ * Callback that is called when the clipboard content is pasted. ++ * Invoked when the user performs the paste action. + */ + onPaste?: ?DirectEventHandler<$ReadOnly<{|target: Int32|}>>, -+ ++ /** * The string that will be rendered before text input has been entered. */ @@ -55,7 +55,7 @@ index 88d3cc8..615c11f 100644 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..e5fbc79 100644 +index 2c0c099..3fd345e 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -804,6 +804,11 @@ export interface TextInputProps @@ -63,7 +63,7 @@ index 2c0c099..e5fbc79 100644 | undefined; + /** -+ * Callback that is called when the clipboard content is pasted. ++ * Invoked when the user performs the paste action. + */ + onPaste?: ((e: NativeSyntheticEvent) => void) | undefined; + @@ -71,7 +71,7 @@ index 2c0c099..e5fbc79 100644 * 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..69ac611 100644 +index 9adbfe9..31ef086 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -796,6 +796,11 @@ export type Props = $ReadOnly<{| @@ -79,7 +79,7 @@ index 9adbfe9..69ac611 100644 onScroll?: ?(e: ScrollEvent) => mixed, + /** -+ * Callback that is called when the clipboard content is pasted. ++ * Invoked when the user performs the paste action. + */ + onPaste?: ?(e: TargetEvent) => mixed, + @@ -87,7 +87,7 @@ index 9adbfe9..69ac611 100644 * 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..3f92f75 100644 +index 481938f..7709bd1 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js @@ -838,6 +838,11 @@ export type Props = $ReadOnly<{| @@ -95,7 +95,7 @@ index 481938f..3f92f75 100644 onScroll?: ?(e: ScrollEvent) => mixed, + /** -+ * Callback that is called when the clipboard content is pasted. ++ * Invoked when the user performs the paste action. + */ + onPaste?: ?(e: TargetEvent) => mixed, + @@ -103,17 +103,17 @@ index 481938f..3f92f75 100644 * 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..43c6c7d 100644 +index 582b49c..5076951 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -@@ -167,6 +167,7 @@ - (void)paste:(id)sender +@@ -166,6 +166,7 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + [_textInputDelegateAdapter didPaste]; + [super paste:sender]; } - // Turn off scroll animation to fix flaky scrolling. diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index 7187177..aaa6a17 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -218,17 +218,17 @@ index a19b555..8146c0d 100644 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..10009ff 100644 +index 4d0afd9..07179be 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -@@ -205,6 +205,7 @@ - (void)paste:(id)sender +@@ -204,6 +204,7 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + [_textInputDelegateAdapter didPaste]; + [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 7ce04da..97c502d 100644 --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -271,7 +271,7 @@ index 0000000..7eacc51 + public void onPaste(); +} 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..98c0f0d 100644 +index 081f2b8..c50912a 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 @@ -110,6 +110,7 @@ public class ReactEditText extends AppCompatEditText { @@ -290,25 +290,23 @@ index 081f2b8..98c0f0d 100644 mTextAttributes = new TextAttributes(); applyTextAttributes(); -@@ -307,10 +309,15 @@ public class ReactEditText extends AppCompatEditText { +@@ -307,10 +309,13 @@ 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 (mPasteWatcher != null) { ++ mPasteWatcher.onPaste(); ++ } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { id = android.R.id.pasteAsPlainText; - } else { -+ boolean actionPerformed = super.onTextContextMenuItem(id); -+ if (mPasteWatcher != null) { -+ mPasteWatcher.onPaste(); -+ } -+ return actionPerformed; + } else if (id == android.R.id.paste) { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData previousClipData = clipboard.getPrimaryClip(); -@@ -389,6 +396,10 @@ public class ReactEditText extends AppCompatEditText { +@@ -389,6 +394,10 @@ public class ReactEditText extends AppCompatEditText { mScrollWatcher = scrollWatcher; } diff --git a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch index 3b681679c470..50e0b578ac1c 100644 --- a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch +++ b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch @@ -1,5 +1,5 @@ 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 43c6c7d..5904762 100644 +index 5076951..40eab80 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,8 @@ @@ -11,18 +11,17 @@ index 43c6c7d..5904762 100644 @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; -@@ -166,7 +168,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - - (void)paste:(id)sender +@@ -167,6 +169,9 @@ - (void)paste:(id)sender { _textWasPasted = YES; -- [super paste:sender]; -+ if (![UIPasteboard generalPasteboard].hasImages) { -+ [super paste:sender]; -+ } [_textInputDelegateAdapter didPaste]; ++ if ([UIPasteboard generalPasteboard].hasImages) { ++ return; ++ } + [super paste:sender]; } -@@ -259,6 +263,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender +@@ -259,6 +264,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } @@ -34,7 +33,7 @@ index 43c6c7d..5904762 100644 } 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 10009ff..273bc0a 100644 +index 07179be..311420e 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,8 @@ @@ -57,14 +56,13 @@ index 10009ff..273bc0a 100644 return [super canPerformAction:action withSender:sender]; } -@@ -204,7 +210,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - - (void)paste:(id)sender +@@ -205,6 +211,9 @@ - (void)paste:(id)sender { _textWasPasted = YES; -- [super paste:sender]; -+ if (![UIPasteboard generalPasteboard].hasImages) { -+ [super paste:sender]; -+ } [_textInputDelegateAdapter didPaste]; ++ if ([UIPasteboard generalPasteboard].hasImages) { ++ return; ++ } + [super paste:sender]; } - + \ No newline at end of file From 3a457baf69f7b6404f489ed3568da72fa3840f41 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:22:19 +0100 Subject: [PATCH 4/8] use file from paste event --- package-lock.json | 6 + package.json | 1 + ...-native+0.73.4+019+iOS-Image-Pasting.patch | 68 --- ...0.73.4+024+Add-onPaste-to-TextInput.patch} | 421 +++++++++++++++--- src/components/Composer/index.native.tsx | 23 +- src/libs/Clipboard/index.ios.ts | 35 -- .../{index.android.ts => index.native.ts} | 11 +- src/libs/Clipboard/index.ts | 5 +- src/libs/Clipboard/types.ts | 5 +- 9 files changed, 379 insertions(+), 196 deletions(-) delete mode 100644 patches/react-native+0.73.4+019+iOS-Image-Pasting.patch rename patches/{react-native+0.73.4+018+Add-onPaste-to-TextInput.patch => react-native+0.73.4+024+Add-onPaste-to-TextInput.patch} (52%) delete mode 100644 src/libs/Clipboard/index.ios.ts rename src/libs/Clipboard/{index.android.ts => index.native.ts} (58%) diff --git a/package-lock.json b/package-lock.json index b7ccd396db6c..d550c7ddb9f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", + "@types/mime-db": "^1.43.5", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -17588,6 +17589,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mime-db": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.5.tgz", + "integrity": "sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "dev": true, diff --git a/package.json b/package.json index 494487a7c676..4d1236f2933a 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", + "@types/mime-db": "^1.43.5", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", diff --git a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch deleted file mode 100644 index 50e0b578ac1c..000000000000 --- a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch +++ /dev/null @@ -1,68 +0,0 @@ -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 5076951..40eab80 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,8 @@ - #import - #import - -+#import -+ - @implementation RCTUITextView { - UILabel *_placeholderView; - UITextView *_detachedTextView; -@@ -167,6 +169,9 @@ - (void)paste:(id)sender - { - _textWasPasted = YES; - [_textInputDelegateAdapter didPaste]; -+ if ([UIPasteboard generalPasteboard].hasImages) { -+ return; -+ } - [super paste:sender]; - } - -@@ -259,6 +264,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/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -index 07179be..311420e 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,8 @@ - #import - #import - -+#import -+ - @implementation RCTUITextField { - RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; - NSDictionary *_defaultTextAttributes; -@@ -139,6 +141,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]; - } - -@@ -205,6 +211,9 @@ - (void)paste:(id)sender - { - _textWasPasted = YES; - [_textInputDelegateAdapter didPaste]; -+ if ([UIPasteboard generalPasteboard].hasImages) { -+ return; -+ } - [super paste:sender]; - } - \ No newline at end of file diff --git a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch similarity index 52% rename from patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch rename to patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch index bba53a8c6da4..00bb93555575 100644 --- a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch +++ b/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch @@ -1,20 +1,30 @@ diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..1df5807 100644 +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,11 @@ export type NativeProps = $ReadOnly<{| +@@ -464,6 +464,21 @@ export type NativeProps = $ReadOnly<{| |}>, >, + /** + * Invoked when the user performs the paste action. + */ -+ onPaste?: ?DirectEventHandler<$ReadOnly<{|target: Int32|}>>, -+ ++ 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 +673,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { +@@ -668,6 +683,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { topScroll: { registrationName: 'onScroll', }, @@ -24,7 +34,7 @@ index 55b770d..1df5807 100644 }, validAttributes: { maxFontSizeMultiplier: true, -@@ -719,6 +727,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { +@@ -719,6 +737,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { textBreakStrategy: true, onScroll: true, onContentSizeChange: true, @@ -55,86 +65,206 @@ index 88d3cc8..615c11f 100644 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..3fd345e 100644 +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 -@@ -804,6 +804,11 @@ export interface TextInputProps +@@ -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; ++ 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..31ef086 100644 +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 -@@ -796,6 +796,11 @@ export type Props = $ReadOnly<{| +@@ -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: TargetEvent) => mixed, ++ 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..7709bd1 100644 +index 481938f..a74fda7 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -838,6 +838,11 @@ export type Props = $ReadOnly<{| +@@ -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: TargetEvent) => mixed, ++ 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..5076951 100644 +index 582b49c..ae3e967 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -@@ -166,6 +166,7 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO +@@ -13,6 +13,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextView { + UILabel *_placeholderView; + UITextView *_detachedTextView; +@@ -166,7 +168,49 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - (void)paste:(id)sender { _textWasPasted = YES; -+ [_textInputDelegateAdapter didPaste]; - [super paste:sender]; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ NSArray *clipboardTypes = [clipboard pasteboardTypes]; ++ if ([clipboardTypes containsObject:@"com.compuserve.gif"]) { ++ NSString *type = @"image/gif"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.compuserve.gif"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.png"]) { ++ NSString *type = @"image/png"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.png"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.jpeg"]) { ++ NSString *type = @"image/jpeg"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.jpeg"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"org.webmproject.webp"]) { ++ NSString *type = @"image/webp"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"org.webmproject.webp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.tiff"]) { ++ NSString *type = @"image/tiff"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.tiff"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"com.microsoft.bmp"]) { ++ NSString *type = @"image/bmp"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.microsoft.bmp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.svg-image"]) { ++ NSString *type = @"image/svg+xml"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.svg-image"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } ++ } else { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ [super paste:sender]; ++ } + } + + // Turn off scroll animation to fix flaky scrolling. +@@ -258,6 +302,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..aaa6a17 100644 +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; ++- (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..2b91f03 100644 +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; ++- (void)didPaste:(NSString *)type withData:(NSString *)data; @end @@ -142,21 +272,21 @@ index f1c32e6..2b91f03 100644 - (instancetype)initWithTextView:(UITextView *)backedTextInputView; - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; -+- (void)didPaste; ++- (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..6814c57 100644 +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 ++- (void)didPaste:(NSString *)type withData:(NSString *)data +{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; +} + #pragma mark - Generalization @@ -166,9 +296,9 @@ index 9dca6a5..6814c57 100644 _previousSelectedTextRange = textRange; } -+- (void)didPaste ++- (void)didPaste:(NSString *)type withData:(NSString *)data +{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; +} + #pragma mark - Generalization @@ -187,19 +317,31 @@ index 209947d..5092dbd 100644 @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..f64dd69 100644 +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,14 @@ - (void)textInputDidChangeSelection +@@ -562,6 +562,26 @@ - (void)textInputDidChangeSelection }); } -+- (void)textInputDidPaste ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data +{ + if (!_onPaste) { + return; + } -+ _onPaste(@{@"target" : self.reactTag}); ++ ++ NSMutableArray *items = [NSMutableArray new]; ++ [items addObject:@{ ++ @"type" : type, ++ @"data" : data, ++ }]; ++ ++ NSDictionary *payload = @{ ++ @"target" : self.reactTag, ++ @"items" : items, ++ }; ++ ++ _onPaste(payload); +} + - (void)updateLocalData @@ -218,29 +360,92 @@ index a19b555..8146c0d 100644 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..07179be 100644 +index 4d0afd9..a611f69 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -@@ -204,6 +204,7 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO +@@ -12,6 +12,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; + NSDictionary *_defaultTextAttributes; +@@ -139,6 +141,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 +210,49 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - (void)paste:(id)sender { _textWasPasted = YES; -+ [_textInputDelegateAdapter didPaste]; - [super paste:sender]; +- [super paste:sender]; ++ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; ++ if (clipboard.hasImages) { ++ NSArray *clipboardTypes = [clipboard pasteboardTypes]; ++ if ([clipboardTypes containsObject:@"com.compuserve.gif"]) { ++ NSString *type = @"image/gif"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.compuserve.gif"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.png"]) { ++ NSString *type = @"image/png"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.png"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.jpeg"]) { ++ NSString *type = @"image/jpeg"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.jpeg"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"org.webmproject.webp"]) { ++ NSString *type = @"image/webp"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"org.webmproject.webp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.tiff"]) { ++ NSString *type = @"image/tiff"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.tiff"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"com.microsoft.bmp"]) { ++ NSString *type = @"image/bmp"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.microsoft.bmp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } else if ([clipboardTypes containsObject:@"public.svg-image"]) { ++ NSString *type = @"image/svg+xml"; ++ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.svg-image"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; ++ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; ++ [_textInputDelegateAdapter didPaste:type withData:data]; ++ } ++ } else { ++ [_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 7ce04da..97c502d 100644 +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 ++- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data +{ + if (_eventEmitter) { -+ static_cast(*_eventEmitter).onPaste(); ++ static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); + } +} + @@ -249,7 +454,7 @@ index 7ce04da..97c502d 100644 - (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..7eacc51 +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 @@ @@ -268,13 +473,48 @@ index 0000000..7eacc51 + * from the EditText to JS + */ +interface PasteWatcher { -+ public void onPaste(); ++ 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..c50912a 100644 +index 081f2b8..cf8f04d 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 -@@ -110,6 +110,7 @@ public class ReactEditText extends AppCompatEditText { +@@ -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; +@@ -28,6 +31,7 @@ import android.text.TextUtils; + import android.text.TextWatcher; + import android.text.method.KeyListener; + import android.text.method.QwertyKeyListener; ++import android.util.Base64; + import android.util.TypedValue; + import android.view.ActionMode; + import android.view.Gravity; +@@ -68,6 +72,7 @@ import com.facebook.react.views.text.TextAttributes; + import com.facebook.react.views.text.TextInlineImageSpan; + import com.facebook.react.views.text.TextLayoutManager; + import com.facebook.react.views.view.ReactViewBackgroundManager; ++import java.io.IOException; + import java.util.ArrayList; + import java.util.Objects; + +@@ -110,6 +115,7 @@ public class ReactEditText extends AppCompatEditText { private @Nullable SelectionWatcher mSelectionWatcher; private @Nullable ContentSizeWatcher mContentSizeWatcher; private @Nullable ScrollWatcher mScrollWatcher; @@ -282,7 +522,7 @@ index 081f2b8..c50912a 100644 private InternalKeyListener mKeyListener; private boolean mDetectScrollMovement = false; private boolean mOnKeyPress = false; -@@ -153,6 +154,7 @@ public class ReactEditText extends AppCompatEditText { +@@ -153,6 +159,7 @@ public class ReactEditText extends AppCompatEditText { mKeyListener = new InternalKeyListener(); } mScrollWatcher = null; @@ -290,23 +530,48 @@ index 081f2b8..c50912a 100644 mTextAttributes = new TextAttributes(); applyTextAttributes(); -@@ -307,10 +309,13 @@ public class ReactEditText extends AppCompatEditText { +@@ -307,10 +314,38 @@ 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 (mPasteWatcher != null) { -+ mPasteWatcher.onPaste(); -+ } 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); ++ if (type != null) { ++ try { ++ String encodedData = Base64.encodeToString(cr.openInputStream(itemUri).readAllBytes(), Base64.DEFAULT); ++ data = "data:" + type + ";base64," + encodedData; ++ } catch (IOException e) { ++ e.printStackTrace(); ++ } ++ } ++ } ++ } ++ 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 +394,10 @@ public class ReactEditText extends AppCompatEditText { +@@ -389,6 +424,10 @@ public class ReactEditText extends AppCompatEditText { mScrollWatcher = scrollWatcher; } @@ -318,7 +583,7 @@ index 081f2b8..c50912a 100644 * 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 8496a7d..deb54d4 100644 +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 { - Clipboard.getImage().then((image) => { - if (!image) { + const pasteFile = useCallback( + (e: NativeSyntheticEvent) => { + const clipboardContent = e.nativeEvent.items[0]; + if (clipboardContent.type === 'text/plain') { return; } - onPasteFile(image); - }); - }, [onPasteFile]); + const filename = 'file.' + mimeDb[clipboardContent.type].extensions?.[0] ?? 'bin'; + const file: FileObject = {uri: clipboardContent.data, name: filename, type: clipboardContent.type}; + onPasteFile(file); + }, + [onPasteFile], + ); useEffect(() => { if (!shouldClear) { @@ -106,7 +111,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} - onPaste={pasteImage} + onPaste={pasteFile} onBlur={(e) => { if (!isFocused) { // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/libs/Clipboard/index.ios.ts b/src/libs/Clipboard/index.ios.ts deleted file mode 100644 index 4b2ba52b9672..000000000000 --- a/src/libs/Clipboard/index.ios.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Clipboard from '@react-native-clipboard/clipboard'; -import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; - -/** - * Sets a string on the Clipboard object via @react-native-clipboard/clipboard - */ -const setString: SetString = (text) => { - Clipboard.setString(text); -}; - -// We don't want to set HTML on native platforms so noop them. -const canSetHtml: CanSetHtml = () => false; -const setHtml: SetHtml = () => {}; - -const getImage: GetImage = () => - Clipboard.hasImage() - .then((hasImage) => { - if (!hasImage) { - return undefined; - } - return Clipboard.getImagePNG(); - }) - .then((imageb64) => { - if (!imageb64) { - return undefined; - } - return {uri: imageb64, name: 'image.png', type: 'image/png'}; - }); - -export default { - setString, - canSetHtml, - setHtml, - getImage, -}; diff --git a/src/libs/Clipboard/index.android.ts b/src/libs/Clipboard/index.native.ts similarity index 58% rename from src/libs/Clipboard/index.android.ts rename to src/libs/Clipboard/index.native.ts index 6e412441b194..2c345327a106 100644 --- a/src/libs/Clipboard/index.android.ts +++ b/src/libs/Clipboard/index.native.ts @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; +import type {CanSetHtml, SetHtml, SetString} from './types'; /** * Sets a string on the Clipboard object via @react-native-clipboard/clipboard @@ -12,17 +12,8 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; -const getImage: GetImage = () => - Clipboard.getImage().then((imageb64) => { - if (!imageb64) { - return undefined; - } - return {uri: imageb64, name: 'image.png', type: 'image/png'}; - }); - export default { setString, canSetHtml, setHtml, - getImage, }; diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index fb0f00e6eb84..130aad270b92 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; +import type {CanSetHtml, SetHtml, SetString} from './types'; type ComposerSelection = { start: number; @@ -136,11 +136,8 @@ const setString: SetString = (text) => { Clipboard.setString(text); }; -const getImage: GetImage = () => Promise.reject(new Error('getImage not supported on web')); - export default { setString, canSetHtml, setHtml, - getImage, }; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index c04cf952f1f7..1d899144a2ba 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,8 +1,5 @@ -import type {FileObject} from '@components/AttachmentModal'; - type SetString = (text: string) => void; type SetHtml = (html: string, text: string) => void; type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); -type GetImage = () => Promise; -export type {SetString, CanSetHtml, SetHtml, GetImage}; +export type {SetString, CanSetHtml, SetHtml}; From 147eff72ad4fae2e5c38c539c5a90d8b8072e6b3 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:09:38 +0100 Subject: [PATCH 5/8] fix lint --- src/components/Composer/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index d4cd44747572..a89935db9fa6 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -4,7 +4,7 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; -import {FileObject} from '@components/AttachmentModal'; +import type {FileObject} from '@components/AttachmentModal'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; @@ -73,7 +73,7 @@ function Composer( if (clipboardContent.type === 'text/plain') { return; } - const filename = 'file.' + mimeDb[clipboardContent.type].extensions?.[0] ?? 'bin'; + const filename = `file.${mimeDb[clipboardContent.type].extensions?.[0] ?? 'bin'}`; const file: FileObject = {uri: clipboardContent.data, name: filename, type: clipboardContent.type}; onPasteFile(file); }, From d5452b5cd8bf9aea5344ddd1d560499960529633 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:52:29 +0100 Subject: [PATCH 6/8] Fix uploading GIF URI on Android --- ....4+025+Android-fix-uploading-GIF-URI.patch | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch diff --git a/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch b/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch new file mode 100644 index 000000000000..7b34abd420ce --- /dev/null +++ b/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +index 7c0d07d..e1a2d5d 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java +@@ -8,8 +8,6 @@ + package com.facebook.react.modules.network; + + import android.content.Context; +-import android.graphics.Bitmap; +-import android.graphics.BitmapFactory; + import android.net.Uri; + import android.os.Build; + import android.util.Base64; +@@ -65,11 +63,7 @@ import okio.Source; + + if (fileContentUriStr.startsWith("data:")) { + byte[] decodedDataUrString = Base64.decode(fileContentUriStr.split(",")[1], Base64.DEFAULT); +- Bitmap bitMap = +- BitmapFactory.decodeByteArray(decodedDataUrString, 0, decodedDataUrString.length); +- ByteArrayOutputStream bytes = new ByteArrayOutputStream(); +- bitMap.compress(Bitmap.CompressFormat.PNG, 0, bytes); +- return new ByteArrayInputStream(bytes.toByteArray()); ++ return new ByteArrayInputStream(decodedDataUrString); + } + + return context.getContentResolver().openInputStream(fileContentUri); From ac1b2dce034a0bf8c4ed9ecab6949feadbde7207 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:55:20 +0100 Subject: [PATCH 7/8] onPaste: send URI for files instead of base64 strings --- package-lock.json | 6 - package.json | 1 - ...+0.73.4+024+Add-onPaste-to-TextInput.patch | 163 ++++++------------ ....4+025+Android-fix-uploading-GIF-URI.patch | 26 --- src/components/Composer/index.native.tsx | 5 +- 5 files changed, 55 insertions(+), 146 deletions(-) delete mode 100644 patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch diff --git a/package-lock.json b/package-lock.json index 0b3b540303ac..2d1e6575337c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", - "@types/mime-db": "^1.43.5", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -17687,11 +17686,6 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, - "node_modules/@types/mime-db": { - "version": "1.43.5", - "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.5.tgz", - "integrity": "sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==" - }, "node_modules/@types/minimatch": { "version": "3.0.5", "dev": true, diff --git a/package.json b/package.json index d33ab4a67461..bd17e45f63fc 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "10.1.20", "@shopify/flash-list": "1.6.3", - "@types/mime-db": "^1.43.5", "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", 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 index 00bb93555575..272bd313faaa 100644 --- 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 @@ -170,70 +170,52 @@ index 481938f..a74fda7 100644 * 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..ae3e967 100644 +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,8 @@ +@@ -13,6 +13,9 @@ #import #import ++#import +#import + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; -@@ -166,7 +168,49 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO +@@ -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) { -+ NSArray *clipboardTypes = [clipboard pasteboardTypes]; -+ if ([clipboardTypes containsObject:@"com.compuserve.gif"]) { -+ NSString *type = @"image/gif"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.compuserve.gif"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.png"]) { -+ NSString *type = @"image/png"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.png"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.jpeg"]) { -+ NSString *type = @"image/jpeg"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.jpeg"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"org.webmproject.webp"]) { -+ NSString *type = @"image/webp"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"org.webmproject.webp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.tiff"]) { -+ NSString *type = @"image/tiff"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.tiff"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"com.microsoft.bmp"]) { -+ NSString *type = @"image/bmp"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.microsoft.bmp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.svg-image"]) { -+ NSString *type = @"image/svg+xml"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.svg-image"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; ++ 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 { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } + [super paste:sender]; + } } // Turn off scroll animation to fix flaky scrolling. -@@ -258,6 +302,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender +@@ -258,6 +284,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } @@ -360,19 +342,20 @@ index a19b555..8146c0d 100644 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..a611f69 100644 +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,8 @@ +@@ -12,6 +12,9 @@ #import #import ++#import +#import + @implementation RCTUITextField { RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; -@@ -139,6 +141,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender +@@ -139,6 +142,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } @@ -383,52 +366,33 @@ index 4d0afd9..a611f69 100644 return [super canPerformAction:action withSender:sender]; } -@@ -204,7 +210,49 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO +@@ -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) { -+ NSArray *clipboardTypes = [clipboard pasteboardTypes]; -+ if ([clipboardTypes containsObject:@"com.compuserve.gif"]) { -+ NSString *type = @"image/gif"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.compuserve.gif"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.png"]) { -+ NSString *type = @"image/png"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.png"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.jpeg"]) { -+ NSString *type = @"image/jpeg"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.jpeg"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"org.webmproject.webp"]) { -+ NSString *type = @"image/webp"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"org.webmproject.webp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.tiff"]) { -+ NSString *type = @"image/tiff"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.tiff"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"com.microsoft.bmp"]) { -+ NSString *type = @"image/bmp"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"com.microsoft.bmp"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; -+ } else if ([clipboardTypes containsObject:@"public.svg-image"]) { -+ NSString *type = @"image/svg+xml"; -+ NSString *encodedData = [[clipboard dataForPasteboardType:@"public.svg-image"] base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; -+ NSString *data = [NSString stringWithFormat:@"data:%@;base64,%@", type, encodedData]; -+ [_textInputDelegateAdapter didPaste:type withData:data]; ++ 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 { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ if (clipboard.hasStrings) { ++ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ } + [super paste:sender]; + } } @@ -476,7 +440,7 @@ index 0000000..bfb5819 + 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..cf8f04d 100644 +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; @@ -498,23 +462,7 @@ index 081f2b8..cf8f04d 100644 import android.os.Build; import android.os.Bundle; import android.text.Editable; -@@ -28,6 +31,7 @@ import android.text.TextUtils; - import android.text.TextWatcher; - import android.text.method.KeyListener; - import android.text.method.QwertyKeyListener; -+import android.util.Base64; - import android.util.TypedValue; - import android.view.ActionMode; - import android.view.Gravity; -@@ -68,6 +72,7 @@ import com.facebook.react.views.text.TextAttributes; - import com.facebook.react.views.text.TextInlineImageSpan; - import com.facebook.react.views.text.TextLayoutManager; - import com.facebook.react.views.view.ReactViewBackgroundManager; -+import java.io.IOException; - import java.util.ArrayList; - import java.util.Objects; - -@@ -110,6 +115,7 @@ public class ReactEditText extends AppCompatEditText { +@@ -110,6 +113,7 @@ public class ReactEditText extends AppCompatEditText { private @Nullable SelectionWatcher mSelectionWatcher; private @Nullable ContentSizeWatcher mContentSizeWatcher; private @Nullable ScrollWatcher mScrollWatcher; @@ -522,7 +470,7 @@ index 081f2b8..cf8f04d 100644 private InternalKeyListener mKeyListener; private boolean mDetectScrollMovement = false; private boolean mOnKeyPress = false; -@@ -153,6 +159,7 @@ public class ReactEditText extends AppCompatEditText { +@@ -153,6 +157,7 @@ public class ReactEditText extends AppCompatEditText { mKeyListener = new InternalKeyListener(); } mScrollWatcher = null; @@ -530,7 +478,7 @@ index 081f2b8..cf8f04d 100644 mTextAttributes = new TextAttributes(); applyTextAttributes(); -@@ -307,10 +314,38 @@ public class ReactEditText extends AppCompatEditText { +@@ -307,10 +312,31 @@ public class ReactEditText extends AppCompatEditText { */ @Override public boolean onTextContextMenuItem(int id) { @@ -553,14 +501,7 @@ index 081f2b8..cf8f04d 100644 + if (itemUri != null) { + ContentResolver cr = getReactContext(this).getContentResolver(); + type = cr.getType(itemUri); -+ if (type != null) { -+ try { -+ String encodedData = Base64.encodeToString(cr.openInputStream(itemUri).readAllBytes(), Base64.DEFAULT); -+ data = "data:" + type + ";base64," + encodedData; -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ } ++ data = itemUri.toString(); + } + } + if (type != null && data != null) { @@ -571,7 +512,7 @@ index 081f2b8..cf8f04d 100644 ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData previousClipData = clipboard.getPrimaryClip(); -@@ -389,6 +424,10 @@ public class ReactEditText extends AppCompatEditText { +@@ -389,6 +415,10 @@ public class ReactEditText extends AppCompatEditText { mScrollWatcher = scrollWatcher; } diff --git a/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch b/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch deleted file mode 100644 index 7b34abd420ce..000000000000 --- a/patches/react-native+0.73.4+025+Android-fix-uploading-GIF-URI.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java -index 7c0d07d..e1a2d5d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/RequestBodyUtil.java -@@ -8,8 +8,6 @@ - package com.facebook.react.modules.network; - - import android.content.Context; --import android.graphics.Bitmap; --import android.graphics.BitmapFactory; - import android.net.Uri; - import android.os.Build; - import android.util.Base64; -@@ -65,11 +63,7 @@ import okio.Source; - - if (fileContentUriStr.startsWith("data:")) { - byte[] decodedDataUrString = Base64.decode(fileContentUriStr.split(",")[1], Base64.DEFAULT); -- Bitmap bitMap = -- BitmapFactory.decodeByteArray(decodedDataUrString, 0, decodedDataUrString.length); -- ByteArrayOutputStream bytes = new ByteArrayOutputStream(); -- bitMap.compress(Bitmap.CompressFormat.PNG, 0, bytes); -- return new ByteArrayInputStream(bytes.toByteArray()); -+ return new ByteArrayInputStream(decodedDataUrString); - } - - return context.getContentResolver().openInputStream(fileContentUri); diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 109ef1235684..966ba0c4af17 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -73,8 +73,9 @@ function Composer( if (clipboardContent.type === 'text/plain') { return; } - const filename = `file.${mimeDb[clipboardContent.type].extensions?.[0] ?? 'bin'}`; - const file: FileObject = {uri: clipboardContent.data, name: filename, type: clipboardContent.type}; + const fileURI = clipboardContent.data; + const fileName = fileURI.split('/').pop(); + const file: FileObject = {uri: fileURI, name: fileName, type: clipboardContent.type}; onPasteFile(file); }, [onPasteFile], From 34e33fac192c94badd7314f1617095c44c3a8140 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:58:06 +0100 Subject: [PATCH 8/8] lint --- src/components/Composer/index.native.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 966ba0c4af17..4b791e8d1034 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,5 +1,4 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; -import mimeDb from 'mime-db'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputPasteEventData} from 'react-native';