* Open/Close Gallery transitions * Include postId in file info & disable opening the gallery in some cases * Add progress bar component * Transition to gallery based on platform * Improve getting the filename for non attachments * Patch RNFetchBlob TS definition * Add gallery types * fix unit tests for message and post attachment * add getLocalPath method * Add react-native-share dependency * Re-style gallery footer * Refactor Gallery screen * Double tap zoom in/out and translate * Make android activity "transparent" * Do not animate to height on dismissing gallery * Open gallery for file uploads * Fix borderRadius for gallery action button * Use progress bar for file uploads * Replace progress bar for file attachment document * Upgrade RNN to 7.1.0 to fix share elements transitions * Fix Gallery unit tests * translate down when popping screen on iOS * Swipe, Pan & Tap fixes * fix gallery footer avatar eslint * Fix gallery snapshot tests * Use CompassIcon in Gallery * Feedback from UX review * Fix gallery UI for other file types * Set other file type gallery button to 48pt
325 lines
8.7 KiB
JavaScript
325 lines
8.7 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {Platform} from 'react-native';
|
|
import RNFetchBlob from 'rn-fetch-blob';
|
|
import mimeDB from 'mime-db';
|
|
|
|
import {DeviceTypes} from '@constants';
|
|
|
|
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
|
const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i;
|
|
const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb
|
|
|
|
export const GENERAL_SUPPORTED_DOCS_FORMAT = [
|
|
'application/json',
|
|
'application/msword',
|
|
'application/pdf',
|
|
'application/rtf',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.ms-powerpoint',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'application/x-x509-ca-cert',
|
|
'application/xml',
|
|
'text/csv',
|
|
'text/plain',
|
|
];
|
|
|
|
const SUPPORTED_DOCS_FORMAT = Platform.select({
|
|
android: GENERAL_SUPPORTED_DOCS_FORMAT,
|
|
ios: [
|
|
...GENERAL_SUPPORTED_DOCS_FORMAT,
|
|
'application/vnd.apple.pages',
|
|
'application/vnd.apple.numbers',
|
|
'application/vnd.apple.keynote',
|
|
],
|
|
});
|
|
|
|
const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
|
ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'],
|
|
android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm'],
|
|
});
|
|
|
|
const types = {};
|
|
const extensions = {};
|
|
|
|
export function generateId() {
|
|
// Implementation taken from http://stackoverflow.com/a/2117523
|
|
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
|
|
|
id = id.replace(/[xy]/g, (c) => {
|
|
const r = Math.floor(Math.random() * 16);
|
|
|
|
let v;
|
|
if (c === 'x') {
|
|
v = r;
|
|
} else {
|
|
v = (r & 0x3) | 0x8;
|
|
}
|
|
|
|
return v.toString(16);
|
|
});
|
|
|
|
return 'uid' + id;
|
|
}
|
|
|
|
const vectorIconsDir = 'vectorIcons';
|
|
const dirsToExclude = ['Cache.db', 'WebKit', 'WebView', vectorIconsDir];
|
|
async function getDirectorySize(fileStats) {
|
|
if (fileStats?.length) {
|
|
const total = await fileStats.reduce(async (previousPromise, stat) => {
|
|
const value = await previousPromise;
|
|
|
|
const exclude = dirsToExclude.find((f) => stat.path.includes(f));
|
|
if (exclude) {
|
|
return value;
|
|
}
|
|
|
|
if (stat.type === 'directory') {
|
|
const stats = await RNFetchBlob.fs.lstat(stat.path);
|
|
const dirSize = await getDirectorySize(stats, 0);
|
|
return value + dirSize;
|
|
}
|
|
|
|
return value + parseInt(stat.size, 10);
|
|
}, Promise.resolve(0));
|
|
|
|
return total;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
export async function getFileCacheSize() {
|
|
const cacheStats = await RNFetchBlob.fs.lstat(RNFetchBlob.fs.dirs.CacheDir);
|
|
const size = await getDirectorySize(cacheStats);
|
|
|
|
return size;
|
|
}
|
|
|
|
export async function deleteFileCache() {
|
|
const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
|
const isCacheDir = await RNFetchBlob.fs.isDir(cacheDir);
|
|
if (isCacheDir) {
|
|
try {
|
|
if (Platform.OS === 'ios') {
|
|
await RNFetchBlob.fs.unlink(cacheDir);
|
|
await RNFetchBlob.fs.mkdir(cacheDir);
|
|
} else {
|
|
const cacheStats = await RNFetchBlob.fs.lstat(RNFetchBlob.fs.dirs.CacheDir);
|
|
if (cacheStats?.length) {
|
|
cacheStats.forEach((stat) => {
|
|
if (!stat.path.includes(vectorIconsDir)) {
|
|
RNFetchBlob.fs.unlink(stat.path);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function lookupMimeType(filename) {
|
|
if (!Object.keys(extensions).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
return types[ext] || 'application/octet-stream';
|
|
}
|
|
|
|
export function buildFileUploadData(file) {
|
|
const re = /heic/i;
|
|
const uri = file.uri;
|
|
let name = file.fileName || file.name || file.path || file.uri;
|
|
let mimeType = lookupMimeType(name.toLowerCase());
|
|
let extension = name.split('.').pop().replace('.', '');
|
|
|
|
if (re.test(extension)) {
|
|
extension = 'JPG';
|
|
name = name.replace(re, 'jpg');
|
|
mimeType = 'image/jpeg';
|
|
}
|
|
|
|
return {
|
|
uri,
|
|
name,
|
|
type: mimeType,
|
|
extension,
|
|
};
|
|
}
|
|
|
|
export const encodeHeaderURIStringToUTF8 = (string) => {
|
|
return encodeURIComponent(string) + '"; filename*="utf-8\'\'' + encodeURIComponent(string);
|
|
};
|
|
|
|
export const getAllowedServerMaxFileSize = (config) => {
|
|
return config && config.MaxFileSize ? parseInt(config.MaxFileSize, 10) : DEFAULT_SERVER_MAX_FILE_SIZE;
|
|
};
|
|
|
|
export const isGif = (file) => {
|
|
let mime = file?.mime_type || file?.type || '';
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return mime === 'image/gif';
|
|
};
|
|
|
|
export const isImage = (file) => (file?.has_preview_image || isGif(file) || file?.type?.startsWith('image/'));
|
|
|
|
export const isDocument = (file) => {
|
|
let mime = file?.mime_type || file?.type || '';
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return SUPPORTED_DOCS_FORMAT.includes(mime);
|
|
};
|
|
|
|
export const isVideo = (file) => {
|
|
let mime = file?.mime_type || file?.type || '';
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return SUPPORTED_VIDEO_FORMAT.includes(mime);
|
|
};
|
|
|
|
/**
|
|
* Get the default extension for a MIME type.
|
|
*
|
|
* @param {string} type
|
|
* @return {string}
|
|
*/
|
|
|
|
export function getExtensionFromMime(type) {
|
|
if (!Object.keys(extensions).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
if (!type || typeof type !== 'string') {
|
|
return undefined;
|
|
}
|
|
|
|
// TODO: use media-typer
|
|
const match = EXTRACT_TYPE_REGEXP.exec(type);
|
|
|
|
// get extensions
|
|
const exts = match && extensions[match[1].toLowerCase()];
|
|
|
|
if (!exts || !exts.length) {
|
|
return undefined;
|
|
}
|
|
|
|
return exts[0];
|
|
}
|
|
|
|
/**
|
|
* Populate the extensions and types maps.
|
|
* @private
|
|
*/
|
|
|
|
function populateMaps() {
|
|
// source preference (least -> most)
|
|
const preference = ['nginx', 'apache', undefined, 'iana'];
|
|
|
|
Object.keys(mimeDB).forEach((type) => {
|
|
const mime = mimeDB[type];
|
|
const exts = mime.extensions;
|
|
|
|
if (!exts || !exts.length) {
|
|
return;
|
|
}
|
|
|
|
extensions[type] = exts;
|
|
|
|
for (let i = 0; i < exts.length; i++) {
|
|
const extension = exts[i];
|
|
|
|
if (types[extension]) {
|
|
const from = preference.indexOf(mimeDB[types[extension]].source);
|
|
const to = preference.indexOf(mime.source);
|
|
|
|
if (types[extension] !== 'application/octet-stream' &&
|
|
(from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
types[extension] = type;
|
|
}
|
|
});
|
|
}
|
|
|
|
export const hashCode = (str) => {
|
|
let hash = 0;
|
|
let i;
|
|
let chr;
|
|
if (!str || str.length === 0) {
|
|
return hash;
|
|
}
|
|
|
|
for (i = 0; i < str.length; i++) {
|
|
chr = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + chr;
|
|
hash |= 0; // Convert to 32bit integer
|
|
}
|
|
return hash;
|
|
};
|
|
|
|
export function getLocalFilePathFromFile(dir, file) {
|
|
if (dir) {
|
|
if (file?.name) {
|
|
const [filename, extension] = file.name.split('.');
|
|
return `${dir}/${filename}-${hashCode(file.id)}.${extension}`;
|
|
} else if (file?.id && file?.extension) {
|
|
return `${dir}/${file.id}.${file.extension}`;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getLocalPath(file) {
|
|
if (file.localPath) {
|
|
return file.localPath;
|
|
} else if (isVideo(file)) {
|
|
return getLocalFilePathFromFile(DeviceTypes.VIDEOS_PATH, file);
|
|
} else if (isImage(file)) {
|
|
return getLocalFilePathFromFile(DeviceTypes.IMAGES_PATH, file);
|
|
}
|
|
|
|
return getLocalFilePathFromFile(DeviceTypes.DOCUMENTS_PATH, file);
|
|
}
|
|
|
|
export function getExtensionFromContentDisposition(contentDisposition) {
|
|
const match = CONTENT_DISPOSITION_REGEXP.exec(contentDisposition);
|
|
let extension = match && match[1];
|
|
if (extension) {
|
|
if (!Object.keys(types).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
extension = extension.toLowerCase();
|
|
if (types[extension]) {
|
|
return extension;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|