Skip to content

Commit

Permalink
Merge pull request #76 from sendbird/refactor/createFileService
Browse files Browse the repository at this point in the history
[UIKIT-3427] Fix/fixed file download on android
  • Loading branch information
bang9 authored Mar 23, 2023
2 parents 701969d + 7401d55 commit 04fa93d
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
// Image compression
if (
isImage(mediaFile.uri, mediaFile.type) &&
shouldCompressImage(mediaFile.uri, features.imageCompressionEnabled)
shouldCompressImage(mediaFile.type, features.imageCompressionEnabled)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
Expand Down Expand Up @@ -170,7 +170,7 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
// Image compression
if (
isImage(mediaFile.uri, mediaFile.type) &&
shouldCompressImage(mediaFile.uri, features.imageCompressionEnabled)
shouldCompressImage(mediaFile.type, features.imageCompressionEnabled)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
Expand Down Expand Up @@ -203,7 +203,7 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
// Image compression
if (
isImage(documentFile.uri, documentFile.type) &&
shouldCompressImage(documentFile.uri, features.imageCompressionEnabled)
shouldCompressImage(documentFile.type, features.imageCompressionEnabled)
) {
await SBUUtils.safeRun(async () => {
const compressed = await mediaService.compressImage({
Expand Down
15 changes: 5 additions & 10 deletions packages/uikit-react-native/src/platform/createFileService.expo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type * as ExpoFs from 'expo-file-system';
import type * as ExpoImagePicker from 'expo-image-picker';
import type * as ExpoMediaLibrary from 'expo-media-library';

import { getFileExtension, getFileType } from '@sendbird/uikit-utils';
import { getFileType } from '@sendbird/uikit-utils';

import SBUError from '../libs/SBUError';
import type { ExpoMediaLibraryPermissionResponse, ExpoPermissionResponse } from '../utils/expoPermissionGranted';
import expoPermissionGranted from '../utils/expoPermissionGranted';
import fileTypeGuard from '../utils/fileTypeGuard';
import normalizeFile from '../utils/normalizeFile';
import type {
FilePickerResponse,
FileServiceInterface,
Expand Down Expand Up @@ -80,10 +80,8 @@ const createExpoFileService = ({

const { uri } = response;
const { size } = await fsModule.getInfoAsync(response.uri);
const ext = getFileExtension(uri);
const type = getFileType(ext);

return fileTypeGuard({ uri, size, type: `${type}/${ext.slice(1)}`, name: Date.now() + ext });
return normalizeFile({ uri, size });
}
async openMediaLibrary(options: OpenMediaLibraryOptions) {
const hasPermission = await this.hasMediaLibraryPermission('read');
Expand Down Expand Up @@ -111,19 +109,16 @@ const createExpoFileService = ({
});
if (response.cancelled) return null;
const { uri } = response;

const { size } = await fsModule.getInfoAsync(uri);
const ext = getFileExtension(uri);
const type = getFileType(ext);
return [fileTypeGuard({ uri, size, type: `${type}/${ext.slice(1)}`, name: Date.now() + ext })];
return [await normalizeFile({ uri, size })];
}

async openDocument(options?: OpenDocumentOptions): Promise<FilePickerResponse> {
try {
const response = await documentPickerModule.getDocumentAsync({ type: '*/*' });
if (response.type === 'cancel') return null;
const { mimeType, uri, size, name } = response;
return fileTypeGuard({ uri, size, name, type: mimeType });
return normalizeFile({ uri, size, name, type: mimeType });
} catch (e) {
options?.onOpenFailure?.(SBUError.UNKNOWN, e);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import type * as ImagePicker from 'react-native-image-picker';
import type * as Permissions from 'react-native-permissions';
import type { Permission } from 'react-native-permissions';

import { getFileExtension, getFileType, normalizeFileName } from '@sendbird/uikit-utils';
import {
getFileExtension,
getFileExtensionFromMime,
getFileExtensionFromUri,
getFileType,
normalizeFileName,
} from '@sendbird/uikit-utils';

import SBUError from '../libs/SBUError';
import fileTypeGuard from '../utils/fileTypeGuard';
import nativePermissionGranted from '../utils/nativePermissionGranted';
import normalizeFile from '../utils/normalizeFile';
import type {
FilePickerResponse,
FileServiceInterface,
Expand Down Expand Up @@ -124,7 +130,7 @@ const createNativeFileService = ({
}

const { fileName: name, fileSize: size, type, uri } = response.assets?.[0] ?? {};
return fileTypeGuard({ uri, size, name, type });
return normalizeFile({ uri, size, name, type });
}
async openMediaLibrary(options?: OpenMediaLibraryOptions): Promise<FilePickerResponse[] | null> {
/**
Expand Down Expand Up @@ -163,14 +169,16 @@ const createNativeFileService = ({
return null;
}

return (response.assets || [])
.slice(0, selectionLimit)
.map(({ fileName: name, fileSize: size, type, uri }) => fileTypeGuard({ uri, size, name, type }));
return Promise.all(
(response.assets || [])
.slice(0, selectionLimit)
.map(({ fileName: name, fileSize: size, type, uri }) => normalizeFile({ uri, size, name, type })),
);
}
async openDocument(options?: OpenDocumentOptions): Promise<FilePickerResponse> {
try {
const { uri, size, name, type } = await documentPickerModule.pickSingle();
return fileTypeGuard({ uri, size, name, type });
return normalizeFile({ uri, size, name, type });
} catch (e) {
if (!documentPickerModule.isCancel(e) && documentPickerModule.isInProgress(e)) {
options?.onOpenFailure?.(SBUError.UNKNOWN, e);
Expand All @@ -185,33 +193,50 @@ const createNativeFileService = ({
if (!granted) throw new Error('Permission not granted');
}

const basePath = Platform.select({ android: fsModule.Dirs.CacheDir, default: fsModule.Dirs.DocumentDir });
let downloadPath = `${basePath}/${options.fileName}`;
if (!getFileExtension(options.fileName)) {
const extensionFromUrl = getFileExtension(options.fileUrl);
if (getFileType(extensionFromUrl).match(/image|video/)) {
downloadPath += extensionFromUrl;
}
}

await fsModule.FileSystem.fetch(options.fileUrl, { path: downloadPath });
const fileType = getFileType(getFileExtension(options.fileUrl));
const { downloadedPath, file } = await this.downloadFile(options);

if (Platform.OS === 'ios' && (fileType === 'image' || fileType === 'video')) {
const type = ({ 'image': 'photo', 'video': 'video' } as const)[fileType];
await mediaLibraryModule.save(downloadPath, { type });
if (Platform.OS === 'ios') {
if (file.type === 'image' || file.type === 'video') {
const mediaTypeMap = { 'image': 'photo', 'video': 'video' } as const;
const mediaType = mediaTypeMap[file.type];
await mediaLibraryModule.save(downloadedPath, { type: mediaType });
}
}

if (Platform.OS === 'android') {
const dirType = { 'file': 'downloads', 'audio': 'audio', 'image': 'images', 'video': 'video' } as const;
await fsModule.FileSystem.cpExternal(
downloadPath,
normalizeFileName(options.fileName, getFileExtension(options.fileUrl)),
dirType[fileType],
);
const externalDirMap = { 'file': 'downloads', 'audio': 'audio', 'image': 'images', 'video': 'video' } as const;
const externalDir = externalDirMap[file.type];
await fsModule.FileSystem.cpExternal(downloadedPath, file.name, externalDir);
}
return downloadPath;

return downloadedPath;
}

private buildDownloadPath = async (options: SaveOptions) => {
const dirname = Platform.select({ android: fsModule.Dirs.CacheDir, default: fsModule.Dirs.DocumentDir });
const context = { dirname, filename: options.fileName };
const extension =
getFileExtension(options.fileName) ||
getFileExtensionFromMime(options.fileType) ||
getFileExtension(options.fileUrl) ||
(await getFileExtensionFromUri(options.fileUrl));

if (extension) context.filename = normalizeFileName(context.filename, extension);

return { path: `${context.dirname}/${context.filename}`, ...context };
};

private downloadFile = async (options: SaveOptions) => {
const { path, filename } = await this.buildDownloadPath(options);
await fsModule.FileSystem.fetch(options.fileUrl, { path });
return {
downloadedPath: path,
file: {
name: filename