Skip to content

Commit eb1ce69

Browse files
authored
Merge pull request Expensify#68121 from lorretheboy/fix/67004
Conversation - App freezes when uploading attachment >24MB – No error modal appears
2 parents 8255359 + 8afd57b commit eb1ce69

2 files changed

Lines changed: 71 additions & 27 deletions

File tree

src/components/AttachmentPicker/index.native.tsx

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
1818
import useStyleUtils from '@hooks/useStyleUtils';
1919
import useTheme from '@hooks/useTheme';
2020
import useThemeStyles from '@hooks/useThemeStyles';
21-
import {cleanFileName, showCameraPermissionsAlert, verifyFileFormat} from '@libs/fileDownload/FileUtils';
21+
import {cleanFileName, resizeImageIfNeeded, showCameraPermissionsAlert, verifyFileFormat} from '@libs/fileDownload/FileUtils';
2222
import type {FileObject, ImagePickerResponse as FileResponse} from '@pages/media/AttachmentModalScreen/types';
2323
import CONST from '@src/CONST';
2424
import type {TranslationPaths} from '@src/languages/types';
@@ -341,32 +341,38 @@ function AttachmentPicker({
341341
};
342342

343343
if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) {
344-
return ImageSize.getSize(fileDataUri)
345-
.then(({width, height}) => {
346-
fileDataObject.width = width;
347-
fileDataObject.height = height;
348-
return fileDataObject;
349-
})
350-
.then((file) => getDataForUpload(file))
344+
return getDataForUpload(fileDataObject)
345+
.then((file) => resizeImageIfNeeded(file))
346+
.then((resizedFile) =>
347+
ImageSize.getSize(resizedFile.uri ?? '').then(({width, height}) => ({
348+
...resizedFile,
349+
width,
350+
height,
351+
})),
352+
)
351353
.catch(() => {
352354
showImageCorruptionAlert();
353355
return null;
354356
});
355357
}
356358

357359
if (fileDataName && Str.isImage(fileDataName)) {
358-
return ImageSize.getSize(fileDataUri)
359-
.then(({width, height}) => {
360-
fileDataObject.width = width;
361-
fileDataObject.height = height;
362-
363-
if (fileDataObject.width <= 0 || fileDataObject.height <= 0) {
364-
showImageCorruptionAlert();
365-
return null;
366-
}
367-
368-
return getDataForUpload(fileDataObject);
369-
})
360+
return getDataForUpload(fileDataObject)
361+
.then((file) => resizeImageIfNeeded(file))
362+
.then((resizedFile) =>
363+
ImageSize.getSize(resizedFile.uri ?? '').then(({width, height}) => {
364+
if (width <= 0 || height <= 0) {
365+
showImageCorruptionAlert();
366+
return null;
367+
}
368+
369+
return {
370+
...resizedFile,
371+
width,
372+
height,
373+
};
374+
}),
375+
)
370376
.catch(() => {
371377
showImageCorruptionAlert();
372378
return null;

src/libs/fileDownload/FileUtils.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {Str} from 'expensify-common';
22
import {Alert, Linking, Platform} from 'react-native';
3+
import ReactNativeBlobUtil from 'react-native-blob-util';
4+
import type {ReactNativeBlobUtilReadStream} from 'react-native-blob-util';
35
import ImageSize from 'react-native-image-size';
46
import type {TupleToUnion, ValueOf} from 'type-fest';
57
import DateUtils from '@libs/DateUtils';
@@ -272,19 +274,55 @@ function validateImageForCorruption(file: FileObject): Promise<{width: number; h
272274

273275
/** Verify file format based on the magic bytes of the file - some formats might be identified by multiple signatures */
274276
function verifyFileFormat({fileUri, formatSignatures}: {fileUri: string; formatSignatures: readonly string[]}) {
275-
return fetch(fileUri)
276-
.then((file) => file.arrayBuffer())
277-
.then((arrayBuffer) => {
278-
const uintArray = new Uint8Array(arrayBuffer, 4, 12);
277+
const BYTES_TO_READ = 1028;
279278

280-
const hexString = Array.from(uintArray)
279+
const cleanUri = fileUri.replace('file://', '');
280+
281+
return ReactNativeBlobUtil.fs
282+
.readStream(cleanUri, 'base64', BYTES_TO_READ, 0)
283+
.then((stream: ReactNativeBlobUtilReadStream) => {
284+
let base64Chunk = '';
285+
286+
return new Promise<string>((resolve, reject) => {
287+
stream.open();
288+
289+
stream.onData((chunk: string | number[]) => {
290+
const chunkStr = Array.isArray(chunk) ? String.fromCharCode(...chunk) : chunk;
291+
base64Chunk += chunkStr;
292+
});
293+
294+
stream.onError((error: Error) => {
295+
reject(error);
296+
});
297+
298+
stream.onEnd(() => {
299+
resolve(base64Chunk);
300+
});
301+
});
302+
})
303+
.then((base64Data: string) => {
304+
const binary = Buffer.from(base64Data, 'base64').toString('binary');
305+
306+
const bytes = new Uint8Array(binary.length);
307+
for (let i = 0; i < binary.length; i++) {
308+
bytes[i] = binary.charCodeAt(i);
309+
}
310+
311+
const startIndex = Math.min(4, bytes.length);
312+
const endIndex = Math.min(16, bytes.length);
313+
const view = bytes.subarray(startIndex, endIndex);
314+
315+
const hex = Array.from(view)
281316
.map((b) => b.toString(16).padStart(2, '0'))
282317
.join('');
283318

284-
return hexString;
319+
return hex;
285320
})
286-
.then((hexSignature) => {
321+
.then((hexSignature: string) => {
287322
return formatSignatures.some((signature) => hexSignature.startsWith(signature));
323+
})
324+
.catch(() => {
325+
return false;
288326
});
289327
}
290328

0 commit comments

Comments
 (0)