Skip to content

Commit

Permalink
Merge pull request Expensify#28449 from fvlvte/24917-migrate-fileDown…
Browse files Browse the repository at this point in the history
…load-lib-to-typescript

[TS migration] Migrate 'fileDownload' lib to TypeScript
  • Loading branch information
flodnv authored Nov 21, 2023
2 parents 71219f2 + b163dce commit 0cca90a
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 187 deletions.
6 changes: 3 additions & 3 deletions src/libs/actions/PersonalDetails.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Str from 'expensify-common/lib/str';
import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
import {CustomRNImageManipulatorResult, FileWithUri} from '@libs/cropOrRotateImage/types';
import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -445,7 +445,7 @@ function openPublicProfilePage(accountID: number) {
/**
* Updates the user's avatar image
*/
function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) {
function updateAvatar(file: File | CustomRNImageManipulatorResult) {
if (!currentUserAccountID) {
return;
}
Expand Down Expand Up @@ -501,7 +501,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) {
];

type UpdateUserAvatarParams = {
file: FileWithUri | CustomRNImageManipulatorResult;
file: File | CustomRNImageManipulatorResult;
};

const parameters: UpdateUserAvatarParams = {file};
Expand Down
6 changes: 3 additions & 3 deletions src/libs/cropOrRotateImage/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CropOptions, CropOrRotateImage, CropOrRotateImageOptions, FileWithUri} from './types';
import {CropOptions, CropOrRotateImage, CropOrRotateImageOptions} from './types';

type SizeFromAngle = {
width: number;
Expand Down Expand Up @@ -71,13 +71,13 @@ function cropCanvas(canvas: HTMLCanvasElement, options: CropOptions) {
return result;
}

function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise<FileWithUri> {
function convertCanvasToFile(canvas: HTMLCanvasElement, options: CropOrRotateImageOptions): Promise<File> {
return new Promise((resolve) => {
canvas.toBlob((blob) => {
if (!blob) {
return;
}
const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'}) as FileWithUri;
const file = new File([blob], options.name || 'fileName.jpeg', {type: options.type || 'image/jpeg'});
file.uri = URL.createObjectURL(file);
resolve(file);
});
Expand Down
8 changes: 2 additions & 6 deletions src/libs/cropOrRotateImage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ type Action = {
rotate?: number;
};

type FileWithUri = File & {
uri: string;
};

type CustomRNImageManipulatorResult = RNImageManipulatorResult & {size: number; type: string; name: string};

type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise<FileWithUri | CustomRNImageManipulatorResult>;
type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise<File | CustomRNImageManipulatorResult>;

export type {CropOrRotateImage, CropOptions, Action, FileWithUri, CropOrRotateImageOptions, CustomRNImageManipulatorResult};
export type {CropOrRotateImage, CropOptions, Action, CropOrRotateImageOptions, CustomRNImageManipulatorResult};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Alert, Linking, Platform} from 'react-native';
import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
import CONST from '@src/CONST';
import type {ReadFileAsync, SplitExtensionFromFileName} from './types';

/**
* Show alert on successful attachment download
Expand Down Expand Up @@ -43,7 +44,9 @@ function showPermissionErrorAlert() {
},
{
text: Localize.translateLocal('common.settings'),
onPress: () => Linking.openSettings(),
onPress: () => {
Linking.openSettings();
},
},
]);
}
Expand All @@ -62,7 +65,9 @@ function showCameraPermissionsAlert() {
},
{
text: Localize.translateLocal('common.settings'),
onPress: () => Linking.openSettings(),
onPress: () => {
Linking.openSettings();
},
},
],
{cancelable: false},
Expand All @@ -71,42 +76,36 @@ function showCameraPermissionsAlert() {

/**
* Generate a random file name with timestamp and file extension
* @param {String} url
* @returns {String}
*/
function getAttachmentName(url) {
function getAttachmentName(url: string): string {
if (!url) {
return '';
}
return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop().trim()}`;
return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop()?.trim()}`;
}

/**
* @param {String} fileName
* @returns {Boolean}
*/
function isImage(fileName) {
function isImage(fileName: string): boolean {
return CONST.FILE_TYPE_REGEX.IMAGE.test(fileName);
}

/**
* @param {String} fileName
* @returns {Boolean}
*/
function isVideo(fileName) {
function isVideo(fileName: string): boolean {
return CONST.FILE_TYPE_REGEX.VIDEO.test(fileName);
}

/**
* Returns file type based on the uri
* @param {String} fileUrl
* @returns {String}
*/
function getFileType(fileUrl) {
function getFileType(fileUrl: string): string | undefined {
if (!fileUrl) {
return;
}
const fileName = fileUrl.split('/').pop().split('?')[0].split('#')[0];

const fileName = fileUrl.split('/').pop()?.split('?')[0].split('#')[0];

if (!fileName) {
return;
}

if (isImage(fileName)) {
return CONST.ATTACHMENT_FILE_TYPE.IMAGE;
}
Expand All @@ -118,32 +117,22 @@ function getFileType(fileUrl) {

/**
* Returns the filename split into fileName and fileExtension
*
* @param {String} fullFileName
* @returns {Object}
*/
function splitExtensionFromFileName(fullFileName) {
const splitExtensionFromFileName: SplitExtensionFromFileName = (fullFileName) => {
const fileName = fullFileName.trim();
const splitFileName = fileName.split('.');
const fileExtension = splitFileName.length > 1 ? splitFileName.pop() : '';
return {fileName: splitFileName.join('.'), fileExtension};
}
return {fileName: splitFileName.join('.'), fileExtension: fileExtension ?? ''};
};

/**
* Returns the filename replacing special characters with underscore
*
* @param {String} fileName
* @returns {String}
*/
function cleanFileName(fileName) {
function cleanFileName(fileName: string): string {
return fileName.replace(/[^a-zA-Z0-9\-._]/g, '_');
}

/**
* @param {String} fileName
* @returns {String}
*/
function appendTimeToFileName(fileName) {
function appendTimeToFileName(fileName: string): string {
const file = splitExtensionFromFileName(fileName);
let newFileName = `${file.fileName}-${DateUtils.getDBTime()}`;
// Replace illegal characters before trying to download the attachment.
Expand All @@ -156,58 +145,61 @@ function appendTimeToFileName(fileName) {

/**
* Reads a locally uploaded file
*
* @param {String} path - the blob url of the locally uplodaded file
* @param {String} fileName
* @param {Function} onSuccess
* @param {Function} onFailure
*
* @returns {Promise}
* @param path - the blob url of the locally uploaded file
* @param fileName - name of the file to read
*/
const readFileAsync = (path, fileName, onSuccess, onFailure = () => {}) =>
const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}) =>
new Promise((resolve) => {
if (!path) {
resolve();
onFailure('[FileUtils] Path not specified');
return;
}

return fetch(path)
fetch(path)
.then((res) => {
// For some reason, fetch is "Unable to read uploaded file"
// on Android even though the blob is returned, so we'll ignore
// in that case
if (!res.ok && Platform.OS !== 'android') {
throw Error(res.statusText);
}
return res.blob();
})
.then((blob) => {
const file = new File([blob], cleanFileName(fileName), {type: blob.type});
file.source = path;
// For some reason, the File object on iOS does not have a uri property
// so images aren't uploaded correctly to the backend
file.uri = path;
onSuccess(file);
res.blob()
.then((blob) => {
const file = new File([blob], cleanFileName(fileName));
file.source = path;
// For some reason, the File object on iOS does not have a uri property
// so images aren't uploaded correctly to the backend
file.uri = path;
onSuccess(file);
resolve(file);
})
.catch((e) => {
console.debug('[FileUtils] Could not read uploaded file', e);
onFailure(e);
resolve();
});
})
.catch((e) => {
console.debug('[FileUtils] Could not read uploaded file', e);
onFailure(e);
resolve();
});
});

/**
* Converts a base64 encoded image string to a File instance.
* Adds a `uri` property to the File instance for accessing the blob as a URI.
*
* @param {string} base64 - The base64 encoded image string.
* @param {string} filename - Desired filename for the File instance.
* @returns {File} The File instance created from the base64 string with an additional `uri` property.
* @param base64 - The base64 encoded image string.
* @param filename - Desired filename for the File instance.
* @returns The File instance created from the base64 string with an additional `uri` property.
*
* @example
* const base64Image = "data:image/png;base64,..."; // your base64 encoded image
* const imageFile = base64ToFile(base64Image, "example.png");
* console.log(imageFile.uri); // Blob URI
*/
function base64ToFile(base64, filename) {
function base64ToFile(base64: string, filename: string): File {
// Decode the base64 string
const byteString = atob(base64.split(',')[1]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import CONST from '@src/CONST';
import type {GetAttachmentDetails} from './types';

/**
* Extract the thumbnail URL, source URL and the original filename from the HTML.
* @param {String} html
* @returns {Object}
*/
export default function getAttachmentDetails(html) {
const getAttachmentDetails: GetAttachmentDetails = (html) => {
// Files can be rendered either as anchor tag or as an image so based on that we have to form regex.
const IS_IMAGE_TAG = /<img([\w\W]+?)\/>/i.test(html);
const PREVIEW_SOURCE_REGEX = new RegExp(`${CONST.ATTACHMENT_PREVIEW_ATTRIBUTE}*=*"(.+?)"`, 'i');
Expand All @@ -21,15 +20,17 @@ export default function getAttachmentDetails(html) {
}

// Files created/uploaded/hosted by App should resolve from API ROOT. Other URLs aren't modified
const sourceURL = tryResolveUrlFromApiRoot(html.match(SOURCE_REGEX)[1]);
const imageURL = IS_IMAGE_TAG && tryResolveUrlFromApiRoot(html.match(PREVIEW_SOURCE_REGEX)[1]);
const sourceURL = tryResolveUrlFromApiRoot(html.match(SOURCE_REGEX)?.[1] ?? '');
const imageURL = IS_IMAGE_TAG ? tryResolveUrlFromApiRoot(html.match(PREVIEW_SOURCE_REGEX)?.[1] ?? '') : null;
const previewSourceURL = IS_IMAGE_TAG ? imageURL : sourceURL;
const originalFileName = html.match(ORIGINAL_FILENAME_REGEX)[1];
const originalFileName = html.match(ORIGINAL_FILENAME_REGEX)?.[1] ?? null;

// Update the image URL so the images can be accessed depending on the config environment
return {
previewSourceURL,
sourceURL,
originalFileName,
};
}
};

export default getAttachmentDetails;
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {Asset} from 'react-native-image-picker';
import type {GetImageResolution} from './types';

/**
* Get image resolution
* Image object is returned as a result of a user selecting image using the react-native-image-picker
* Image already has width and height properties coming from library so we just need to return them on native
* Opposite to web where we need to create a new Image object and get dimensions from it
*
* @param {*} file Picked file blob
* @returns {Promise}
*/
function getImageResolution(file) {
return Promise.resolve({width: file.width, height: file.height});
}
const getImageResolution: GetImageResolution = (file: Asset) => Promise.resolve({width: file.width ?? 0, height: file.height ?? 0});

export default getImageResolution;
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {GetImageResolution} from './types';

/**
* Get image resolution
* File object is returned as a result of a user selecting image using the <input type="file" />
Expand All @@ -7,10 +9,8 @@
* new Image() is used specifically for performance reasons, opposed to using FileReader (5ms vs +100ms)
* because FileReader is slow and causes a noticeable delay in the UI when selecting an image.
*
* @param {*} file Picked file blob
* @returns {Promise}
*/
function getImageResolution(file) {
const getImageResolution: GetImageResolution = (file) => {
if (!(file instanceof File)) {
return Promise.reject(new Error('Object is not an instance of File'));
}
Expand All @@ -20,14 +20,14 @@ function getImageResolution(file) {
const objectUrl = URL.createObjectURL(file);
image.onload = function () {
resolve({
width: this.naturalWidth,
height: this.naturalHeight,
width: (this as HTMLImageElement).naturalWidth,
height: (this as HTMLImageElement).naturalHeight,
});
URL.revokeObjectURL(objectUrl);
};
image.onerror = reject;
image.src = objectUrl;
});
}
};

export default getImageResolution;
Loading

0 comments on commit 0cca90a

Please sign in to comment.