[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';
|
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());
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { 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*/}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
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 (
|
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)}>
|
||||||
<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>
|
</Touchable>
|
||||||
{/* @ts-ignore*/}
|
{/* @ts-ignore*/}
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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)
|
- 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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue