[NEW] Preview or download attachments (#3470)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Alex Junior 2021-11-16 12:59:58 -03:00 committed by GitHub
parent 3249c54d03
commit c216544cc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 18 deletions

View File

@ -1,5 +1,21 @@
import initStoryshots from '@storybook/addon-storyshots';
jest.mock('rn-fetch-blob', () => ({
fs: {
dirs: {
DocumentDir: '/data/com.rocket.chat/documents',
DownloadDir: '/data/com.rocket.chat/downloads'
},
exists: jest.fn(() => null)
},
fetch: jest.fn(() => null),
config: jest.fn(() => null)
}));
jest.mock('react-native-file-viewer', () => ({
open: jest.fn(() => null)
}));
jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());

View File

@ -65,6 +65,7 @@ export const themes: any = {
previewBackground: '#1F2329',
previewTintColor: '#ffffff',
backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7,
...mentions
},
dark: {
@ -112,6 +113,7 @@ export const themes: any = {
previewBackground: '#030b1b',
previewTintColor: '#ffffff',
backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions
},
black: {
@ -159,6 +161,7 @@ export const themes: any = {
previewBackground: '#000000',
previewTintColor: '#ffffff',
backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions
}
};

View File

@ -0,0 +1,4 @@
import RNFetchBlob from 'rn-fetch-blob';
export const DOCUMENTS_PATH = `${RNFetchBlob.fs.dirs.DocumentDir}/`;
export const DOWNLOAD_PATH = `${RNFetchBlob.fs.dirs.DownloadDir}/`;

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import moment from 'moment';
import { transparentize } from 'color2k';
@ -11,6 +11,9 @@ import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils';
import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({
button: {
@ -28,6 +31,9 @@ const styles = StyleSheet.create({
flexDirection: 'column',
padding: 15
},
backdrop: {
...StyleSheet.absoluteFillObject
},
authorContainer: {
flex: 1,
flexDirection: 'row',
@ -120,7 +126,7 @@ interface IMessageFields {
}
interface IMessageReply {
attachment: Partial<IMessageReplyAttachment>;
attachment: IMessageReplyAttachment;
timeFormat: string;
index: number;
theme: string;
@ -209,12 +215,14 @@ const Fields = React.memo(
const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => {
const [loading, setLoading] = useState(false);
if (!attachment) {
return null;
}
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = () => {
const onPress = async () => {
let url = attachment.title_link || attachment.author_link;
if (attachment.message_link) {
return jumpToMessage(attachment.message_link);
@ -223,10 +231,11 @@ const Reply = React.memo(
return;
}
if (attachment.type === 'file') {
if (!url.startsWith('http')) {
url = `${baseUrl}${url}`;
}
url = `${url}?rc_uid=${user.id}&rc_token=${user.token}`;
setLoading(true);
url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl);
await fileDownloadAndPreview(url, attachment);
setLoading(false);
return;
}
openLink(url, theme);
};
@ -254,12 +263,23 @@ const Reply = React.memo(
borderColor
}
]}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={loading}>
<View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
{loading ? (
<View style={[styles.backdrop]}>
<View
style={[
styles.backdrop,
{ backgroundColor: themes[theme].bannerBackground, opacity: themes[theme].attachmentLoadingOpacity }
]}></View>
<RCActivityIndicator theme={theme} />
</View>
) : null}
</View>
</Touchable>
{/* @ts-ignore*/}

View File

@ -1,15 +1,19 @@
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { StyleSheet } from 'react-native';
import { dequal } from 'dequal';
import Touchable from './Touchable';
import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors';
import MessageContext from './Context';
import { fileDownload } from '../../utils/fileDownload';
import EventEmitter from '../../utils/events';
import { LISTENER } from '../Toast';
import I18n from '../../i18n';
import RCActivityIndicator from '../ActivityIndicator';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,6 +31,9 @@ const styles = StyleSheet.create({
interface IMessageVideo {
file: {
title: string;
title_link: string;
type: string;
video_type: string;
video_url: string;
description: string;
@ -39,15 +46,34 @@ interface IMessageVideo {
const Video = React.memo(
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false);
if (!baseUrl) {
return null;
}
const onPress = () => {
const onPress = async () => {
if (isTypeSupported(file.video_type)) {
return showAttachment(file);
}
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
openLink(uri, theme);
if (!isIOS) {
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
await downloadVideo(uri);
return;
}
EventEmitter.emit(LISTENER, { message: I18n.t('Unsupported_format') });
};
const downloadVideo = async (uri: string) => {
setLoading(true);
const fileDownloaded = await fileDownload(uri, file);
setLoading(false);
if (fileDownloaded) {
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
return;
}
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') });
};
return (
@ -56,7 +82,11 @@ const Video = React.memo(
onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}>
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
{loading ? (
<RCActivityIndicator theme={theme} />
) : (
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
)}
</Touchable>
{/* @ts-ignore*/}
<Markdown

View File

@ -782,5 +782,8 @@
"No_canned_responses": "No canned responses",
"Send_email_confirmation": "Send email confirmation",
"sending_email_confirmation": "sending email confirmation",
"Enable_Message_Parser": "Enable Message Parser"
}
"Enable_Message_Parser": "Enable Message Parser",
"Unsupported_format": "Unsupported format",
"Downloaded_file": "Downloaded file",
"Error_Download_file": "Error while downloading file"
}

View File

@ -733,5 +733,8 @@
"Sharing": "Compartilhando",
"No_canned_responses": "Não há respostas predefinidas",
"Send_email_confirmation": "Enviar email de confirmação",
"sending_email_confirmation": "enviando email de confirmação"
}
"sending_email_confirmation": "enviando email de confirmação",
"Unsupported_format": "Formato não suportado",
"Downloaded_file": "Arquivo baixado",
"Error_Download_file": "Erro ao baixar o arquivo"
}

View File

@ -0,0 +1,59 @@
import RNFetchBlob, { FetchBlobResponse } from 'rn-fetch-blob';
import FileViewer from 'react-native-file-viewer';
import EventEmitter from '../events';
import { LISTENER } from '../../containers/Toast';
import I18n from '../../i18n';
import { DOCUMENTS_PATH, DOWNLOAD_PATH } from '../../constants/localPath';
interface IAttachment {
title: string;
title_link: string;
type: string;
description: string;
}
export const getLocalFilePathFromFile = (localPath: string, attachment: IAttachment): string => `${localPath}${attachment.title}`;
export const fileDownload = (url: string, attachment: IAttachment): Promise<FetchBlobResponse> => {
const path = getLocalFilePathFromFile(DOWNLOAD_PATH, attachment);
const options = {
path,
timeout: 10000,
indicator: true,
overwrite: true,
addAndroidDownloads: {
path,
notification: true,
useDownloadManager: true
}
};
return RNFetchBlob.config(options).fetch('GET', url);
};
export const fileDownloadAndPreview = async (url: string, attachment: IAttachment): Promise<void> => {
try {
const path = getLocalFilePathFromFile(DOCUMENTS_PATH, attachment);
const file = await RNFetchBlob.config({
timeout: 10000,
indicator: true,
path
}).fetch('GET', url);
FileViewer.open(file.data, {
showOpenWithDialog: true,
showAppsSuggestions: true
})
.then(res => res)
.catch(async () => {
const file = await fileDownload(url, attachment);
file
? EventEmitter.emit(LISTENER, { message: I18n.t('Downloaded_file') })
: EventEmitter.emit(LISTENER, { message: I18n.t('Error_Download_file') });
});
} catch (e) {
EventEmitter.emit(LISTENER, { message: I18n.t('Error_Download_file') });
}
};

View File

@ -532,6 +532,8 @@ PODS:
- Firebase/Crashlytics (~> 6.27.0)
- React
- RNFBApp
- RNFileViewer (2.1.4):
- React-Core
- RNGestureHandler (1.10.3):
- React-Core
- RNImageCropPicker (0.36.3):
@ -700,6 +702,7 @@ DEPENDENCIES:
- "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)"
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNLocalize (from `../node_modules/react-native-localize`)
@ -894,6 +897,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/app"
RNFBCrashlytics:
:path: "../node_modules/@react-native-firebase/crashlytics"
RNFileViewer:
:path: "../node_modules/react-native-file-viewer"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker:
@ -1028,6 +1033,7 @@ SPEC CHECKSUMS:
RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e
RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0
RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e
RNFileViewer: 83cc066ad795b1f986791d03b56fe0ee14b6a69f
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d
RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b

View File

@ -87,6 +87,7 @@
"react-native-document-picker": "5.2.0",
"react-native-easy-grid": "^0.2.2",
"react-native-easy-toast": "^1.2.0",
"react-native-file-viewer": "^2.1.4",
"react-native-gesture-handler": "^1.10.3",
"react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker",
"react-native-image-progress": "^1.1.1",

View File

@ -14254,6 +14254,11 @@ react-native-easy-toast@^1.2.0:
dependencies:
prop-types "^15.5.10"
react-native-file-viewer@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/react-native-file-viewer/-/react-native-file-viewer-2.1.4.tgz#987b2902f0f0ac87b42f3ac3d3037c8ae98f17a6"
integrity sha512-G3ko9lmqxT+lWhsDNy2K3Jes6xSMsUvlYwuwnRCNk2wC6hgYMeoeaiwDt8R3CdON781hB6Ej1eu3ir1QATtHXg==
react-native-flipper@^0.34.0:
version "0.34.0"
resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d"