[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'; 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)); jest.mock('../app/lib/database', () => jest.fn(() => null));
global.Date.now = jest.fn(() => new Date('2019-10-10').getTime()); global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());

View File

@ -65,6 +65,7 @@ export const themes: any = {
previewBackground: '#1F2329', previewBackground: '#1F2329',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.3, backdropOpacity: 0.3,
attachmentLoadingOpacity: 0.7,
...mentions ...mentions
}, },
dark: { dark: {
@ -112,6 +113,7 @@ export const themes: any = {
previewBackground: '#030b1b', previewBackground: '#030b1b',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions ...mentions
}, },
black: { black: {
@ -159,6 +161,7 @@ export const themes: any = {
previewBackground: '#000000', previewBackground: '#000000',
previewTintColor: '#ffffff', previewTintColor: '#ffffff',
backdropOpacity: 0.9, backdropOpacity: 0.9,
attachmentLoadingOpacity: 0.3,
...mentions ...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 { StyleSheet, Text, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { transparentize } from 'color2k'; import { transparentize } from 'color2k';
@ -11,6 +11,9 @@ import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils';
import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -28,6 +31,9 @@ const styles = StyleSheet.create({
flexDirection: 'column', flexDirection: 'column',
padding: 15 padding: 15
}, },
backdrop: {
...StyleSheet.absoluteFillObject
},
authorContainer: { authorContainer: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
@ -120,7 +126,7 @@ interface IMessageFields {
} }
interface IMessageReply { interface IMessageReply {
attachment: Partial<IMessageReplyAttachment>; attachment: IMessageReplyAttachment;
timeFormat: string; timeFormat: string;
index: number; index: number;
theme: string; theme: string;
@ -209,12 +215,14 @@ const Fields = React.memo(
const Reply = React.memo( const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => { ({ attachment, timeFormat, index, getCustomEmoji, theme }: IMessageReply) => {
const [loading, setLoading] = useState(false);
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const onPress = () => { const onPress = async () => {
let url = attachment.title_link || attachment.author_link; let url = attachment.title_link || attachment.author_link;
if (attachment.message_link) { if (attachment.message_link) {
return jumpToMessage(attachment.message_link); return jumpToMessage(attachment.message_link);
@ -223,10 +231,11 @@ const Reply = React.memo(
return; return;
} }
if (attachment.type === 'file') { if (attachment.type === 'file') {
if (!url.startsWith('http')) { setLoading(true);
url = `${baseUrl}${url}`; url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl);
} await fileDownloadAndPreview(url, attachment);
url = `${url}?rc_uid=${user.id}&rc_token=${user.token}`; setLoading(false);
return;
} }
openLink(url, theme); openLink(url, theme);
}; };
@ -254,12 +263,23 @@ const Reply = React.memo(
borderColor borderColor
} }
]} ]}
background={Touchable.Ripple(themes[theme].bannerBackground)}> background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={loading}>
<View style={styles.attachmentContainer}> <View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> <Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
<UrlImage image={attachment.thumb_url} /> <UrlImage image={attachment.thumb_url} />
<Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
<Fields 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> </View>
</Touchable> </Touchable>
{/* @ts-ignore*/} {/* @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 { StyleSheet } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; 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 SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,6 +31,9 @@ const styles = StyleSheet.create({
interface IMessageVideo { interface IMessageVideo {
file: { file: {
title: string;
title_link: string;
type: string;
video_type: string; video_type: string;
video_url: string; video_url: string;
description: string; description: string;
@ -39,15 +46,34 @@ interface IMessageVideo {
const Video = React.memo( const Video = React.memo(
({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => { ({ file, showAttachment, getCustomEmoji, theme }: IMessageVideo) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const [loading, setLoading] = useState(false);
if (!baseUrl) { if (!baseUrl) {
return null; return null;
} }
const onPress = () => { const onPress = async () => {
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type)) {
return showAttachment(file); return showAttachment(file);
} }
if (!isIOS) {
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl); const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
openLink(uri, theme); 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 ( return (
@ -56,7 +82,11 @@ const Video = React.memo(
onPress={onPress} onPress={onPress}
style={[styles.button, { backgroundColor: themes[theme].videoBackground }]} style={[styles.button, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}> background={Touchable.Ripple(themes[theme].bannerBackground)}>
{loading ? (
<RCActivityIndicator theme={theme} />
) : (
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} /> <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
)}
</Touchable> </Touchable>
{/* @ts-ignore*/} {/* @ts-ignore*/}
<Markdown <Markdown

View File

@ -782,5 +782,8 @@
"No_canned_responses": "No canned responses", "No_canned_responses": "No canned responses",
"Send_email_confirmation": "Send email confirmation", "Send_email_confirmation": "Send email confirmation",
"sending_email_confirmation": "sending 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", "Sharing": "Compartilhando",
"No_canned_responses": "Não há respostas predefinidas", "No_canned_responses": "Não há respostas predefinidas",
"Send_email_confirmation": "Enviar email de confirmação", "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) - Firebase/Crashlytics (~> 6.27.0)
- React - React
- RNFBApp - RNFBApp
- RNFileViewer (2.1.4):
- React-Core
- RNGestureHandler (1.10.3): - RNGestureHandler (1.10.3):
- React-Core - React-Core
- RNImageCropPicker (0.36.3): - RNImageCropPicker (0.36.3):
@ -700,6 +702,7 @@ DEPENDENCIES:
- "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)"
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
- RNLocalize (from `../node_modules/react-native-localize`) - RNLocalize (from `../node_modules/react-native-localize`)
@ -894,6 +897,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-firebase/app" :path: "../node_modules/@react-native-firebase/app"
RNFBCrashlytics: RNFBCrashlytics:
:path: "../node_modules/@react-native-firebase/crashlytics" :path: "../node_modules/@react-native-firebase/crashlytics"
RNFileViewer:
:path: "../node_modules/react-native-file-viewer"
RNGestureHandler: RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler" :path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker: RNImageCropPicker:
@ -1028,6 +1033,7 @@ SPEC CHECKSUMS:
RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e RNFBAnalytics: dae6d7b280ba61c96e1bbdd34aca3154388f025e
RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0 RNFBApp: 6fd8a7e757135d4168bf033a8812c241af7363a0
RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e RNFBCrashlytics: 88de72c2476b5868a892d9523b89b86c527c540e
RNFileViewer: 83cc066ad795b1f986791d03b56fe0ee14b6a69f
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d RNImageCropPicker: 97289cd94fb01ab79db4e5c92938be4d0d63415d
RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b RNLocalize: 82a569022724d35461e2dc5b5d015a13c3ca995b

View File

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

View File

@ -14254,6 +14254,11 @@ react-native-easy-toast@^1.2.0:
dependencies: dependencies:
prop-types "^15.5.10" 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: react-native-flipper@^0.34.0:
version "0.34.0" version "0.34.0"
resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d" resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d"