From 7f25685d493338224c043825cdfaa33e4fa6fdfe Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 14 Aug 2025 20:02:53 +0200 Subject: [PATCH 01/16] feat: allow multiple files --- .../Composer/implementation/index.native.tsx | 37 ++++++++++++------- .../Composer/implementation/index.tsx | 33 ++++++++++------- src/components/Composer/types.ts | 2 +- .../ComposerWithSuggestions.tsx | 4 +- .../ReportActionCompose.tsx | 21 +++++------ 5 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index d7b6747d8921..745e2f055d8c 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -93,20 +93,29 @@ function Composer( const pasteFile = useCallback( (e: NativeSyntheticEvent) => { - const clipboardContent = e.nativeEvent.items.at(0); - if (clipboardContent?.type === 'text/plain') { - return; - } - const mimeType = clipboardContent?.type ?? ''; - const fileURI = clipboardContent?.data; - const baseFileName = fileURI?.split('/').pop() ?? 'file'; - const {fileName: stem, fileExtension: originalFileExtension} = splitExtensionFromFileName(baseFileName); - const fileExtension = originalFileExtension || (mimeDb[mimeType].extensions?.[0] ?? 'bin'); - const fileName = `${stem}.${fileExtension}`; - let file: FileObject = {uri: fileURI, name: fileName, type: mimeType, size: 0}; - getFileSize(file.uri ?? '') - .then((size) => (file = {...file, size})) - .finally(() => onPasteFile(file)); + const filePromises: Array> = e.nativeEvent.items.map((item) => { + const clipboardContent = item; + if (clipboardContent?.type === 'text/plain') { + return Promise.resolve(undefined); + } + + const mimeType = clipboardContent?.type ?? ''; + const fileURI = clipboardContent?.data; + const baseFileName = fileURI?.split('/').pop() ?? 'file'; + const {fileName: stem, fileExtension: originalFileExtension} = splitExtensionFromFileName(baseFileName); + const fileExtension = originalFileExtension || (mimeDb[mimeType].extensions?.[0] ?? 'bin'); + const fileName = `${stem}.${fileExtension}`; + let file: FileObject = {uri: fileURI, name: fileName, type: mimeType, size: 0}; + + return getFileSize(file.uri ?? '') + .then((size) => (file = {...file, size} as FileObject)) + .finally(() => file); + }); + + Promise.all(filePromises).then((files) => { + const validFiles = files.filter((file) => file !== undefined) as FileObject[]; + onPasteFile(validFiles); + }); }, [onPasteFile], ); diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 1ddcb16d2251..18fa35031303 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -21,6 +21,7 @@ import {containsOnlyEmojis} from '@libs/EmojiUtils'; import {base64ToFile} from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import Parser from '@libs/Parser'; +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -163,7 +164,8 @@ function Composer( // If paste contains files, then trigger file management if (event.clipboardData?.files.length && event.clipboardData.files.length > 0) { // Prevent the default so we do not post the file name into the text box - onPasteFile(event.clipboardData.files[0]); + const files = Array.from(event.clipboardData.files) as FileObject[]; + onPasteFile(files); return true; } @@ -173,12 +175,9 @@ function Composer( const pastedHTML = clipboardDataHtml; const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML)?.images; - if (embeddedImages.length > 0 && embeddedImages[0].src) { - const src = embeddedImages[0].src; - const file = base64ToFile(src, 'image.png'); - onPasteFile(file); - return true; - } + const files = Array.from(embeddedImages).map((image) => base64ToFile(image.src, 'image.png')) as FileObject[]; + onPasteFile(files); + return true; } // If paste contains image from Google Workspaces ex: Sheets, Docs, Slide, etc @@ -187,19 +186,25 @@ function Composer( const pastedHTML = clipboardDataHtml; const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; - if (embeddedImages.length > 0 && embeddedImages[0]?.src) { - const src = embeddedImages[0].src; - if (src.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { - fetch(src) + const filePromises = Array.from(embeddedImages).map((image) => { + if (image.src.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { + return fetch(image.src) .then((response) => response.blob()) .then((blob) => { const file = new File([blob], 'image.jpg', {type: 'image/jpeg'}); - onPasteFile(file); + return file; }); - return true; } - } + return Promise.resolve(undefined); + }); + + Promise.all(filePromises).then((files) => { + const validFiles = files.filter((file) => file !== undefined) as FileObject[]; + onPasteFile(validFiles); + }); + return true; } + return false; }, [onPasteFile, checkComposerVisibility], diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 7ecf77add492..66a86923d575 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -35,7 +35,7 @@ type ComposerProps = Omit & { onChangeText?: (numberOfLines: string) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file: FileObject) => void; + onPasteFile?: (file: FileObject | FileObject[]) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index d9c14cdeae53..81ba266d4036 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -101,7 +101,7 @@ type ComposerWithSuggestionsProps = Partial & { inputPlaceholder: string; /** Function to display a file in a modal */ - displayFilesInModal: (file: FileObject[]) => void; + displayFilesInModal: (file: File | FileObject[]) => void; /** Whether the input is disabled, defaults to false */ disabled?: boolean; @@ -810,7 +810,7 @@ function ComposerWithSuggestions( onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={(file) => { textInputRef.current?.blur(); - displayFilesInModal([file]); + displayFilesInModal(file); }} onClear={onClear} isDisabled={disabled} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index ae2895467544..dbcbb689bc0d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -586,21 +586,18 @@ function ReportActionCompose({ if (isAttachmentPreviewActive) { return; } - if (event.dataTransfer?.files.length && event.dataTransfer?.files.length > 1) { - const files = Array.from(event.dataTransfer?.files).map((file) => { - // eslint-disable-next-line no-param-reassign - file.uri = URL.createObjectURL(file); - return file; - }); - displayFilesInModal(files, Array.from(event.dataTransfer?.items ?? [])); + + const files = Array.from(event.dataTransfer?.files ?? []).map((file) => { + // eslint-disable-next-line no-param-reassign + file.uri = URL.createObjectURL(file); + return file; + }); + + if (files.length === 0) { return; } - const data = event.dataTransfer?.files[0]; - if (data) { - data.uri = URL.createObjectURL(data); - displayFilesInModal([data], Array.from(event.dataTransfer?.items ?? [])); - } + displayFilesInModal(files); }; return ( From d5799b6471868ca7ee5e3619305431f34c1183a5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 18 Feb 2026 11:23:24 +0000 Subject: [PATCH 02/16] fix: lint errors and add error if validation fails --- .../Composer/implementation/index.native.tsx | 13 +++++++++---- src/components/Composer/implementation/index.tsx | 15 ++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 5e4071cb5780..25f94f417ce5 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -16,6 +16,7 @@ import Parser from '@libs/Parser'; import getFileSize from '@pages/Share/getFileSize'; import CONST from '@src/CONST'; import type {FileObject} from '@src/types/utils/Attachment'; +import Log from '@libs/Log'; const excludeNoStyles: Array = []; const excludeReportMentionStyle: Array = ['mentionReport']; @@ -108,10 +109,14 @@ function Composer({ .finally(() => file); }); - Promise.all(filePromises).then((files) => { - const validFiles = files.filter((file) => file !== undefined) as FileObject[]; - onPasteFile(validFiles); - }); + Promise.all(filePromises) + .then((files) => { + const validFiles = files.filter((file) => file !== undefined); + onPasteFile(validFiles); + }) + .catch((error) => { + Log.warn('Pasted files could not be validated', {error}); + }); }, [onPasteFile], ); diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 8a4d635c6186..07dc8c505ff9 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -22,6 +22,7 @@ import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposit import Parser from '@libs/Parser'; import type {FileObject} from '@src/types/utils/Attachment'; import CONST from '@src/CONST'; +import Log from '@libs/Log'; const excludeNoStyles: Array = []; const excludeReportMentionStyle: Array = ['mentionReport']; @@ -188,16 +189,20 @@ function Composer({ .then((response) => response.blob()) .then((blob) => { const file = new File([blob], 'image.jpg', {type: 'image/jpeg'}); - return file; + return file as FileObject; }); } return Promise.resolve(undefined); }); - Promise.all(filePromises).then((files) => { - const validFiles = files.filter((file) => file !== undefined) as FileObject[]; - onPasteFile(validFiles); - }); + Promise.all(filePromises) + .then((files) => { + const validFiles = files.filter((file) => file !== undefined); + onPasteFile(validFiles); + }) + .catch((error) => { + Log.warn('Pasted files could not be validated', {error}); + }); return true; } From 537a54bb81ae0ba9de40cbfb514ba60be8c6ca9e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 18 Feb 2026 11:25:50 +0000 Subject: [PATCH 03/16] refactor: create a new file object with URI instead of modifying the original --- .../report/ReportActionCompose/ReportActionCompose.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx index 59bd179d01c3..3c7db57c0913 100644 --- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx @@ -544,9 +544,10 @@ function ReportActionCompose({ } const files = Array.from(event.dataTransfer?.files ?? []).map((file) => { - // eslint-disable-next-line no-param-reassign - file.uri = URL.createObjectURL(file); - return file; + const fileWithUri = file; + + fileWithUri.uri = URL.createObjectURL(fileWithUri); + return fileWithUri; }); if (files.length === 0) { From 20fa0d9ad7c3842689596a9f32f323a401e2d552 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 18 Feb 2026 13:14:24 +0000 Subject: [PATCH 04/16] fix: always paste files and add optimizations --- .../Composer/implementation/index.native.tsx | 9 ++++-- .../Composer/implementation/index.tsx | 30 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 25f94f417ce5..605c9d493cd3 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -109,13 +109,16 @@ function Composer({ .finally(() => file); }); + let files: FileObject[] = []; Promise.all(filePromises) - .then((files) => { - const validFiles = files.filter((file) => file !== undefined); - onPasteFile(validFiles); + .then((f) => { + files = f.filter((file) => file !== undefined); }) .catch((error) => { Log.warn('Pasted files could not be validated', {error}); + }) + .finally(() => { + onPasteFile(files); }); }, [onPasteFile], diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 07dc8c505ff9..4f406d38c9f2 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -154,34 +154,32 @@ function Composer({ event.preventDefault(); - const TEXT_HTML = 'text/html'; - - const clipboardDataHtml = event.clipboardData?.getData(TEXT_HTML) ?? ''; + const files: Array = []; // If paste contains files, then trigger file management if (event.clipboardData?.files.length && event.clipboardData.files.length > 0) { // Prevent the default so we do not post the file name into the text box - const files = Array.from(event.clipboardData.files) as FileObject[]; - onPasteFile(files); - return true; + files.push(...(Array.from(event.clipboardData.files) as FileObject[])); } // If paste contains base64 image + + const clipboardDataHtml = event.clipboardData?.getData(CONST.SHARE_FILE_MIMETYPE.HTML) ?? ''; if (clipboardDataHtml?.includes(CONST.IMAGE_BASE64_MATCH)) { const domparser = new DOMParser(); const pastedHTML = clipboardDataHtml; - const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML)?.images; + const embeddedImages = domparser.parseFromString(pastedHTML, CONST.SHARE_FILE_MIMETYPE.HTML)?.images; - const files = Array.from(embeddedImages).map((image) => base64ToFile(image.src, 'image.png')) as FileObject[]; - onPasteFile(files); - return true; + if (embeddedImages.length > 0) { + files.push(...(Array.from(embeddedImages).map((image) => base64ToFile(image.src, 'image.png')) as FileObject[])); + } } // If paste contains image from Google Workspaces ex: Sheets, Docs, Slide, etc if (clipboardDataHtml?.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { const domparser = new DOMParser(); const pastedHTML = clipboardDataHtml; - const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; + const embeddedImages = domparser.parseFromString(pastedHTML, CONST.SHARE_FILE_MIMETYPE.HTML).images; const filePromises = Array.from(embeddedImages).map((image) => { if (image.src.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { @@ -196,13 +194,17 @@ function Composer({ }); Promise.all(filePromises) - .then((files) => { - const validFiles = files.filter((file) => file !== undefined); - onPasteFile(validFiles); + .then((f) => { + files.push(...f); }) .catch((error) => { Log.warn('Pasted files could not be validated', {error}); }); + } + + const validFiles = files.filter((file) => file !== undefined); + if (validFiles.length > 0) { + onPasteFile(validFiles); return true; } From 698c8aae799ccb59e321709580fca1a09fca4909 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 18 Feb 2026 21:02:11 +0000 Subject: [PATCH 05/16] chore: update RN `onPaste` patch to allow multiple files --- ...+0.81.4+011+Add-onPaste-to-TextInput.patch | 510 +++++++++++------- 1 file changed, 330 insertions(+), 180 deletions(-) diff --git a/patches/react-native/react-native+0.81.4+011+Add-onPaste-to-TextInput.patch b/patches/react-native/react-native+0.81.4+011+Add-onPaste-to-TextInput.patch index 6138c636d0da..4e53ce93af75 100644 --- a/patches/react-native/react-native+0.81.4+011+Add-onPaste-to-TextInput.patch +++ b/patches/react-native/react-native+0.81.4+011+Add-onPaste-to-TextInput.patch @@ -136,7 +136,7 @@ index 5fa8811..ad2bf61 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 6e9c384..2c509eb 100644 +index 6e9c384..c9f38ba 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,10 @@ @@ -150,44 +150,98 @@ index 6e9c384..2c509eb 100644 @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; -@@ -209,7 +213,31 @@ static UIColor *defaultPlaceholderColor(void) - - (void)paste:(id)sender - { +@@ -206,10 +210,83 @@ static UIColor *defaultPlaceholderColor(void) + [super scrollRangeToVisible:range]; + } + +-- (void)paste:(id)sender +-{ ++- (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; -+ if (clipboard.hasImages) { -+ for (NSItemProvider *itemProvider in clipboard.itemProviders) { -+ if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { -+ for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { -+ if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { -+ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); -+ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *filePath = RCTTempFilePath(fileExtension, nil); -+ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; -+ NSData *fileData = [clipboard dataForPasteboardType:identifier]; -+ [fileData writeToFile:filePath atomically:YES]; -+ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; -+ break; -+ } -+ } ++ ++ if (clipboard.hasStrings && clipboard.string != nil) { ++ NSDictionary * stringItem = @{@"type" : @"text/plain", @"data" : clipboard.string}; ++ [_textInputDelegateAdapter didPaste:@[stringItem]]; ++ [super paste:sender]; ++ return; ++ } ++ ++ NSMutableArray *> *items = ++ [NSMutableArray new]; ++ ++ for (NSInteger i = 0; i < clipboard.numberOfItems; i++) { ++ NSIndexSet *itemSet = [NSIndexSet indexSetWithIndex:i]; ++ NSArray *types = ++ [clipboard pasteboardTypesForItemSet:itemSet].firstObject; ++ if (types.count == 0) { ++ continue; ++ } ++ ++ // Prefer one canonical representation per item. ++ NSString *type = nil; ++ for (NSString *preferred in @[ ++ (NSString *)kUTTypeFileURL, (NSString *)kUTTypeURL, ++ (NSString *)kUTTypePlainText ++ ]) { ++ if ([types containsObject:preferred]) { ++ type = preferred; + break; + } + } -+ } else { -+ if (clipboard.hasStrings && clipboard.string != nil) { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ if (!type) { ++ type = types.firstObject; ++ } ++ ++ NSData *data = ++ [clipboard dataForPasteboardType:type inItemSet:itemSet].firstObject; ++ if (!data) { ++ continue; ++ } ++ ++ NSString *mimeType = ++ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass( ++ (__bridge CFStringRef)type, kUTTagClassMIMEType) ++ ?: @"application/octet-stream"; ++ NSString *payload = nil; ++ ++ if (UTTypeConformsTo((__bridge CFStringRef)type, kUTTypePlainText) || ++ UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeURL) || ++ UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeFileURL)) { ++ payload = [[NSString alloc] initWithData:data ++ encoding:NSUTF8StringEncoding]; ++ } else { ++ NSString *ext = ++ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass( ++ (__bridge CFStringRef)type, kUTTagClassFilenameExtension) ++ ?: @"bin"; ++ NSString *base = [NSTemporaryDirectory() ++ stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; ++ NSString *path = [base stringByAppendingPathExtension:ext]; ++ [data writeToFile:path atomically:YES]; ++ payload = [NSURL fileURLWithPath:path].absoluteString; + } ++ ++ if (payload.length > 0) { ++ [items addObject:@{@"type" : mimeType, @"data" : payload}]; ++ } ++ } ++ ++ if (items.count == 0) { + [super paste:sender]; ++ return; + } ++ ++ [self->_textInputDelegateAdapter didPaste:[items copy]]; } // Turn off scroll animation to fix flaky scrolling. -@@ -301,6 +329,10 @@ static UIColor *defaultPlaceholderColor(void) +@@ -301,6 +378,10 @@ static UIColor *defaultPlaceholderColor(void) return NO; } -+ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].numberOfItems > 0) { + return YES; + } + @@ -195,27 +249,27 @@ index 6e9c384..2c509eb 100644 } diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h -index 7187177..da00893 100644 +index 7187177..8f34534 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -37,6 +37,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)textInputDidChangeSelection; -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data; ++- (void)textInputDidPaste:(NSArray *> *)items; + @optional - (void)scrollViewDidScroll:(UIScrollView *)scrollView; diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h -index f1c32e6..0ce9dfe 100644 +index f1c32e6..1db66dd 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; - (void)selectedTextRangeWasSet; -+- (void)didPaste:(NSString *)type withData:(NSString *)data; ++- (void)didPaste:(NSArray *> *)items; @end @@ -223,21 +277,21 @@ index f1c32e6..0ce9dfe 100644 - (instancetype)initWithTextView:(UITextView *)backedTextInputView; - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; -+- (void)didPaste:(NSString *)type withData:(NSString *)data; ++- (void)didPaste:(NSArray *> *)items; @end diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm -index 82d9a79..8cc48ec 100644 +index 82d9a79..2d5d4cd 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -148,6 +148,11 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo [self textFieldProbablyDidChangeSelection]; } -+- (void)didPaste:(NSString *)type withData:(NSString *)data ++- (void)didPaste:(NSArray *> *)items +{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++ [_backedTextInputView.textInputDelegate textInputDidPaste:items]; +} + #pragma mark - Generalization @@ -247,9 +301,9 @@ index 82d9a79..8cc48ec 100644 _previousSelectedTextRange = textRange; } -+- (void)didPaste:(NSString *)type withData:(NSString *)data ++- (void)didPaste:(NSArray *> *)items +{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; ++ [_backedTextInputView.textInputDelegate textInputDidPaste:items]; +} + #pragma mark - Generalization @@ -268,25 +322,19 @@ index 4804624..90b7081 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 6a2d4f8..b6e6060 100644 +index 6a2d4f8..10b1716 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm -@@ -599,6 +599,26 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) +@@ -599,6 +599,20 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) }); } -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++- (void)textInputDidPaste:(NSArray *> *)items +{ + if (!_onPaste) { + return; + } + -+ NSMutableArray *items = [NSMutableArray new]; -+ [items addObject:@{ -+ @"type" : type, -+ @"data" : data, -+ }]; -+ + NSDictionary *payload = @{ + @"target" : self.reactTag, + @"items" : items, @@ -299,7 +347,7 @@ index 6a2d4f8..b6e6060 100644 { [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 407d46e..0a5b412 100644 +index 407d46e..8be994c 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -71,6 +71,7 @@ RCT_EXPORT_VIEW_PROPERTY(onClear, RCTDirectEventBlock) @@ -311,7 +359,7 @@ index 407d46e..0a5b412 100644 RCT_EXPORT_SHADOW_PROPERTY(text, NSString) RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString) 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 377f41e..b8f48e6 100644 +index 377f41e..960c9bb 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,10 @@ @@ -329,58 +377,120 @@ index 377f41e..b8f48e6 100644 return NO; } -+ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].numberOfItems > 0) { + return YES; + } + return [super canPerformAction:action withSender:sender]; } -@@ -263,7 +271,31 @@ - - (void)paste:(id)sender - { +@@ -260,10 +268,83 @@ + // Singleline TextInput does not require scrolling after calling setSelectedTextRange (PR 38679). + } + +-- (void)paste:(id)sender +-{ ++- (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; -+ if (clipboard.hasImages) { -+ for (NSItemProvider *itemProvider in clipboard.itemProviders) { -+ if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { -+ for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { -+ if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { -+ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); -+ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *filePath = RCTTempFilePath(fileExtension, nil); -+ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; -+ NSData *fileData = [clipboard dataForPasteboardType:identifier]; -+ [fileData writeToFile:filePath atomically:YES]; -+ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; -+ break; -+ } -+ } ++ ++ if (clipboard.hasStrings && clipboard.string != nil) { ++ NSDictionary * stringItem = @{@"type" : @"text/plain", @"data" : clipboard.string}; ++ [_textInputDelegateAdapter didPaste:@[stringItem]]; ++ [super paste:sender]; ++ return; ++ } ++ ++ NSMutableArray *> *items = ++ [NSMutableArray new]; ++ ++ for (NSInteger i = 0; i < clipboard.numberOfItems; i++) { ++ NSIndexSet *itemSet = [NSIndexSet indexSetWithIndex:i]; ++ NSArray *types = ++ [clipboard pasteboardTypesForItemSet:itemSet].firstObject; ++ if (types.count == 0) { ++ continue; ++ } ++ ++ // Prefer one canonical representation per item. ++ NSString *type = nil; ++ for (NSString *preferred in @[ ++ (NSString *)kUTTypeFileURL, (NSString *)kUTTypeURL, ++ (NSString *)kUTTypePlainText ++ ]) { ++ if ([types containsObject:preferred]) { ++ type = preferred; + break; + } + } -+ } else { -+ if (clipboard.hasStrings && clipboard.string != nil) { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; ++ if (!type) { ++ type = types.firstObject; + } ++ ++ NSData *data = ++ [clipboard dataForPasteboardType:type inItemSet:itemSet].firstObject; ++ if (!data) { ++ continue; ++ } ++ ++ NSString *mimeType = ++ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass( ++ (__bridge CFStringRef)type, kUTTagClassMIMEType) ++ ?: @"application/octet-stream"; ++ NSString *payload = nil; ++ ++ if (UTTypeConformsTo((__bridge CFStringRef)type, kUTTypePlainText) || ++ UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeURL) || ++ UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeFileURL)) { ++ payload = [[NSString alloc] initWithData:data ++ encoding:NSUTF8StringEncoding]; ++ } else { ++ NSString *ext = ++ (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass( ++ (__bridge CFStringRef)type, kUTTagClassFilenameExtension) ++ ?: @"bin"; ++ NSString *base = [NSTemporaryDirectory() ++ stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; ++ NSString *path = [base stringByAppendingPathExtension:ext]; ++ [data writeToFile:path atomically:YES]; ++ payload = [NSURL fileURLWithPath:path].absoluteString; ++ } ++ ++ if (payload.length > 0) { ++ [items addObject:@{@"type" : mimeType, @"data" : payload}]; ++ } ++ } ++ ++ if (items.count == 0) { + [super paste:sender]; ++ return; + } ++ ++ [self->_textInputDelegateAdapter didPaste:[items copy]]; } #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 44c8737..0efbbe7 100644 +index d160af1..e3de3a7 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 -@@ -520,6 +520,13 @@ static NSSet *returnKeyTypesSet; +@@ -521,6 +521,21 @@ static NSSet *returnKeyTypesSet; } } -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data ++- (void)textInputDidPaste:(NSArray *> *)items +{ + if (_eventEmitter) { -+ static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); ++ std::vector> itemsVector; ++ for (NSDictionary *item in items) { ++ NSString *type = item[@"type"]; ++ NSString *data = item[@"data"]; ++ if (type && data) { ++ itemsVector.push_back({std::string([type UTF8String]), std::string([data UTF8String])}); ++ } ++ } ++ static_cast(*_eventEmitter).onPaste(itemsVector); + } +} + @@ -389,7 +499,7 @@ index 44c8737..0efbbe7 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..bfb5819 +index 0000000..8195a84 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java @@ -0,0 +1,17 @@ @@ -408,10 +518,10 @@ index 0000000..bfb5819 + * from the EditText to JS + */ +public interface PasteWatcher { -+ public void onPaste(String type, String data); ++ public void onPaste(java.util.List> items); +} diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt -index 42f6e03..ffba031 100644 +index 42f6e03..73238cf 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -8,6 +8,10 @@ @@ -434,7 +544,7 @@ index 42f6e03..ffba031 100644 import android.os.Build import android.os.Bundle import android.text.Editable -@@ -128,6 +133,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat +@@ -128,6 +134,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var selectionWatcher: SelectionWatcher? = null private var contentSizeWatcher: ContentSizeWatcher? = null private var scrollWatcher: ScrollWatcher? @@ -450,7 +560,7 @@ index 42f6e03..ffba031 100644 textAttributes = TextAttributes() applyTextAttributes() -@@ -356,9 +364,50 @@ public open class ReactEditText public constructor(context: Context) : AppCompat +@@ -356,9 +364,64 @@ public open class ReactEditText public constructor(context: Context) : AppCompat * Called when a context menu option for the text view is selected. * React Native replaces copy (as rich text) with copy as plain text. */ @@ -466,45 +576,59 @@ index 42f6e03..ffba031 100644 + + if (modifiedId == android.R.id.pasteAsPlainText) { + val clipboardManager = getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager -+ val clipData = clipboardManager.primaryClip -+ if (clipData != null) { -+ val item = clipData.getItemAt(0) -+ val itemUri = item.uri -+ var type: String? = null -+ var data: String? = null -+ -+ if (itemUri != null) { -+ val cr = getReactContext(this).contentResolver -+ type = cr.getType(itemUri) -+ if (type != null && type != ClipDescription.MIMETYPE_TEXT_PLAIN) { -+ data = itemUri.toString() -+ if (pasteWatcher != null) { -+ pasteWatcher?.onPaste(type, data) -+ } -+ // Prevents default behavior to avoid inserting raw binary data into the text field -+ return true -+ } ++ ++ val clip = clipboardManager.primaryClip ?: return false ++ val items = mutableListOf>() ++ ++ for (i in 0 until clip.itemCount) { ++ val item = clip.getItemAt(i) ++ ++ // Plain text ++ item.text?.toString()?.takeIf { it.isNotEmpty() }?.let { ++ items += Pair(ClipDescription.MIMETYPE_TEXT_PLAIN, it) + } + -+ if (clipData.description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { -+ type = ClipDescription.MIMETYPE_TEXT_PLAIN -+ val text: CharSequence? = item.text -+ if (text != null) { -+ data = text.toString() -+ if (pasteWatcher != null) { -+ pasteWatcher?.onPaste(type, data) -+ } -+ // Don't return - let the system proceed with default text pasting behavior ++ // HTML text ++ item.htmlText?.takeIf { it.isNotEmpty() }?.let { ++ items += Pair(ClipDescription.MIMETYPE_TEXT_HTML, it) ++ } ++ ++ // URI (files/content/image/etc) ++ item.uri?.let { uri -> ++ val mime = runCatching { context.contentResolver.getType(uri) }.getOrNull() ++ ?: if (uri.scheme == ContentResolver.SCHEME_FILE) "application/octet-stream" ++ else ClipDescription.MIMETYPE_TEXT_URILIST ++ items += Pair(mime, uri.toString()) ++ } ++ ++ // Intent payloads (rare, but valid clipboard data) ++ item.intent?.let { intent -> ++ items += Pair("application/vnd.android.intent", intent.toUri(Intent.URI_INTENT_SCHEME)) ++ } ++ ++ // Fallback if nothing above was populated ++ if (item.text == null && item.htmlText == null && item.uri == null && item.intent == null) { ++ item.coerceToText(context)?.toString()?.takeIf { it.isNotEmpty() }?.let { ++ items += Pair(ClipDescription.MIMETYPE_TEXT_PLAIN, it) ++ } + } + } ++ ++ if (items.isNotEmpty()) { ++ if (pasteWatcher != null) { ++ pasteWatcher?.onPaste(items) ++ return true ++ } ++ } ++ ++ return false + } -+ } + return super.onTextContextMenuItem(modifiedId) -+ } ++ } internal fun clearFocusAndMaybeRefocus() { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !isInTouchMode) { -@@ -421,6 +470,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat +@@ -421,6 +484,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat this.scrollWatcher = scrollWatcher } @@ -516,10 +640,10 @@ index 42f6e03..ffba031 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.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt -index f47c87a..3588df6 100644 +index 35cd70f..e90ffc0 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt -@@ -135,6 +135,8 @@ public open class ReactTextInputManager public constructor() : +@@ -138,6 +138,8 @@ public open class ReactTextInputManager public constructor() : mapOf(getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"))) eventTypeConstants.putAll( mapOf(ReactTextClearEvent.EVENT_NAME to mapOf("registrationName" to "onClear"))) @@ -528,7 +652,7 @@ index f47c87a..3588df6 100644 return eventTypeConstants } -@@ -349,6 +351,15 @@ public open class ReactTextInputManager public constructor() : +@@ -352,6 +354,15 @@ public open class ReactTextInputManager public constructor() : } } @@ -544,7 +668,7 @@ index f47c87a..3588df6 100644 @ReactProp(name = "onKeyPress", defaultBoolean = false) public fun setOnKeyPress(view: ReactEditText, onKeyPress: Boolean) { view.setOnKeyPress(onKeyPress) -@@ -966,6 +977,24 @@ public open class ReactTextInputManager public constructor() : +@@ -969,6 +980,25 @@ public open class ReactTextInputManager public constructor() : } } @@ -559,9 +683,10 @@ index f47c87a..3588df6 100644 + mSurfaceId = UIManagerHelper.getSurfaceId(reactContext) + } + -+ override fun onPaste(type: String, data: String) { ++ override fun onPaste(items: List>) { ++ val itemsList = items.map { Pair(it.first, it.second) } + mEventDispatcher?.dispatchEvent( -+ ReactTextInputPasteEvent(mSurfaceId, mReactEditText.id, type, data) ++ ReactTextInputPasteEvent(mSurfaceId, mReactEditText.id, itemsList) + ) + } + } @@ -571,10 +696,10 @@ index f47c87a..3588df6 100644 "AutoCapitalizationType" to diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt new file mode 100644 -index 0000000..98bb63f +index 0000000..f35cdd8 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt -@@ -0,0 +1,61 @@ +@@ -0,0 +1,83 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -582,77 +707,102 @@ index 0000000..98bb63f + * LICENSE file in the root directory of this source tree. + */ + -+package com.facebook.react.views.textinput -+ -+import androidx.annotation.Nullable -+import com.facebook.react.bridge.Arguments -+import com.facebook.react.bridge.WritableMap -+import com.facebook.react.bridge.WritableArray -+import com.facebook.react.uimanager.common.ViewUtil -+import com.facebook.react.uimanager.events.Event -+ -+/** -+ * Event emitted by EditText native view when clipboard content is pasted -+ */ -+public class ReactTextInputPasteEvent : Event { -+ -+ public companion object { -+ private const val EVENT_NAME = "topPaste" -+ } -+ -+ private val mType: String -+ private val mData: String -+ -+ @Deprecated("Use constructor with surfaceId") -+ public constructor(viewId: Int, type: String, data: String) : -+ this(ViewUtil.NO_SURFACE_ID, viewId, type, data) -+ -+ public constructor(surfaceId: Int, viewId: Int, type: String, data: String) : -+ super(surfaceId, viewId) { -+ mType = type -+ mData = data -+ } -+ -+ override fun getEventName(): String { -+ return EVENT_NAME -+ } -+ -+ override fun canCoalesce(): Boolean { -+ return false -+ } -+ -+ @Nullable -+ override fun getEventData(): WritableMap? { -+ val eventData = Arguments.createMap() -+ -+ val items: WritableArray = Arguments.createArray() -+ val item: WritableMap = Arguments.createMap() -+ item.putString("type", mType) -+ item.putString("data", mData) -+ items.pushMap(item) -+ -+ eventData.putArray("items", items) -+ -+ return eventData -+ } -+} ++ package com.facebook.react.views.textinput ++ ++ import androidx.annotation.Nullable ++ import com.facebook.react.bridge.Arguments ++ import com.facebook.react.bridge.WritableMap ++ import com.facebook.react.bridge.WritableArray ++ import com.facebook.react.uimanager.common.ViewUtil ++ import com.facebook.react.uimanager.events.Event ++ ++ /** ++ * Event emitted by EditText native view when clipboard content is pasted ++ */ ++ public class ReactTextInputPasteEvent : Event { ++ ++ public companion object { ++ private const val EVENT_NAME = "topPaste" ++ } ++ ++ private val mType: String? ++ private val mData: String? ++ private val mItems: List>? ++ ++ @Deprecated("Use constructor with surfaceId") ++ public constructor(viewId: Int, type: String, data: String) : ++ this(ViewUtil.NO_SURFACE_ID, viewId, type, data) ++ ++ public constructor(surfaceId: Int, viewId: Int, type: String, data: String) : ++ super(surfaceId, viewId) { ++ mType = type ++ mData = data ++ mItems = null ++ } ++ ++ public constructor(surfaceId: Int, viewId: Int, items: List>) : ++ super(surfaceId, viewId) { ++ mType = null ++ mData = null ++ mItems = items ++ } ++ ++ override fun getEventName(): String { ++ return EVENT_NAME ++ } ++ ++ override fun canCoalesce(): Boolean { ++ return false ++ } ++ ++ @Nullable ++ override fun getEventData(): WritableMap? { ++ val eventData = Arguments.createMap() ++ ++ val items: WritableArray = Arguments.createArray() ++ ++ if (mItems != null) { ++ // Multiple items ++ for ((type, data) in mItems) { ++ val item: WritableMap = Arguments.createMap() ++ item.putString("type", type) ++ item.putString("data", data) ++ items.pushMap(item) ++ } ++ } else if (mType != null && mData != null) { ++ // Single item (backward compatibility) ++ val item: WritableMap = Arguments.createMap() ++ item.putString("type", mType) ++ item.putString("data", mData) ++ items.pushMap(item) ++ } ++ ++ eventData.putArray("items", items) ++ ++ return eventData ++ } ++ } ++ +\ No newline at end of file diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp -index 42c445b..22ff2d9 100644 +index 42c445b..f3b6e14 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp -@@ -181,6 +181,19 @@ void TextInputEventEmitter::onScroll(const Metrics& textInputMetrics) const { +@@ -181,6 +181,21 @@ void TextInputEventEmitter::onScroll(const Metrics& textInputMetrics) const { }); } -+void TextInputEventEmitter::onPaste(const std::string& type, const std::string& data) const { -+ dispatchEvent("onPaste", [type, data](jsi::Runtime& runtime) { ++void TextInputEventEmitter::onPaste(const std::vector>& items) const { ++ dispatchEvent("onPaste", [items](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); -+ auto items = jsi::Array(runtime, 1); -+ auto item = jsi::Object(runtime); -+ item.setProperty(runtime, "type", type); -+ item.setProperty(runtime, "data", data); -+ items.setValueAtIndex(runtime, 0, item); -+ payload.setProperty(runtime, "items", items); ++ auto itemsArray = jsi::Array(runtime, items.size()); ++ for (size_t i = 0; i < items.size(); i++) { ++ auto item = jsi::Object(runtime); ++ item.setProperty(runtime, "type", jsi::String::createFromUtf8(runtime, items[i].first)); ++ item.setProperty(runtime, "data", jsi::String::createFromUtf8(runtime, items[i].second)); ++ itemsArray.setValueAtIndex(runtime, i, item); ++ } ++ payload.setProperty(runtime, "items", itemsArray); + return payload; + }); +} @@ -661,14 +811,14 @@ index 42c445b..22ff2d9 100644 const std::string& name, const Metrics& textInputMetrics, diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h -index 968c93c..4396346 100644 +index 968c93c..8cb0634 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h +++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h @@ -45,6 +45,7 @@ class TextInputEventEmitter : public ViewEventEmitter { void onSubmitEditing(const Metrics& textInputMetrics) const; void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; void onScroll(const Metrics& textInputMetrics) const; -+ void onPaste(const std::string& type, const std::string& data) const; ++ void onPaste(const std::vector>& items) const; private: void dispatchTextInputEvent( From 5b479fe3197cef5c98c47b69e8c4d88a9c0e0672 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 19 Feb 2026 20:26:16 +0000 Subject: [PATCH 06/16] chore: update `react-native-live-markdown --- ios/Podfile.lock | 4 ++-- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0d49832c18eb..c240aab7ffeb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3528,7 +3528,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.321): + - RNLiveMarkdown (0.1.324): - boost - DoubleConversion - fast_float @@ -4737,7 +4737,7 @@ SPEC CHECKSUMS: RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 RNGestureHandler: b8d2e75c2e88fc2a1f6be3b3beeeed80b88fa37d RNGoogleSignin: 89877c73f0fbf6af2038fbdb7b73b5a25b8330cc - RNLiveMarkdown: 93e71e9e8623139368e0e9a8d9c54a2beb67e731 + RNLiveMarkdown: 60621617bc0504ac39669e3a8d1e9cadf1ada34f RNLocalize: 05e367a873223683f0e268d0af9a8a8e6aed3b26 rnmapbox-maps: 392ac61c42a9ff01a51d4c2f6775d9131b5951fb RNNitroSQLite: fb251387cfbee73b100cd484a3c886fda681b3b5 diff --git a/package-lock.json b/package-lock.json index b50703a23191..75df2a0b6672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.321", + "@expensify/react-native-live-markdown": "0.1.324", "@expensify/react-native-wallet": "0.1.11", "@expo/metro-runtime": "^6.0.2", "@firebase/app": "^0.13.2", @@ -5645,9 +5645,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.321", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.321.tgz", - "integrity": "sha512-wNCGQe1fc7AICCGh94AZjky5ZqfQ2ot9vQJECuH50IBfkuePOGQ/HaLmEjLeFk3Ak3+iBVt05v0dlicT4j7nCA==", + "version": "0.1.324", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.324.tgz", + "integrity": "sha512-o/mlt1R30uQ19wm2B1v2wJMJF4z3AGTMl6GK/StJBjM7TVluDKTxglBrhxZSwrg7LoVx+8SXKotIw1nAtnwutQ==", "license": "MIT", "workspaces": [ "./example", diff --git a/package.json b/package.json index f69b8fde7cfb..f8bf25b93b6f 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.321", + "@expensify/react-native-live-markdown": "0.1.324", "@expensify/react-native-wallet": "0.1.11", "@expo/metro-runtime": "^6.0.2", "@firebase/app": "^0.13.2", From 9fbf82a14e91fbf88a0a432c26453dd87edfa7ae Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 20 Feb 2026 12:21:30 +0000 Subject: [PATCH 07/16] chore: extract `Podfile.lock` changes into separate PR --- ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c240aab7ffeb..0d49832c18eb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3528,7 +3528,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.324): + - RNLiveMarkdown (0.1.321): - boost - DoubleConversion - fast_float @@ -4737,7 +4737,7 @@ SPEC CHECKSUMS: RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 RNGestureHandler: b8d2e75c2e88fc2a1f6be3b3beeeed80b88fa37d RNGoogleSignin: 89877c73f0fbf6af2038fbdb7b73b5a25b8330cc - RNLiveMarkdown: 60621617bc0504ac39669e3a8d1e9cadf1ada34f + RNLiveMarkdown: 93e71e9e8623139368e0e9a8d9c54a2beb67e731 RNLocalize: 05e367a873223683f0e268d0af9a8a8e6aed3b26 rnmapbox-maps: 392ac61c42a9ff01a51d4c2f6775d9131b5951fb RNNitroSQLite: fb251387cfbee73b100cd484a3c886fda681b3b5 From b5a409aaed3a929e672815282fc77dec992aebdc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 20 Feb 2026 18:33:21 +0000 Subject: [PATCH 08/16] revert: RNLM bump --- package-lock.json | 20 ++++++++++---------- package.json | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ea7ddf90402..07d1db9aea69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.23-2", + "version": "9.3.24-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.23-2", + "version": "9.3.24-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,7 +18,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.324", + "@expensify/react-native-live-markdown": "0.1.321", "@expensify/react-native-wallet": "0.1.11", "@expo/metro-runtime": "^6.0.2", "@firebase/app": "^0.13.2", @@ -119,7 +119,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.34", + "react-native-onyx": "3.0.35", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-performance": "^6.0.0", @@ -5645,9 +5645,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.324", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.324.tgz", - "integrity": "sha512-o/mlt1R30uQ19wm2B1v2wJMJF4z3AGTMl6GK/StJBjM7TVluDKTxglBrhxZSwrg7LoVx+8SXKotIw1nAtnwutQ==", + "version": "0.1.321", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.321.tgz", + "integrity": "sha512-wNCGQe1fc7AICCGh94AZjky5ZqfQ2ot9vQJECuH50IBfkuePOGQ/HaLmEjLeFk3Ak3+iBVt05v0dlicT4j7nCA==", "license": "MIT", "workspaces": [ "./example", @@ -34285,9 +34285,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.34", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.34.tgz", - "integrity": "sha512-pq7TcAa2NQn2tk3CrUIC2RArfKeAeCJqW5VMt5cRQ5CIhqVBT04oJpZJicBB8HZetZeIba3+bDYFvWhr5JuaVQ==", + "version": "3.0.35", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.35.tgz", + "integrity": "sha512-YlDoiN7IGi/Mj0UUwv/cPz33IOITNeQFohlNL8RdEppC88aA/Xv4vmoTs9AC71AleDx0OPOoDW/lm5jbxkzphQ==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index cc90e0e1659c..aa8f603aa47a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.23-2", + "version": "9.3.24-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -84,7 +84,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.324", + "@expensify/react-native-live-markdown": "0.1.321", "@expensify/react-native-wallet": "0.1.11", "@expo/metro-runtime": "^6.0.2", "@firebase/app": "^0.13.2", @@ -185,7 +185,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.34", + "react-native-onyx": "3.0.35", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-performance": "^6.0.0", From 2b240cc1f01ce1ad639c1c08651160f7d86ad2bc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 24 Feb 2026 09:49:21 +0000 Subject: [PATCH 09/16] fix: prettier --- src/components/Composer/implementation/index.native.tsx | 2 +- src/components/Composer/implementation/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 605c9d493cd3..c8e135266290 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -12,11 +12,11 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {containsOnlyEmojis} from '@libs/EmojiUtils'; import {splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; +import Log from '@libs/Log'; import Parser from '@libs/Parser'; import getFileSize from '@pages/Share/getFileSize'; import CONST from '@src/CONST'; import type {FileObject} from '@src/types/utils/Attachment'; -import Log from '@libs/Log'; const excludeNoStyles: Array = []; const excludeReportMentionStyle: Array = ['mentionReport']; diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 4f406d38c9f2..319f5a52077b 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -19,10 +19,10 @@ import {isMobileSafari, isSafari} from '@libs/Browser'; import {containsOnlyEmojis} from '@libs/EmojiUtils'; import {base64ToFile} from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; +import Log from '@libs/Log'; import Parser from '@libs/Parser'; -import type {FileObject} from '@src/types/utils/Attachment'; import CONST from '@src/CONST'; -import Log from '@libs/Log'; +import type {FileObject} from '@src/types/utils/Attachment'; const excludeNoStyles: Array = []; const excludeReportMentionStyle: Array = ['mentionReport']; From 624b888293ce85925d3f75e7c54cf0631a0d9c29 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 26 Feb 2026 23:17:45 +0000 Subject: [PATCH 10/16] fix: not waiting for promises to fulfill --- src/components/Composer/implementation/index.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 84b222fd2973..428b4053103f 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -200,13 +200,8 @@ function Composer({ return Promise.resolve(undefined); }); - Promise.all(filePromises) - .then((f) => { - files.push(...f); - }) - .catch((error) => { - Log.warn('Pasted files could not be validated', {error}); - }); + const f = await Promise.all(filePromises) + files.push(...f); } const validFiles = files.filter((file) => file !== undefined); From ff9f51d82b0272da791f2b7aa7eaf88ae9717d32 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 26 Feb 2026 23:19:14 +0000 Subject: [PATCH 11/16] fix: remove unnecessary check --- .../inbox/report/ReportActionCompose/ReportActionCompose.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx index dc2ca286c8a7..9d2dc578d45a 100644 --- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx @@ -546,10 +546,6 @@ function ReportActionCompose({ }); const handleAttachmentDrop = (event: DragEvent) => { - if (isAttachmentPreviewActive) { - return; - } - const files = Array.from(event.dataTransfer?.files ?? []).map((file) => { const fileWithUri = file; From 333f7679cec8086144e7aa7b0c0cf2c991fe86d5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 26 Feb 2026 23:23:37 +0000 Subject: [PATCH 12/16] fix: address not fulfilling promises --- src/components/Composer/implementation/index.tsx | 5 ++--- src/hooks/useHtmlPaste/types.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 428b4053103f..0bdb57165630 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -20,7 +20,6 @@ import {isMobileSafari, isSafari} from '@libs/Browser'; import {containsOnlyEmojis} from '@libs/EmojiUtils'; import {base64ToFile} from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import Log from '@libs/Log'; import Parser from '@libs/Parser'; import CONST from '@src/CONST'; import type {FileObject} from '@src/types/utils/Attachment'; @@ -138,7 +137,7 @@ function Composer({ * Otherwise, convert pasted HTML to Markdown and set it on the composer. */ const handlePaste = useCallback( - (event: ClipboardEvent) => { + async (event: ClipboardEvent) => { const isVisible = checkComposerVisibility(); const isFocused = textInput.current?.isFocused(); const isContenteditableDivFocused = document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable'); @@ -200,7 +199,7 @@ function Composer({ return Promise.resolve(undefined); }); - const f = await Promise.all(filePromises) + const f = await Promise.all(filePromises); files.push(...f); } diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts index c8d8e9e6ba9b..4de492d4cdcd 100644 --- a/src/hooks/useHtmlPaste/types.ts +++ b/src/hooks/useHtmlPaste/types.ts @@ -3,7 +3,7 @@ import type {TextInput} from 'react-native'; type UseHtmlPaste = ( textInputRef: RefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>, - preHtmlPasteCallback?: (event: ClipboardEvent) => boolean, + preHtmlPasteCallback?: (event: ClipboardEvent) => boolean | Promise, isActive?: boolean, maxLength?: number, // Maximum length of the text input value after pasting ) => void | { From a5192a42467f7e8300bd8d81018e00020000ed1f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 27 Feb 2026 11:08:55 +0000 Subject: [PATCH 13/16] fix: composer html paste --- .../Composer/implementation/index.tsx | 18 +++++++++++------- src/hooks/useHtmlPaste/types.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 0bdb57165630..25c2722755b4 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -181,6 +181,15 @@ function Composer({ } } + const pasteValidFiles = async () => { + const validFiles = files.filter((file) => file !== undefined); + if (validFiles.length > 0) { + onPasteFile(validFiles); + return true; + } + return false; + }; + // If paste contains image from Google Workspaces ex: Sheets, Docs, Slide, etc if (clipboardDataHtml?.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { const domparser = new DOMParser(); @@ -201,15 +210,10 @@ function Composer({ const f = await Promise.all(filePromises); files.push(...f); + return pasteValidFiles(); } - const validFiles = files.filter((file) => file !== undefined); - if (validFiles.length > 0) { - onPasteFile(validFiles); - return true; - } - - return false; + return pasteValidFiles(); }, [onPasteFile, checkComposerVisibility], ); diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts index 4de492d4cdcd..c8d8e9e6ba9b 100644 --- a/src/hooks/useHtmlPaste/types.ts +++ b/src/hooks/useHtmlPaste/types.ts @@ -3,7 +3,7 @@ import type {TextInput} from 'react-native'; type UseHtmlPaste = ( textInputRef: RefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>, - preHtmlPasteCallback?: (event: ClipboardEvent) => boolean | Promise, + preHtmlPasteCallback?: (event: ClipboardEvent) => boolean, isActive?: boolean, maxLength?: number, // Maximum length of the text input value after pasting ) => void | { From 662e423b94cc4a0df5f3e7d1bb734c7dd216af8b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 27 Feb 2026 18:17:48 +0000 Subject: [PATCH 14/16] fix: typecheck --- .../Composer/implementation/index.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 25c2722755b4..0189f6325f62 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -137,7 +137,7 @@ function Composer({ * Otherwise, convert pasted HTML to Markdown and set it on the composer. */ const handlePaste = useCallback( - async (event: ClipboardEvent) => { + (event: ClipboardEvent) => { const isVisible = checkComposerVisibility(); const isFocused = textInput.current?.isFocused(); const isContenteditableDivFocused = document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable'); @@ -181,13 +181,14 @@ function Composer({ } } - const pasteValidFiles = async () => { + const pasteValidFiles = () => { const validFiles = files.filter((file) => file !== undefined); - if (validFiles.length > 0) { - onPasteFile(validFiles); - return true; + if (validFiles.length === 0) { + return false; } - return false; + + onPasteFile(validFiles); + return true; }; // If paste contains image from Google Workspaces ex: Sheets, Docs, Slide, etc @@ -208,9 +209,11 @@ function Composer({ return Promise.resolve(undefined); }); - const f = await Promise.all(filePromises); - files.push(...f); - return pasteValidFiles(); + Promise.all(filePromises).then((f) => { + files.push(...f.filter((file) => file !== undefined)); + pasteValidFiles(); + }); + return true; } return pasteValidFiles(); From 82974ed83ab5b2d8b86f0775f96b0ad301d29035 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 27 Feb 2026 18:30:39 +0000 Subject: [PATCH 15/16] fix: remove unnecessary `Promise.resolve` --- src/components/Composer/implementation/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index c8e135266290..9e9ac7d3fc00 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -90,10 +90,10 @@ function Composer({ const pasteFile = useCallback( (e: NativeSyntheticEvent) => { - const filePromises: Array> = e.nativeEvent.items.map((item) => { + const filePromises: Array> = e.nativeEvent.items.map(async (item) => { const clipboardContent = item; if (clipboardContent?.type === 'text/plain') { - return Promise.resolve(undefined); + return; } const mimeType = clipboardContent?.type ?? ''; From 3a64c9ecedaecc807bc3518501b661f8c386d797 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 2 Mar 2026 22:02:36 +0000 Subject: [PATCH 16/16] Update details.md --- patches/react-native/details.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index be59cade91d9..392f96e40af9 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -75,7 +75,7 @@ ### [react-native+0.81.4+011+Add-onPaste-to-TextInput.patch](react-native+0.81.4+011+Add-onPaste-to-TextInput.patch) - Reasons: - - Adds `onPaste` callback to `TextInput` to support image pasting on native + - Adds `onPaste` callback to `TextInput` to support image and file pasting on native - Fixes an issue where pasted image displays as binary text on some Android devices where rich clipboard data is stored in binary form - Fixes an issue where pasting from WPS Office app crashes the app on Android where its content URI is not recognized by Android `ContentResolver` - Fixes an issue where mentions copied from mWeb and pasted on Android are not displayed. @@ -188,7 +188,7 @@ ``` This patch restores the old InteractionManager behavior. React Native 0.80 deprecated InteractionManager and modified it to behave like `setImmediate`, more info here - https://github.com/facebook/react-native/blob/d9262c60f4c02d66417008970dc9c34b742aaa75/CHANGELOG.md?plain=1#L597 - + We need to restore the previous behavior to avoid introducing any bugs in the app. Bug example - https://github.com/Expensify/App/pull/69535#issuecomment-3443059319 ``` @@ -206,7 +206,7 @@ ### [react-native+0.81.4+027+perf-disable-hermes-young-gc-before-tti-reached.patch](react-native+0.81.4+027+perf-disable-hermes-young-gc-before-tti-reached.patch) -- Reason: This patch disables Hermes Young-Gen Garbage Collection (GC), which improves initial TTI and app startup time, by delaying GC for early allocated memory to the first Old-Gen GC run. +- Reason: This patch disables Hermes Young-Gen Garbage Collection (GC), which improves initial TTI and app startup time, by delaying GC for early allocated memory to the first Old-Gen GC run. - Upstream PR/issue: This is not intended to be upstreamed, since this is a low-level fix very specific to the Expensify app's requirements. - E/App issue: [#76859](https://github.com/Expensify/App/issues/76859) - PR introducing patch: [#76154](https://github.com/Expensify/App/pull/76154)