[NEW] Preview or download attachments (#3470)
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
3249c54d03
commit
c216544cc4
|
@ -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());
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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}/`;
|
|
@ -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*/}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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') });
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue