Add Document and Video previews (#1170)
* Video file previewer * Document preview support * Fix tests * Feedback review * feedback review 2 * feedback review 3 * Fix video_controls copyright * Fix mime undefined
99
NOTICE.txt
|
|
@ -1412,3 +1412,102 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-video
|
||||
|
||||
This product contains 'react-native-video', A <Video> component for react-native.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/react-native-video
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Brent Vatne, Baris Sencan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-slider
|
||||
|
||||
This product contains 'react-native-slider', It is a drop-in replacement for React Native Slider by Jean Regisser.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/jeanregisser/react-native-slider
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present Jean Regisser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-doc-viewer
|
||||
|
||||
This product contains 'react-native-doc-viewer', A React Native bridge module: Document Viewer for files (pdf, png, jpg, xls, doc, ppt, xlsx, docx, pptx etc.)
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/philipphecht/react-native-doc-viewer
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Phil Pike
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-doc-viewer')
|
||||
compile project(':react-native-video')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:25.0.1"
|
||||
compile 'com.android.support:percent:25.3.1'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import android.content.Context;
|
|||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.reactlibrary.RNReactNativeDocViewerPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.horcrux.svg.SvgPackage;
|
||||
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
|
||||
import io.sentry.RNSentryPackage;
|
||||
|
|
@ -66,7 +68,9 @@ public class MainApplication extends NavigationApplication implements INotificat
|
|||
new MattermostPackage(this),
|
||||
new RNSentryPackage(this),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube()
|
||||
new ReactNativeYouTube(),
|
||||
new ReactVideoPackage(),
|
||||
new RNReactNativeDocViewerPackage()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
rootProject.name = 'Mattermost'
|
||||
include ':react-native-doc-viewer'
|
||||
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||
include ':react-native-youtube'
|
||||
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
|
||||
include ':react-native-sentry'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from './file_attachment_document';
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
import FileAttachmentImage from './file_attachment_image';
|
||||
|
||||
|
|
@ -28,7 +29,11 @@ export default class FileAttachment extends PureComponent {
|
|||
static defaultProps = {
|
||||
onInfoPress: () => true,
|
||||
onPreviewPress: () => true
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = () => {
|
||||
this.props.onPreviewPress(this.props.file);
|
||||
};
|
||||
|
||||
renderFileInfo() {
|
||||
const {file, theme} = this.props;
|
||||
|
|
@ -55,40 +60,50 @@ export default class FileAttachment extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
handlePreviewPress = () => {
|
||||
this.props.onPreviewPress(this.props.file);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {file, theme} = this.props;
|
||||
const {file, onInfoPress, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let mime = file.mime_type;
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
}
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if (file.has_preview_image || file.loading || file.mime_type === 'image/gif') {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentDocument
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.fileWrapper}>
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
{fileAttachmentComponent}
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onInfoPress}
|
||||
onPress={onInfoPress}
|
||||
style={style.fileInfoContainer}
|
||||
>
|
||||
{this.renderFileInfo()}
|
||||
|
|
@ -133,6 +148,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
marginTop: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
circularProgress: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
circularProgressContent: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
318
app/components/file_attachment_list/file_attachment_document.js
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import OpenFile from 'react-native-doc-viewer';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
|
||||
const {DOCUMENTS_PATH} = DeviceTypes;
|
||||
export const SUPPORTED_DOCS_FORMAT = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'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/xml',
|
||||
'text/csv'
|
||||
];
|
||||
|
||||
export default class FileAttachmentDocument extends PureComponent {
|
||||
static propTypes = {
|
||||
iconHeight: PropTypes.number,
|
||||
iconWidth: PropTypes.number,
|
||||
file: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: 65,
|
||||
iconWidth: 65,
|
||||
wrapperHeight: 100,
|
||||
wrapperWidth: 100
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
state = {
|
||||
didCancel: false,
|
||||
downloading: false,
|
||||
progress: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
cancelDownload = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({didCancel: true});
|
||||
}
|
||||
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
downloadAndPreviewFile = async (file) => {
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
|
||||
this.setState({didCancel: false});
|
||||
|
||||
try {
|
||||
const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH);
|
||||
if (!isDir) {
|
||||
try {
|
||||
await RNFetchBlob.fs.mkdir(DOCUMENTS_PATH);
|
||||
} catch (error) {
|
||||
this.showDownloadFailedAlert();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
session: file.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
path
|
||||
};
|
||||
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
if (exist) {
|
||||
this.openDocument(file, 0);
|
||||
} else {
|
||||
this.setState({downloading: true});
|
||||
this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(file.id)).
|
||||
progress((received, total) => {
|
||||
const progress = (received / total) * 100;
|
||||
if (this.mounted) {
|
||||
this.setState({progress});
|
||||
}
|
||||
});
|
||||
|
||||
await this.downloadTask;
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 100
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
this.openDocument(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
if (this.mounted) {
|
||||
this.setState({downloading: false, progress: 0});
|
||||
|
||||
if (error.message !== 'cancelled') {
|
||||
const {intl} = this.context;
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_title',
|
||||
defaultMessage: 'Download failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_description',
|
||||
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = async () => {
|
||||
const {file} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
|
||||
if (downloading && progress < 100) {
|
||||
this.cancelDownload();
|
||||
} else if (downloading) {
|
||||
this.resetViewState();
|
||||
} else {
|
||||
this.downloadAndPreviewFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
openDocument = (file, delay = 2000) => {
|
||||
// The animation for the progress circle takes about 2 seconds to finish
|
||||
// therefore we are delaying the opening of the document to have the UI
|
||||
// shown nicely and smooth
|
||||
setTimeout(() => {
|
||||
if (!this.state.didCancel && this.mounted) {
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
OpenFile.openDoc([{
|
||||
url: `${prefix}${path}`,
|
||||
fileName: file.name,
|
||||
fileType: file.extension
|
||||
}], (error) => {
|
||||
if (error) {
|
||||
const {intl} = this.context;
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.document_preview.failed_title',
|
||||
defaultMessage: 'Open Document failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.document_preview.failed_description',
|
||||
defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n'
|
||||
}, {
|
||||
fileType: file.extension.toUpperCase()
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
}
|
||||
this.setState({downloading: false, progress: 0});
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
resetViewState = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 0,
|
||||
didCancel: true
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
setTimeout(() => this.setState({downloading: false}), 2000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderProgress = () => {
|
||||
const {iconHeight, iconWidth, file, theme, wrapperWidth} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
theme={theme}
|
||||
wrapperHeight={iconHeight}
|
||||
wrapperWidth={iconWidth}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
showDownloadFailedAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_title',
|
||||
defaultMessage: 'Download failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_description',
|
||||
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
}),
|
||||
onPress: () => this.downloadDidCancel()
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<AnimatedCircularProgress
|
||||
size={wrapperHeight}
|
||||
fill={progress}
|
||||
width={4}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
style={style.circularProgress}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</AnimatedCircularProgress>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
wrapperHeight={wrapperHeight}
|
||||
wrapperWidth={wrapperWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
circularProgress: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
circularProgressContent: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0
|
||||
}
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
View,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
|
|
@ -57,7 +58,7 @@ export default class FileAttachmentList extends Component {
|
|||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
screenBackgroundColor: 'black',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ function makeMapStateToProps() {
|
|||
const getFilesForPost = makeGetFilesForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
fetchCache: state.views.fetchCache,
|
||||
files: getFilesForPost(state, ownProps.postId),
|
||||
theme: getTheme(state),
|
||||
|
|
|
|||
|
|
@ -76,9 +76,16 @@ export default class PostList extends PureComponent {
|
|||
scrollList = () => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (this.props.postIds.length && this.newMessagesIndex !== -1) {
|
||||
this.refs.list.scrollToIndex({index: this.newMessagesIndex, viewPosition: 1, viewOffset: -10, animated: true});
|
||||
if (this.refs.list) {
|
||||
this.refs.list.scrollToIndex({
|
||||
index: this.newMessagesIndex,
|
||||
viewPosition: 1,
|
||||
viewOffset: -10,
|
||||
animated: true
|
||||
});
|
||||
}
|
||||
this.newMessagesIndex = -1;
|
||||
} else {
|
||||
} else if (this.refs.list) {
|
||||
this.refs.list.scrollToOffset({y: 0, animated: false});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
325
app/components/video_controls.js
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
// Copyright (c) 2016-2017 Charles.
|
||||
// Modified work: Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
AppState,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Slider from 'react-native-slider';
|
||||
|
||||
import fullscreenImage from 'assets/images/video_player/fullscreen.png';
|
||||
import pauseImage from 'assets/images/video_player/pause.png';
|
||||
import playImage from 'assets/images/video_player/play.png';
|
||||
import replayImage from 'assets/images/video_player/replay.png';
|
||||
|
||||
export const PLAYER_STATE = {
|
||||
PLAYING: 0,
|
||||
PAUSED: 1,
|
||||
ENDED: 2
|
||||
};
|
||||
|
||||
export default class VideoControls extends PureComponent {
|
||||
static propTypes = {
|
||||
duration: PropTypes.number,
|
||||
isLoading: PropTypes.bool,
|
||||
isFullScreen: PropTypes.bool,
|
||||
mainColor: PropTypes.string,
|
||||
onFullScreen: PropTypes.func,
|
||||
onPaused: PropTypes.func,
|
||||
onReplay: PropTypes.func,
|
||||
onSeek: PropTypes.func,
|
||||
onSeeking: PropTypes.func,
|
||||
playerState: PropTypes.number,
|
||||
progress: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
duration: 0,
|
||||
mainColor: 'rgba(12, 83, 175, 0.9)'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
opacity: new Animated.Value(1),
|
||||
isVisible: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.playerState === PLAYER_STATE.ENDED || nextProps.isLoading ||
|
||||
(nextProps.playerState === PLAYER_STATE.PAUSED && this.props.playerState === PLAYER_STATE.PLAYING)) {
|
||||
this.fadeInControls(false);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
}
|
||||
|
||||
cancelAnimation = () => {
|
||||
this.state.opacity.stopAnimation(() => {
|
||||
this.setState({isVisible: true});
|
||||
});
|
||||
};
|
||||
|
||||
fadeInControls = (loop = true) => {
|
||||
this.setState({isVisible: true});
|
||||
Animated.timing(this.state.opacity, {toValue: 1, duration: 250, delay: 0}).start(() => {
|
||||
if (loop) {
|
||||
this.fadeOutControls(2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fadeOutControls = (delay = 0) => {
|
||||
Animated.timing(this.state.opacity, {toValue: 0, duration: 250, delay}).start((result) => {
|
||||
if (result.finished) {
|
||||
this.setState({isVisible: false});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getPlayerStateIcon = (playerState) => {
|
||||
switch (playerState) {
|
||||
case PLAYER_STATE.PAUSED:
|
||||
return playImage;
|
||||
case PLAYER_STATE.PLAYING:
|
||||
return pauseImage;
|
||||
case PLAYER_STATE.ENDED:
|
||||
return replayImage;
|
||||
}
|
||||
|
||||
return playImage;
|
||||
};
|
||||
|
||||
handleAppStateChange = (nextAppState) => {
|
||||
if (nextAppState !== 'active' && this.props.playerState === PLAYER_STATE.PLAYING) {
|
||||
this.onPause();
|
||||
}
|
||||
};
|
||||
|
||||
humanizeVideoDuration = (seconds) => {
|
||||
const [begin, end] = seconds >= 3600 ? [11, 8] : [14, 5];
|
||||
const date = new Date(null);
|
||||
date.setSeconds(seconds);
|
||||
return date.toISOString().substr(begin, end);
|
||||
};
|
||||
|
||||
onPause = () => {
|
||||
if (this.props.playerState === PLAYER_STATE.PLAYING) {
|
||||
this.cancelAnimation();
|
||||
}
|
||||
if (this.props.playerState === PLAYER_STATE.PAUSED) {
|
||||
this.fadeOutControls(250);
|
||||
}
|
||||
this.props.onPaused();
|
||||
};
|
||||
|
||||
onReplay = () => {
|
||||
this.fadeOutControls(500);
|
||||
this.props.onReplay();
|
||||
};
|
||||
|
||||
renderControls() {
|
||||
if (!this.state.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.controlsRow}/>
|
||||
<View style={[styles.controlsRow]}>
|
||||
{
|
||||
this.props.isLoading ? this.setLoadingView() : this.setPlayerControls(this.props.playerState)
|
||||
}
|
||||
</View>
|
||||
<View style={[styles.controlsRow, styles.progressContainer]}>
|
||||
<View style={styles.progressColumnContainer}>
|
||||
<View style={[styles.timerLabelsContainer]}>
|
||||
<Text style={styles.timerLabel}>
|
||||
{this.humanizeVideoDuration(this.props.progress)}
|
||||
</Text>
|
||||
<Text style={styles.timerLabel}>
|
||||
{this.humanizeVideoDuration(this.props.duration)}
|
||||
</Text>
|
||||
</View>
|
||||
<Slider
|
||||
style={styles.progressSlider}
|
||||
onSlidingComplete={this.seekVideo}
|
||||
onSlidingStart={this.seekStart}
|
||||
maximumValue={Math.floor(this.props.duration)}
|
||||
value={Math.floor(this.props.progress)}
|
||||
trackStyle={styles.track}
|
||||
thumbStyle={[styles.thumb, {borderColor: this.props.mainColor}]}
|
||||
minimumTrackTintColor={this.props.mainColor}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.fullScreenContainer}
|
||||
onPress={this.props.onFullScreen}
|
||||
>
|
||||
<Image source={fullscreenImage}/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
seekStart = () => {
|
||||
if (this.props.onSeeking) {
|
||||
this.props.onSeeking(false);
|
||||
}
|
||||
};
|
||||
|
||||
seekVideo = (value) => {
|
||||
this.props.onSeek(value);
|
||||
if (this.props.onSeeking) {
|
||||
this.props.onSeeking(true);
|
||||
}
|
||||
};
|
||||
|
||||
setPlayerControls = (playerState) => {
|
||||
const icon = this.getPlayerStateIcon(playerState);
|
||||
const pressAction = playerState === PLAYER_STATE.ENDED ? this.onReplay : this.onPause;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.playButton, {backgroundColor: this.props.mainColor}]}
|
||||
onPress={pressAction}
|
||||
>
|
||||
<Image
|
||||
source={icon}
|
||||
style={styles.playIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
setLoadingView = () => {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
size='large'
|
||||
color='#FFF'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
toggleControls = () => {
|
||||
this.state.opacity.stopAnimation(
|
||||
(value) => {
|
||||
this.setState({isVisible: Boolean(value)});
|
||||
if (value) {
|
||||
this.fadeOutControls();
|
||||
} else {
|
||||
this.fadeInControls(this.props.playerState === PLAYER_STATE.PLAYING);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={this.toggleControls}>
|
||||
<Animated.View style={[styles.container, {opacity: this.state.opacity}]}>
|
||||
{this.renderControls()}
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 13,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(45, 59, 62, 0.4)',
|
||||
justifyContent: 'space-between',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
},
|
||||
controlsRow: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
timeRow: {
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
playButton: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 3,
|
||||
borderWidth: 1.5,
|
||||
borderColor: 'rgba(255,255,255,0.5)'
|
||||
},
|
||||
playIcon: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
resizeMode: 'contain'
|
||||
},
|
||||
replayIcon: {
|
||||
width: 25,
|
||||
height: 20,
|
||||
resizeMode: 'stretch'
|
||||
},
|
||||
progressContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: -25
|
||||
},
|
||||
progressColumnContainer: {
|
||||
flex: 1
|
||||
},
|
||||
fullScreenContainer: {
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 20
|
||||
},
|
||||
progressSlider: {
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
timerLabelsContainer: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: -7
|
||||
},
|
||||
timerLabel: {
|
||||
fontSize: 12,
|
||||
color: 'white'
|
||||
},
|
||||
track: {
|
||||
height: 5,
|
||||
borderRadius: 1
|
||||
},
|
||||
thumb: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 50,
|
||||
backgroundColor: 'white',
|
||||
borderWidth: 3
|
||||
}
|
||||
});
|
||||
|
|
@ -2,11 +2,18 @@
|
|||
// See License.txt for license information.
|
||||
|
||||
import keyMirror from 'mattermost-redux/utils/key_mirror';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
|
||||
export default keyMirror({
|
||||
const deviceTypes = keyMirror({
|
||||
CONNECTION_CHANGED: null,
|
||||
DEVICE_DIMENSIONS_CHANGED: null,
|
||||
DEVICE_TYPE_CHANGED: null,
|
||||
DEVICE_ORIENTATION_CHANGED: null,
|
||||
STATUSBAR_HEIGHT_CHANGED: null
|
||||
});
|
||||
|
||||
export default {
|
||||
...deviceTypes,
|
||||
DOCUMENTS_PATH: `${RNFetchBlob.fs.dirs.CacheDir}/Documents`,
|
||||
VIDEOS_PATH: `${RNFetchBlob.fs.dirs.CacheDir}/Videos`
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import PushNotifications from 'app/push_notifications';
|
|||
import {registerScreens} from 'app/screens';
|
||||
import configureStore from 'app/store';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {deleteFileCache} from 'app/utils/file';
|
||||
import {
|
||||
captureException,
|
||||
initializeSentry,
|
||||
|
|
@ -358,6 +359,7 @@ export default class Mattermost {
|
|||
|
||||
handleReset = () => {
|
||||
this.appStarted = false;
|
||||
deleteFileCache();
|
||||
this.resetBadgeAndVersion();
|
||||
this.startApp('fade');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,17 +3,11 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
CameraRoll,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {Alert, Animated, CameraRoll, InteractionManager, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
|
|
@ -29,13 +23,24 @@ export default class Downloader extends PureComponent {
|
|||
file: PropTypes.object.isRequired,
|
||||
onDownloadCancel: PropTypes.func,
|
||||
onDownloadSuccess: PropTypes.func,
|
||||
show: PropTypes.bool
|
||||
prompt: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
downloadPath: PropTypes.string,
|
||||
saveToCameraRoll: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancelPress: emptyFunction,
|
||||
onDownloadCancel: emptyFunction,
|
||||
onDownloadSuccess: emptyFunction,
|
||||
show: false
|
||||
prompt: false,
|
||||
show: false,
|
||||
force: false,
|
||||
saveToCameraRoll: true
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
@ -43,10 +48,27 @@ export default class Downloader extends PureComponent {
|
|||
|
||||
this.state = {
|
||||
downloaderTop: new Animated.Value(props.deviceHeight),
|
||||
progress: 0
|
||||
progress: 0,
|
||||
started: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
if (this.props.show) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.toggleDownloader(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
this.toggleDownloader();
|
||||
|
|
@ -61,6 +83,29 @@ export default class Downloader extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
downloadDidCancel = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({progress: 0, started: false, didCancel: false});
|
||||
}
|
||||
this.props.onDownloadCancel();
|
||||
};
|
||||
|
||||
handleCancelDownload = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
didCancel: true,
|
||||
progress: 0,
|
||||
started: false
|
||||
});
|
||||
}
|
||||
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel(() => {
|
||||
this.props.onDownloadCancel();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
recenterDownloader = (props) => {
|
||||
const {deviceHeight, show} = props;
|
||||
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
|
||||
|
|
@ -75,83 +120,9 @@ export default class Downloader extends PureComponent {
|
|||
]).start();
|
||||
};
|
||||
|
||||
toggleDownloader = (show = true) => {
|
||||
const {deviceHeight} = this.props;
|
||||
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
|
||||
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5
|
||||
}).start(() => {
|
||||
if (show) {
|
||||
this.startDownload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
startDownload = async () => {
|
||||
try {
|
||||
const {file} = this.props;
|
||||
const imageUrl = Client4.getFileUrl(file.id);
|
||||
|
||||
this.downloadTask = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt: file.extension
|
||||
}).fetch('GET', imageUrl).progress((received, total) => {
|
||||
const progress = (received / total) * 100;
|
||||
this.setState({
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
const res = await this.downloadTask;
|
||||
const path = res.path();
|
||||
const newPath = await CameraRoll.saveToCameraRoll(path, 'photo');
|
||||
|
||||
this.setState({
|
||||
progress: 100
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// handles the case of a late cancellation by the user
|
||||
// and ensures that we remove the file if they did cancel late.
|
||||
if (this.state.didCancel) {
|
||||
// TODO: There's issue with deleting files from the cameraRoll on iOS here https://github.com/wkh237/react-native-fetch-blob/issues/479
|
||||
// This only occurs if the user cancels when the download is around 80% or more
|
||||
await RNFetchBlob.fs.unlink(newPath);
|
||||
this.props.onDownloadCancel();
|
||||
} else {
|
||||
this.props.onDownloadSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
// ensure the downloader at least closes if a error
|
||||
this.props.onDownloadCancel();
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
res.flush(); // remove the temp file
|
||||
this.downloadTask = null;
|
||||
} catch (error) {
|
||||
// cancellation throws so we need to catch
|
||||
}
|
||||
}
|
||||
|
||||
handleCancelDownload = () => {
|
||||
this.setState({
|
||||
didCancel: true
|
||||
});
|
||||
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel(() => {
|
||||
this.props.onDownloadCancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const {saveToCameraRoll} = this.props;
|
||||
const {isVideo} = this.state;
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
let component;
|
||||
|
|
@ -169,6 +140,7 @@ export default class Downloader extends PureComponent {
|
|||
<Text style={styles.progressText}>
|
||||
{`${fill.toFixed(0)}%`}
|
||||
</Text>
|
||||
{!isVideo &&
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={this.handleCancelDownload}
|
||||
|
|
@ -179,42 +151,236 @@ export default class Downloader extends PureComponent {
|
|||
style={styles.cancelText}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let savedComponent;
|
||||
if (realFill < 100 || this.state.didCancel) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.downloading'
|
||||
defaultMessage='Downloading...'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else if (saveToCameraRoll && isVideo) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.video_saved'
|
||||
defaultMessage='Video Saved'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else if (saveToCameraRoll) {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.image_saved'
|
||||
defaultMessage='Image Saved'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
savedComponent = (
|
||||
<FormattedText
|
||||
id='mobile.downloader.complete'
|
||||
defaultMessage='Download complete'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.progressContent}>
|
||||
{component}
|
||||
<View style={styles.bottomContent}>
|
||||
{(realFill < 100 || this.state.didCancel) ?
|
||||
<FormattedText
|
||||
id='mobile.downloader.downloading'
|
||||
defaultMessage='Downloading...'
|
||||
style={styles.bottomText}
|
||||
/> :
|
||||
<FormattedText
|
||||
id='mobile.downloader.success'
|
||||
defaultMessage='Image Saved'
|
||||
style={styles.bottomText}
|
||||
/>
|
||||
}
|
||||
{savedComponent}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderStartDownload = () => {
|
||||
return (
|
||||
<View style={styles.progressContent}>
|
||||
<TouchableOpacity onPress={this.startDownload}>
|
||||
<View style={styles.manualDownloadContainer}>
|
||||
<Icon
|
||||
name='md-download'
|
||||
size={48}
|
||||
color='white'
|
||||
/>
|
||||
<View style={styles.downloadTextContainer}>
|
||||
<FormattedText
|
||||
id='file_attachment.download'
|
||||
defaultMessage='Download'
|
||||
style={styles.cancelText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
saveVideo = (videoPath) => {
|
||||
const {deviceHeight} = this.props;
|
||||
const top = (deviceHeight / 2) - 100;
|
||||
this.setState({progress: 100, started: true, force: true, isVideo: true});
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5
|
||||
}).start(async () => {
|
||||
await CameraRoll.saveToCameraRoll(videoPath, 'video');
|
||||
this.props.onDownloadSuccess();
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.setState({force: false, isVideo: false});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showDownloadFailedAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_title',
|
||||
defaultMessage: 'Download failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_description',
|
||||
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
}),
|
||||
onPress: () => this.downloadDidCancel()
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
startDownload = async () => {
|
||||
const {file, downloadPath, prompt, saveToCameraRoll} = this.props;
|
||||
|
||||
try {
|
||||
if (this.state.didCancel) {
|
||||
this.setState({didCancel: false});
|
||||
}
|
||||
|
||||
const imageUrl = Client4.getFileUrl(file.id);
|
||||
const options = {
|
||||
session: file.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true
|
||||
};
|
||||
|
||||
if (downloadPath && prompt) {
|
||||
const isDir = await RNFetchBlob.fs.isDir(downloadPath);
|
||||
if (!isDir) {
|
||||
try {
|
||||
await RNFetchBlob.fs.mkdir(downloadPath);
|
||||
} catch (error) {
|
||||
this.showDownloadFailedAlert();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
options.path = `${downloadPath}/${file.id}.${file.extension}`;
|
||||
} else {
|
||||
options.fileCache = true;
|
||||
options.appendExt = file.extension;
|
||||
}
|
||||
|
||||
this.downloadTask = RNFetchBlob.config(options).fetch('GET', imageUrl).
|
||||
progress((received, total) => {
|
||||
const progress = (received / total) * 100;
|
||||
if (this.mounted) {
|
||||
this.setState({progress, started: true});
|
||||
}
|
||||
});
|
||||
|
||||
const res = await this.downloadTask;
|
||||
let path = res.path();
|
||||
|
||||
if (saveToCameraRoll) {
|
||||
path = await CameraRoll.saveToCameraRoll(path, 'photo');
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 100
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
setTimeout(async () => {
|
||||
if (this.state.didCancel) {
|
||||
try {
|
||||
await RNFetchBlob.fs.unlink(path);
|
||||
} finally {
|
||||
this.props.onDownloadCancel();
|
||||
}
|
||||
} else {
|
||||
this.props.onDownloadSuccess();
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
if (saveToCameraRoll) {
|
||||
res.flush(); // remove the temp file
|
||||
}
|
||||
this.downloadTask = null;
|
||||
} catch (error) {
|
||||
// cancellation throws so we need to catch
|
||||
if (downloadPath) {
|
||||
RNFetchBlob.fs.unlink(`${downloadPath}/${file.id}.${file.extension}`);
|
||||
}
|
||||
if (error.message !== 'cancelled' && this.mounted) {
|
||||
this.showDownloadFailedAlert();
|
||||
} else {
|
||||
this.downloadDidCancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleDownloader = (show = true) => {
|
||||
const {deviceHeight, prompt} = this.props;
|
||||
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
|
||||
|
||||
Animated.spring(this.state.downloaderTop, {
|
||||
toValue: top,
|
||||
tension: 8,
|
||||
friction: 5
|
||||
}).start(() => {
|
||||
if (show && !prompt) {
|
||||
this.startDownload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {show} = this.props;
|
||||
if (!show) {
|
||||
const {show, downloadPath} = this.props;
|
||||
if (!show && !this.state.force) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {didCancel, progress} = this.state;
|
||||
const {didCancel, progress, started} = this.state;
|
||||
|
||||
const trueProgress = didCancel ? 0 : progress;
|
||||
|
||||
const containerHeight = show ? '100%' : 0;
|
||||
|
||||
let component;
|
||||
if (downloadPath && !started) {
|
||||
component = this.renderStartDownload;
|
||||
} else {
|
||||
component = this.renderProgress;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, {height: containerHeight}]}>
|
||||
<AnimatedView style={[styles.downloader, {top: this.state.downloaderTop}]}>
|
||||
|
|
@ -228,7 +394,7 @@ export default class Downloader extends PureComponent {
|
|||
rotation={0}
|
||||
style={styles.progressCircle}
|
||||
>
|
||||
{this.renderProgress}
|
||||
{component}
|
||||
</AnimatedCircularProgress>
|
||||
</View>
|
||||
</AnimatedView>
|
||||
|
|
@ -308,5 +474,12 @@ const styles = StyleSheet.create({
|
|||
progressText: {
|
||||
color: 'white',
|
||||
fontSize: 18
|
||||
},
|
||||
manualDownloadContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
downloadTextContainer: {
|
||||
marginTop: 5
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,19 +16,24 @@ import {
|
|||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from 'app/components/file_attachment_list/file_attachment_document';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import Downloader from './downloader';
|
||||
import Previewer from './previewer';
|
||||
import VideoPreview from './video_preview';
|
||||
|
||||
const {VIDEOS_PATH} = DeviceTypes;
|
||||
const {View: AnimatedView} = Animated;
|
||||
const DRAG_VERTICAL_THRESHOLD_START = 25; // When do we want to start capturing the drag
|
||||
const DRAG_VERTICAL_THRESHOLD_END = 100; // When do we want to navigate back
|
||||
|
|
@ -38,6 +43,10 @@ const STATUSBAR_HEIGHT = Platform.select({
|
|||
ios: 0,
|
||||
android: 20
|
||||
});
|
||||
const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
||||
ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'],
|
||||
android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm']
|
||||
});
|
||||
|
||||
export default class ImagePreview extends PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -57,7 +66,7 @@ export default class ImagePreview extends PureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
@ -104,11 +113,9 @@ export default class ImagePreview extends PureComponent {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.deviceWidth !== nextProps.deviceWidth) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (this.scrollView) {
|
||||
this.scrollView.scrollTo({x: (this.state.currentFile * nextProps.deviceWidth), animated: false});
|
||||
}
|
||||
});
|
||||
if (this.scrollView) {
|
||||
this.scrollView.scrollTo({x: (this.state.currentFile * nextProps.deviceWidth), animated: true});
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextProps.files.length) {
|
||||
|
|
@ -122,10 +129,72 @@ export default class ImagePreview extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
attachScrollView = (c) => {
|
||||
this.scrollView = c;
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.props.navigator.dismissModal({animationType: 'none'});
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.showFileInfo) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
hideDownloader = (hideFileInfo = true) => {
|
||||
this.setState({showDownloader: false});
|
||||
if (hideFileInfo) {
|
||||
this.setHeaderAndFileInfoVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
handleImageDoubleTap = (x, y) => {
|
||||
this.zoomableImages[this.state.currentFile].toggleZoom(x, y);
|
||||
};
|
||||
|
||||
handleImageTap = () => {
|
||||
this.hideDownloader(false);
|
||||
this.setHeaderAndFileInfoVisible(!this.state.showFileInfo);
|
||||
};
|
||||
|
||||
handleScroll = (event) => {
|
||||
const offset = event.nativeEvent.contentOffset.x / this.props.deviceWidth;
|
||||
const wholeNumber = Number((offset).toFixed(0));
|
||||
|
||||
if (Math.abs(offset - wholeNumber) < 0.01) {
|
||||
this.setState({
|
||||
currentFile: wholeNumber,
|
||||
pagingEnabled: true,
|
||||
shouldShrinkImages: false
|
||||
});
|
||||
} else if (!this.state.shouldShrinkImages && !this.state.isZooming) {
|
||||
this.setState({
|
||||
shouldShrinkImages: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollStopped = () => {
|
||||
EventEmitter.emit('stop-video-playback');
|
||||
};
|
||||
|
||||
handleVideoSeek = (seeking) => {
|
||||
this.setState({
|
||||
isZooming: !seeking
|
||||
});
|
||||
};
|
||||
|
||||
imageIsZooming = (zooming) => {
|
||||
if (zooming !== this.state.isZooming) {
|
||||
this.setHeaderAndFileInfoVisible(!zooming);
|
||||
this.setState({
|
||||
isZooming: zooming
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mainViewMoveShouldSetPanResponderCapture = (evt, gestureState) => {
|
||||
if (gestureState.numberActiveTouches === 2 || this.state.isZooming) {
|
||||
return false;
|
||||
|
|
@ -152,21 +221,14 @@ export default class ImagePreview extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.showFileInfo) {
|
||||
this.close();
|
||||
saveVideo = () => {
|
||||
const file = this.state.files[this.state.currentFile];
|
||||
if (this.refs.downloader) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
this.refs.downloader.saveVideo(`${VIDEOS_PATH}/${file.id}.${file.extension}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleImageTap = () => {
|
||||
this.hideDownloader(false);
|
||||
this.setHeaderAndFileInfoVisible(!this.state.showFileInfo);
|
||||
};
|
||||
|
||||
handleImageDoubleTap = (x, y) => {
|
||||
this.zoomableImages[this.state.currentFile].toggleZoom(x, y);
|
||||
};
|
||||
|
||||
setHeaderAndFileInfoVisible = (show) => {
|
||||
this.setState({
|
||||
showFileInfo: show
|
||||
|
|
@ -184,35 +246,6 @@ export default class ImagePreview extends PureComponent {
|
|||
}).start();
|
||||
};
|
||||
|
||||
handleScroll = (event) => {
|
||||
const offset = event.nativeEvent.contentOffset.x / this.props.deviceWidth;
|
||||
const wholeNumber = Number((offset).toFixed(0));
|
||||
if (Math.abs(offset - wholeNumber) < 0.01) {
|
||||
this.setState({
|
||||
currentFile: wholeNumber,
|
||||
pagingEnabled: true,
|
||||
shouldShrinkImages: false
|
||||
});
|
||||
} else if (!this.state.shouldShrinkImages && !this.state.isZooming) {
|
||||
this.setState({
|
||||
shouldShrinkImages: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
attachScrollView = (c) => {
|
||||
this.scrollView = c;
|
||||
};
|
||||
|
||||
imageIsZooming = (zooming) => {
|
||||
if (zooming !== this.state.isZooming) {
|
||||
this.setHeaderAndFileInfoVisible(!zooming);
|
||||
this.setState({
|
||||
isZooming: zooming
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
showDeletedFilesAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
|
|
@ -233,7 +266,15 @@ export default class ImagePreview extends PureComponent {
|
|||
onPress: this.close
|
||||
}]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
showDownloader = () => {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
|
||||
this.setState({
|
||||
showDownloader: true
|
||||
});
|
||||
};
|
||||
|
||||
showDownloadOptions = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
|
|
@ -247,53 +288,114 @@ export default class ImagePreview extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
showIOSDownloadOptions = () => {
|
||||
this.setHeaderAndFileInfoVisible(false);
|
||||
|
||||
const options = {
|
||||
title: this.state.files[this.state.currentFile].name,
|
||||
items: [{
|
||||
showIOSDownloadOptions = async () => {
|
||||
const file = this.state.files[this.state.currentFile];
|
||||
const items = [];
|
||||
if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
const path = `${VIDEOS_PATH}/${file.id}.${file.extension}`;
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
if (exist) {
|
||||
items.push({
|
||||
action: this.saveVideo,
|
||||
text: {
|
||||
id: 'mobile.image_preview.save_video',
|
||||
defaultMessage: 'Save Video'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.showVideoDownloadRequiredAlert();
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
action: this.showDownloader,
|
||||
text: {
|
||||
id: 'mobile.image_preview.save',
|
||||
defaultMessage: 'Save Image'
|
||||
}
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
title: file.name,
|
||||
items,
|
||||
onCancelPress: () => this.setHeaderAndFileInfoVisible(true)
|
||||
};
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
...options
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
};
|
||||
if (items.length) {
|
||||
this.setHeaderAndFileInfoVisible(false);
|
||||
|
||||
showDownloader = () => {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
||||
|
||||
this.setState({
|
||||
showDownloader: true
|
||||
});
|
||||
};
|
||||
|
||||
hideDownloader = (hideFileInfo = true) => {
|
||||
this.setState({showDownloader: false});
|
||||
if (hideFileInfo) {
|
||||
this.setHeaderAndFileInfoVisible(true);
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
...options
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
showVideoDownloadRequiredAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video.save_error_title',
|
||||
defaultMessage: 'Save Video Error'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video.save_error_message',
|
||||
defaultMessage: 'To save the video file you need to download it first.'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
renderAttachmentDocument = (file) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<FileAttachmentDocument
|
||||
file={file}
|
||||
theme={theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderAttachmentIcon = (file) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
onPress={this.handleImageTap}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
renderDownloadButton = () => {
|
||||
const {canDownloadFiles} = this.props;
|
||||
const {files} = this.state;
|
||||
|
|
@ -312,7 +414,7 @@ export default class ImagePreview extends PureComponent {
|
|||
color='#fff'
|
||||
/>
|
||||
);
|
||||
} else if (file.has_preview_image) {
|
||||
} else if (file.has_preview_image || SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
action = this.showDownloadOptions;
|
||||
icon = (
|
||||
<Icon
|
||||
|
|
@ -334,9 +436,46 @@ export default class ImagePreview extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
renderPreviewer = (file, index) => {
|
||||
const maxImageHeight = this.props.deviceHeight - STATUSBAR_HEIGHT;
|
||||
|
||||
return (
|
||||
<Previewer
|
||||
ref={(c) => {
|
||||
this.zoomableImages[index] = c;
|
||||
}}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
imageHeight={Math.min(maxImageHeight, file.height)}
|
||||
imageWidth={Math.min(this.props.deviceWidth, file.width)}
|
||||
shrink={this.state.shouldShrinkImages}
|
||||
wrapperHeight={this.props.deviceHeight}
|
||||
wrapperWidth={this.props.deviceWidth}
|
||||
onImageTap={this.handleImageTap}
|
||||
onImageDoubleTap={this.handleImageDoubleTap}
|
||||
onZoom={this.imageIsZooming}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderVideoPreview = (file) => {
|
||||
const {deviceHeight, deviceWidth, theme} = this.props;
|
||||
|
||||
return (
|
||||
<VideoPreview
|
||||
file={file}
|
||||
onFullScreen={this.handleImageTap}
|
||||
onSeeking={this.handleVideoSeek}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const marginStyle = {
|
||||
...Platform.select({
|
||||
ios: {
|
||||
|
|
@ -363,41 +502,24 @@ export default class ImagePreview extends PureComponent {
|
|||
pagingEnabled={!this.state.isZooming}
|
||||
bounces={false}
|
||||
onScroll={this.handleScroll}
|
||||
onMomentumScrollEnd={this.handleScrollStopped}
|
||||
scrollEventThrottle={2}
|
||||
>
|
||||
{this.state.files.map((file, index) => {
|
||||
let mime = file.mime_type;
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
}
|
||||
|
||||
let component;
|
||||
if (file.has_preview_image || file.mime_type === 'image/gif') {
|
||||
component = (
|
||||
<Previewer
|
||||
ref={(c) => {
|
||||
this.zoomableImages[index] = c;
|
||||
}}
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
imageHeight={Math.min(maxImageHeight, file.height)}
|
||||
imageWidth={Math.min(this.props.deviceWidth, file.width)}
|
||||
shrink={this.state.shouldShrinkImages}
|
||||
wrapperHeight={this.props.deviceHeight}
|
||||
wrapperWidth={this.props.deviceWidth}
|
||||
onImageTap={this.handleImageTap}
|
||||
onImageDoubleTap={this.handleImageDoubleTap}
|
||||
onZoom={this.imageIsZooming}
|
||||
/>
|
||||
);
|
||||
component = this.renderPreviewer(file, index);
|
||||
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
|
||||
component = this.renderAttachmentDocument(file);
|
||||
} else if (SUPPORTED_VIDEO_FORMAT.includes(file.mime_type)) {
|
||||
component = this.renderVideoPreview(file);
|
||||
} else {
|
||||
component = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={this.props.theme}
|
||||
iconHeight={120}
|
||||
iconWidth={120}
|
||||
wrapperHeight={200}
|
||||
wrapperWidth={200}
|
||||
/>
|
||||
);
|
||||
component = this.renderAttachmentIcon(file);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -446,6 +568,7 @@ export default class ImagePreview extends PureComponent {
|
|||
</AnimatedView>
|
||||
</AnimatedView>
|
||||
<Downloader
|
||||
ref='downloader'
|
||||
show={this.state.showDownloader}
|
||||
file={this.state.files[this.state.currentFile]}
|
||||
deviceHeight={this.props.deviceHeight}
|
||||
|
|
@ -472,7 +595,15 @@ const style = StyleSheet.create({
|
|||
height: 70,
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 16
|
||||
paddingBottom: 16,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 13
|
||||
},
|
||||
ios: {
|
||||
marginBottom: 0
|
||||
}
|
||||
})
|
||||
},
|
||||
footerHeaderWrapper: {
|
||||
position: 'absolute',
|
||||
|
|
@ -490,7 +621,15 @@ const style = StyleSheet.create({
|
|||
headerControls: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginTop: 0
|
||||
},
|
||||
ios: {
|
||||
marginTop: 5
|
||||
}
|
||||
})
|
||||
},
|
||||
headerIcon: {
|
||||
height: 44,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ function makeMapStateToProps() {
|
|||
const getFilesForPost = makeGetFilesForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
...getDimensions(state),
|
||||
canDownloadFiles: canDownloadFilesOnMobile(state),
|
||||
fetchCache: state.views.fetchCache,
|
||||
|
|
|
|||
239
app/screens/image_preview/video_preview.js
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Video from 'react-native-video';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import VideoControls, {PLAYER_STATE} from 'app/components/video_controls';
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
|
||||
import Downloader from './downloader.ios';
|
||||
|
||||
const {VIDEOS_PATH} = DeviceTypes;
|
||||
|
||||
export default class VideoPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
file: PropTypes.object.isRequired,
|
||||
onFullScreen: PropTypes.func.isRequired,
|
||||
onSeeking: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
isFullScreen: false,
|
||||
playerState: PLAYER_STATE.PAUSED,
|
||||
paused: true,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
path: null,
|
||||
showDownloader: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
EventEmitter.on('stop-video-playback', this.stopPlayback);
|
||||
this.initializeComponent();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('stop-video-playback', this.stopPlayback);
|
||||
}
|
||||
|
||||
async initializeComponent() {
|
||||
const {file} = this.props;
|
||||
const path = `${VIDEOS_PATH}/${file.id}.${file.extension}`;
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
if (exist) {
|
||||
this.setState({path});
|
||||
}
|
||||
}
|
||||
|
||||
exitFullScreen = () => {
|
||||
this.setState({isFullScreen: false});
|
||||
this.props.onFullScreen();
|
||||
};
|
||||
|
||||
enterFullScreen = () => {
|
||||
this.setState({isFullScreen: true});
|
||||
this.props.onFullScreen();
|
||||
};
|
||||
|
||||
onDownloadSuccess = () => {
|
||||
const {file} = this.props;
|
||||
const path = `${VIDEOS_PATH}/${file.id}.${file.extension}`;
|
||||
|
||||
this.setState({showDownloader: false, path});
|
||||
};
|
||||
|
||||
onEnd = () => {
|
||||
if (this.state.isFullScreen) {
|
||||
this.props.onFullScreen();
|
||||
}
|
||||
|
||||
this.setState({playerState: PLAYER_STATE.ENDED, isFullScreen: false, paused: true});
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_title',
|
||||
defaultMessage: 'Video playback failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.video_playback.failed_description',
|
||||
defaultMessage: 'An error occurred while trying to play the video.\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
onFullScreen = () => {
|
||||
if (this.state.isFullScreen) {
|
||||
this.exitFullScreen();
|
||||
} else {
|
||||
this.enterFullScreen();
|
||||
}
|
||||
};
|
||||
|
||||
onLoad = (data) => {
|
||||
this.setState({duration: data.duration, isLoading: false});
|
||||
};
|
||||
|
||||
onLoadStart = () => {
|
||||
this.setState({isLoading: true});
|
||||
};
|
||||
|
||||
onPaused = () => {
|
||||
this.setState({
|
||||
paused: !this.state.paused,
|
||||
playerState: this.state.paused ? PLAYER_STATE.PLAYING : PLAYER_STATE.PAUSED
|
||||
});
|
||||
};
|
||||
|
||||
onProgress = (data) => {
|
||||
if (this.state.isLoading || this.state.playerState !== PLAYER_STATE.PLAYING) {
|
||||
return;
|
||||
}
|
||||
this.setState({currentTime: data.currentTime});
|
||||
};
|
||||
|
||||
onReplay = () => {
|
||||
if (this.refs.videoPlayer) {
|
||||
this.setState({playerState: PLAYER_STATE.PLAYING, paused: false});
|
||||
this.refs.videoPlayer.seek(0);
|
||||
}
|
||||
};
|
||||
|
||||
onSeek = (seek) => {
|
||||
if (this.refs.videoPlayer) {
|
||||
this.refs.videoPlayer.seek(seek);
|
||||
}
|
||||
};
|
||||
|
||||
stopPlayback = () => {
|
||||
if (!this.state.paused) {
|
||||
this.onPaused();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
deviceHeight,
|
||||
deviceWidth,
|
||||
file,
|
||||
onSeeking,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
const {currentTime, duration, isFullScreen, isLoading, path, paused, playerState, showDownloader} = this.state;
|
||||
|
||||
if (!path) {
|
||||
return (
|
||||
<Downloader
|
||||
show={showDownloader}
|
||||
file={file}
|
||||
deviceHeight={deviceHeight}
|
||||
deviceWidth={deviceWidth}
|
||||
downloadPath={VIDEOS_PATH}
|
||||
prompt={true}
|
||||
saveToCameraRoll={false}
|
||||
onDownloadSuccess={this.onDownloadSuccess}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, {height: deviceHeight, width: deviceWidth}]}>
|
||||
<Video
|
||||
ref='videoPlayer'
|
||||
style={[styles.mediaPlayer, {width: deviceWidth}]}
|
||||
resizeMode='contain'
|
||||
source={{uri: path}}
|
||||
volume={1.0}
|
||||
paused={paused}
|
||||
onEnd={this.onEnd}
|
||||
onLoad={this.onLoad}
|
||||
onLoadStart={this.onLoadStart}
|
||||
onProgress={this.onProgress}
|
||||
onError={this.onError}
|
||||
/>
|
||||
<VideoControls
|
||||
ref='controls'
|
||||
mainColor={theme.linkColor}
|
||||
playerState={playerState}
|
||||
isFullScreen={isFullScreen}
|
||||
isLoading={isLoading}
|
||||
progress={currentTime}
|
||||
duration={duration}
|
||||
onPaused={this.onPaused}
|
||||
onSeek={this.onSeek}
|
||||
onSeeking={onSeeking}
|
||||
onReplay={this.onReplay}
|
||||
onFullScreen={this.onFullScreen}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
mediaPlayer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'black'
|
||||
}
|
||||
});
|
||||
|
|
@ -5,14 +5,19 @@ import React, {PureComponent} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Platform,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import SettingsItem from 'app/screens/settings/settings_item';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {deleteFileCache, getFileCacheSize} from 'app/utils/file';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
class AdvancedSettings extends PureComponent {
|
||||
|
|
@ -24,7 +29,16 @@ class AdvancedSettings extends PureComponent {
|
|||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
clearOfflineCache = () => {
|
||||
state = {
|
||||
cacheSize: null,
|
||||
cacheSizedFetched: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getDownloadCacheSize();
|
||||
}
|
||||
|
||||
clearOfflineCache = wrapWithPreventDoubleTap(() => {
|
||||
const {actions, intl} = this.props;
|
||||
|
||||
Alert.alert(
|
||||
|
|
@ -38,10 +52,65 @@ class AdvancedSettings extends PureComponent {
|
|||
onPress: () => true
|
||||
}]
|
||||
);
|
||||
});
|
||||
|
||||
clearDownloadCache = wrapWithPreventDoubleTap(() => {
|
||||
const {intl} = this.props;
|
||||
const {cacheSize} = this.state;
|
||||
|
||||
if (cacheSize) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.advanced_settings.delete_file_cache',
|
||||
defaultMessage: 'Delete File Cache'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.advanced_settings.delete_file_cache_message',
|
||||
defaultMessage: '\nThis will delete all the files stored in the cache. Are you sure you want to delete them?\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'mobile.advanced_settings.delete', defaultMessage: 'Delete'}),
|
||||
onPress: () => {
|
||||
this.setState({cacheSize: null, cacheSizedFetched: false}, async () => {
|
||||
await deleteFileCache();
|
||||
this.setState({cacheSize: 0, cacheSizedFetched: true});
|
||||
});
|
||||
}
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'}),
|
||||
onPress: () => true
|
||||
}]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
getDownloadCacheSize = async () => {
|
||||
const size = await getFileCacheSize();
|
||||
this.setState({cacheSize: size, cacheSizedFetched: true});
|
||||
};
|
||||
|
||||
handlePress = (action) => {
|
||||
preventDoubleTap(action, this);
|
||||
renderCacheFileSize = () => {
|
||||
const {theme} = this.props;
|
||||
const {cacheSize, cacheSizedFetched} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let component;
|
||||
if (cacheSize === null) {
|
||||
component = (
|
||||
<ActivityIndicator
|
||||
size='small'
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
);
|
||||
} else if (cacheSizedFetched) {
|
||||
component = (
|
||||
<Text style={style.cacheSize}>
|
||||
{getFormattedFileSize({size: cacheSize})}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
@ -58,12 +127,24 @@ class AdvancedSettings extends PureComponent {
|
|||
i18nId='mobile.advanced_settings.reset_title'
|
||||
iconName='ios-refresh'
|
||||
iconType='ion'
|
||||
onPress={() => this.handlePress(this.clearOfflineCache)}
|
||||
onPress={this.clearOfflineCache}
|
||||
separator={false}
|
||||
showArrow={false}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.divider}/>
|
||||
<SettingsItem
|
||||
defaultMessage='Delete File Cache'
|
||||
i18nId='mobile.advanced_settings.clear_downloads'
|
||||
iconName='md-trash'
|
||||
iconType='ion'
|
||||
onPress={this.clearDownloadCache}
|
||||
separator={false}
|
||||
showArrow={false}
|
||||
rightComponent={this.renderCacheFileSize()}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.divider}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -88,6 +169,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1
|
||||
},
|
||||
cacheSize: {
|
||||
color: theme.centerChannelColor,
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
lineHeight: 43
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default class SettingsItem extends PureComponent {
|
|||
isDestructor: PropTypes.bool,
|
||||
centered: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
rightComponent: PropTypes.node,
|
||||
separator: PropTypes.bool,
|
||||
showArrow: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
|
|
@ -39,6 +40,7 @@ export default class SettingsItem extends PureComponent {
|
|||
iconType,
|
||||
isDestructor,
|
||||
onPress,
|
||||
rightComponent,
|
||||
separator,
|
||||
showArrow,
|
||||
theme
|
||||
|
|
@ -66,6 +68,18 @@ export default class SettingsItem extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
let additionalComponent;
|
||||
if (showArrow) {
|
||||
additionalComponent = (
|
||||
<FontAwesomeIcon
|
||||
name='angle-right'
|
||||
style={style.arrow}
|
||||
/>
|
||||
);
|
||||
} else if (rightComponent) {
|
||||
additionalComponent = rightComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
|
|
@ -83,12 +97,9 @@ export default class SettingsItem extends PureComponent {
|
|||
defaultMessage={defaultMessage}
|
||||
style={[style.label, destructor, centered ? style.centerLabel : {}]}
|
||||
/>
|
||||
{showArrow &&
|
||||
{Boolean(additionalComponent) &&
|
||||
<View style={style.arrowContainer}>
|
||||
<FontAwesomeIcon
|
||||
name='angle-right'
|
||||
style={style.arrow}
|
||||
/>
|
||||
{additionalComponent}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ export default makeStyleSheetFromTheme((theme) => {
|
|||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
height: 1
|
||||
},
|
||||
arrowContainer: {
|
||||
justifyContent: 'center',
|
||||
paddingRight: 15
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
|
||||
const {DOCUMENTS_PATH, VIDEOS_PATH} = DeviceTypes;
|
||||
|
||||
export function generateId() {
|
||||
// Implementation taken from http://stackoverflow.com/a/2117523
|
||||
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||
|
|
@ -20,3 +25,31 @@ export function generateId() {
|
|||
|
||||
return 'uid' + id;
|
||||
}
|
||||
|
||||
export async function getFileCacheSize() {
|
||||
const isDocsDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH);
|
||||
const isVideosDir = await RNFetchBlob.fs.isDir(VIDEOS_PATH);
|
||||
let size = 0;
|
||||
|
||||
if (isDocsDir) {
|
||||
const docsStats = await RNFetchBlob.fs.lstat(DOCUMENTS_PATH);
|
||||
size = docsStats.reduce((accumulator, stat) => {
|
||||
return accumulator + parseInt(stat.size, 10);
|
||||
}, size);
|
||||
}
|
||||
|
||||
if (isVideosDir) {
|
||||
const videoStats = await RNFetchBlob.fs.lstat(VIDEOS_PATH);
|
||||
size = videoStats.reduce((accumulator, stat) => {
|
||||
return accumulator + parseInt(stat.size, 10);
|
||||
}, size);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export async function deleteFileCache() {
|
||||
await RNFetchBlob.fs.unlink(DOCUMENTS_PATH);
|
||||
await RNFetchBlob.fs.unlink(VIDEOS_PATH);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1878,6 +1878,9 @@
|
|||
"mobile.account_notifications.threads_mentions": "Mentions in threads",
|
||||
"mobile.account_notifications.threads_start": "Threads that I start",
|
||||
"mobile.account_notifications.threads_start_participate": "Threads that I start or participate in",
|
||||
"mobile.advanced_settings.delete": "Delete",
|
||||
"mobile.advanced_settings.delete_file_cache": "Delete File Cache",
|
||||
"mobile.advanced_settings.delete_file_cache_message": "\nThis will delete all the files stored in the cache. Are you sure you want to delete them?\n",
|
||||
"mobile.advanced_settings.reset_button": "Reset",
|
||||
"mobile.advanced_settings.reset_message": "\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n",
|
||||
"mobile.advanced_settings.reset_title": "Reset Cache",
|
||||
|
|
@ -1938,6 +1941,14 @@
|
|||
"mobile.create_channel.private": "New Private Channel",
|
||||
"mobile.create_channel.public": "New Public Channel",
|
||||
"mobile.custom_list.no_results": "No Results",
|
||||
"mobile.document_preview.failed_title": "Open Document failed",
|
||||
"mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n",
|
||||
"mobile.downloader.complete": "Download complete",
|
||||
"mobile.downloader.downloading": "Downloading...",
|
||||
"mobile.downloader.failed_description": "An error occurred while downloading the file. Please check your internet connection and try again.\n",
|
||||
"mobile.downloader.failed_title": "Download failed",
|
||||
"mobile.downloader.image_saved": "Image Saved",
|
||||
"mobile.downloader.video_saved": "Video Saved",
|
||||
"mobile.drawer.teamsTitle": "Teams",
|
||||
"mobile.edit_post.title": "Editing Message",
|
||||
"mobile.emoji_picker.activity": "ACTIVITY",
|
||||
|
|
@ -1960,6 +1971,7 @@
|
|||
"mobile.image_preview.deleted_post_message": "This post and its files have been deleted. The previewer will now be closed.",
|
||||
"mobile.image_preview.deleted_post_title": "Post Deleted",
|
||||
"mobile.image_preview.save": "Save Image",
|
||||
"mobile.image_preview.save_video": "Save Video",
|
||||
"mobile.intro_messages.DM": "This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.",
|
||||
"mobile.intro_messages.default_message": "This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.",
|
||||
"mobile.intro_messages.default_welcome": "Welcome to {name}!",
|
||||
|
|
@ -2071,6 +2083,10 @@
|
|||
"mobile.settings.modal.check_for_upgrade": "Check for Updates",
|
||||
"mobile.settings.team_selection": "Team Selection",
|
||||
"mobile.suggestion.members": "Members",
|
||||
"mobile.video.save_error_message": "To save the video file you need to download it first.",
|
||||
"mobile.video.save_error_title": "Save Video Error",
|
||||
"mobile.video_playback.failed_title": "Video playback failed",
|
||||
"mobile.video_playback.failed_description": "An error occurred while trying to play the video.\n",
|
||||
"modal.manaul_status.ask": "Do not ask me again",
|
||||
"modal.manaul_status.button": "Yes, set my status to \"Online\"",
|
||||
"modal.manaul_status.message": "Would you like to switch your status to \"Online\"?",
|
||||
|
|
|
|||
BIN
assets/base/images/video_player/fullscreen@2x.png
Normal file
|
After Width: | Height: | Size: 469 B |
BIN
assets/base/images/video_player/fullscreen@3x.png
Normal file
|
After Width: | Height: | Size: 696 B |
BIN
assets/base/images/video_player/pause@2x.png
Normal file
|
After Width: | Height: | Size: 284 B |
BIN
assets/base/images/video_player/pause@3x.png
Normal file
|
After Width: | Height: | Size: 367 B |
BIN
assets/base/images/video_player/play@2x.png
Normal file
|
After Width: | Height: | Size: 934 B |
BIN
assets/base/images/video_player/play@3x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/base/images/video_player/replay@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/base/images/video_player/replay@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -5,7 +5,6 @@
|
|||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
|
||||
|
|
@ -33,6 +32,7 @@
|
|||
3D38ABA732A34A9BB3294F90 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; };
|
||||
420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EDC04CBCF81642219D199CBB /* Octicons.ttf */; };
|
||||
55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */; };
|
||||
5A0920184BD344979BCFCD5C /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B89192186C764B9FA473403A /* libRCTVideo.a */; };
|
||||
5CB86E9DFB94485CAA748DF3 /* YTPlayerView-iframe-player.html in Resources */ = {isa = PBXBuildFile; fileRef = 79CB6EBA24FE4ABFB0C155F0 /* YTPlayerView-iframe-player.html */; };
|
||||
5E1AF7B72B8D4A4E9E53FF9D /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; };
|
||||
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
|
||||
|
|
@ -75,6 +75,7 @@
|
|||
F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 356C9186FA374641A00EB2EA /* MaterialIcons.ttf */; };
|
||||
F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; };
|
||||
F23C99AA5FA10E457A76803A /* libPods-MattermostTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4246DC09024BB33A3E491E25 /* libPods-MattermostTests.a */; };
|
||||
3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -442,6 +443,41 @@
|
|||
remoteGlobalIDString = 38C138441D3FC3F600A8162D;
|
||||
remoteInfo = JailMonkey;
|
||||
};
|
||||
7F93F9B11FBB726B0088E416 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3DBE0D001F3B181A0099AA32;
|
||||
remoteInfo = fishhook;
|
||||
};
|
||||
7F93F9B31FBB726B0088E416 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32;
|
||||
remoteInfo = "fishhook-tvOS";
|
||||
};
|
||||
7F93F9C71FBB726B0088E416 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = E72EC1401F7ABB5A0001BC90;
|
||||
remoteInfo = "RNDeviceInfo-tvOS";
|
||||
};
|
||||
7F93F9D81FBB726B0088E416 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||
remoteInfo = RCTVideo;
|
||||
};
|
||||
7F93F9DA1FBB726B0088E416 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 641E28441F0EEC8500443AF6;
|
||||
remoteInfo = "RCTVideo-tvOS";
|
||||
};
|
||||
7FA795051F61A1A500C02924 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 9552041247E749308CE7B412 /* RNSentry.xcodeproj */;
|
||||
|
|
@ -585,6 +621,7 @@
|
|||
A2968536BEC74A4ABBB73BD9 /* RCTYouTube.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTYouTube.xcodeproj; path = "../node_modules/react-native-youtube/RCTYouTube.xcodeproj"; sourceTree = "<group>"; };
|
||||
AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = "<group>"; };
|
||||
ACC6B9FDC0AD45A6BFA4FBCD /* libBVLinearGradient.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libBVLinearGradient.a; sourceTree = "<group>"; };
|
||||
B89192186C764B9FA473403A /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTVideo.a; sourceTree = "<group>"; };
|
||||
B97AA6F961BB47D3A3297E8E /* RNDeviceInfo.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNDeviceInfo.xcodeproj; path = "../node_modules/react-native-device-info/RNDeviceInfo.xcodeproj"; sourceTree = "<group>"; };
|
||||
BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = "<group>"; };
|
||||
|
|
@ -592,12 +629,15 @@
|
|||
C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Semibold.ttf"; path = "../assets/fonts/OpenSans-Semibold.ttf"; sourceTree = "<group>"; };
|
||||
D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNLocalAuth.xcodeproj; path = "../node_modules/react-native-local-auth/RNLocalAuth.xcodeproj"; sourceTree = "<group>"; };
|
||||
D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Bold.ttf"; path = "../assets/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTVideo.xcodeproj; path = "../node_modules/react-native-video/ios/RCTVideo.xcodeproj"; sourceTree = "<group>"; };
|
||||
EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNPasscodeStatus.xcodeproj; path = "../node_modules/react-native-passcode-status/ios/RNPasscodeStatus.xcodeproj"; sourceTree = "<group>"; };
|
||||
EDC04CBCF81642219D199CBB /* Octicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Octicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = "<group>"; };
|
||||
EE3EE4548D3F4A49B1274722 /* libRNLocalAuth.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNLocalAuth.a; sourceTree = "<group>"; };
|
||||
EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = "<group>"; };
|
||||
F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
|
||||
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = "<group>"; };
|
||||
41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */ = {isa = PBXFileReference; name = "RNReactNativeDocViewer.xcodeproj"; path = "../node_modules/react-native-doc-viewer/ios/RNReactNativeDocViewer.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
|
||||
6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */ = {isa = PBXFileReference; name = "libRNReactNativeDocViewer.a"; path = "libRNReactNativeDocViewer.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -649,6 +689,8 @@
|
|||
7F43D5DE1F6BF96A001FC614 /* libRCTYouTube.a in Frameworks */,
|
||||
7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */,
|
||||
A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */,
|
||||
5A0920184BD344979BCFCD5C /* libRCTVideo.a in Frameworks */,
|
||||
3F876A0DC6314E27B16C8A96 /* libRNReactNativeDocViewer.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -875,6 +917,7 @@
|
|||
2AE23B96BF7545B9A77C0C87 /* libRNSentry.a */,
|
||||
8606EB1EB7E349EF8248933E /* libz.tbd */,
|
||||
7535D128F00C4A47A182627E /* libRNCookieManagerIOS.a */,
|
||||
B89192186C764B9FA473403A /* libRCTVideo.a */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -985,6 +1028,15 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F93F9D41FBB726B0088E416 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7F93F9D91FBB726B0088E416 /* libRCTVideo.a */,
|
||||
7F93F9DB1FBB726B0088E416 /* libRCTVideo.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7FA795021F61A1A500C02924 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1069,6 +1121,8 @@
|
|||
9552041247E749308CE7B412 /* RNSentry.xcodeproj */,
|
||||
62A58E5E984E4CF1811620B8 /* RNCookieManagerIOS.xcodeproj */,
|
||||
A2968536BEC74A4ABBB73BD9 /* RCTYouTube.xcodeproj */,
|
||||
DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */,
|
||||
41898656FAE24E0BB390D0E4 /* RNReactNativeDocViewer.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1255,6 +1309,10 @@
|
|||
ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */;
|
||||
ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 7F93F9D41FBB726B0088E416 /* Products */;
|
||||
ProjectRef = DF9DAAAA482343F3910A1A4C /* RCTVideo.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 139FDEE71B06529A00C62182 /* Products */;
|
||||
ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
|
|
@ -1686,6 +1744,41 @@
|
|||
remoteRef = 7F8C4A611F3E21FB003A22BA /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7F93F9B21FBB726B0088E416 /* libfishhook.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libfishhook.a;
|
||||
remoteRef = 7F93F9B11FBB726B0088E416 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7F93F9B41FBB726B0088E416 /* libfishhook-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libfishhook-tvOS.a";
|
||||
remoteRef = 7F93F9B31FBB726B0088E416 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7F93F9C81FBB726B0088E416 /* libRNDeviceInfo-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRNDeviceInfo-tvOS.a";
|
||||
remoteRef = 7F93F9C71FBB726B0088E416 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7F93F9D91FBB726B0088E416 /* libRCTVideo.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTVideo.a;
|
||||
remoteRef = 7F93F9D81FBB726B0088E416 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7F93F9DB1FBB726B0088E416 /* libRCTVideo.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTVideo.a;
|
||||
remoteRef = 7F93F9DA1FBB726B0088E416 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
7FA795061F61A1A500C02924 /* libRNSentry.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
|
|
@ -1947,13 +2040,19 @@
|
|||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-video/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = MattermostTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -1972,13 +2071,19 @@
|
|||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/../node_modules/react-native-video/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = MattermostTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
"\"$(SRCROOT)/$(TARGET_NAME)\"",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -2020,6 +2125,8 @@
|
|||
"$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS",
|
||||
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-youtube/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-video/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
|
|
@ -2067,6 +2174,8 @@
|
|||
"$(SRCROOT)/../node_modules/react-native-cookies/ios/RNCookieManagerIOS",
|
||||
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-youtube/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-video/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**",
|
||||
);
|
||||
INFOPLIST_FILE = Mattermost/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<key>NSCameraUsageDescription</key>
|
||||
<string>Take a Photo or Video and upload it to your mattermost instance</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<string/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Upload Photos and Videos to your Mattermost instance</string>
|
||||
<key>UIAppFonts</key>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"react-native-circular-progress": "jaystramel/react-native-circular-progress",
|
||||
"react-native-cookies": "3.2.0",
|
||||
"react-native-device-info": "0.12.1",
|
||||
"react-native-doc-viewer": "2.4.9",
|
||||
"react-native-drawer": "2.5.0",
|
||||
"react-native-exception-handler": "2.3.0",
|
||||
"react-native-fast-image": "1.0.0",
|
||||
|
|
@ -38,10 +39,12 @@
|
|||
"react-native-orientation": "enahum/react-native-orientation.git",
|
||||
"react-native-passcode-status": "1.1.0",
|
||||
"react-native-sentry": "0.15.1",
|
||||
"react-native-slider": "0.11.0",
|
||||
"react-native-status-bar-size": "0.3.2",
|
||||
"react-native-svg": "6.0.0",
|
||||
"react-native-tooltip": "enahum/react-native-tooltip",
|
||||
"react-native-vector-icons": "4.4.2",
|
||||
"react-native-video": "2.0.0",
|
||||
"react-native-youtube": "1.0.1",
|
||||
"react-redux": "5.0.6",
|
||||
"redux": "3.7.2",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,17 @@ mockery.registerMock('react-native-sentry', {
|
|||
captureBreadcrumb() {}
|
||||
}
|
||||
});
|
||||
|
||||
mockery.registerMock('react-native-fetch-blob', {
|
||||
RNFetchBlob: {
|
||||
DocumentDir: ''
|
||||
},
|
||||
fs: {
|
||||
dirs: {
|
||||
DocumentDir: '',
|
||||
CacheDir: ''
|
||||
}
|
||||
}
|
||||
});
|
||||
// Ignore all node_modules except these
|
||||
const modulesToCompile = [
|
||||
'react-native'
|
||||
|
|
|
|||
21
yarn.lock
|
|
@ -3704,7 +3704,7 @@ jws@^3.1.4:
|
|||
jwa "^1.1.4"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
keymirror@=0.1.1, keymirror@^0.1.1:
|
||||
keymirror@0.1.1, keymirror@=0.1.1, keymirror@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35"
|
||||
|
||||
|
|
@ -4746,7 +4746,7 @@ promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@15.6.0, prop-types@^15.6.0:
|
||||
prop-types@15.6.0, prop-types@^15.5.6, prop-types@^15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
|
||||
dependencies:
|
||||
|
|
@ -5032,6 +5032,10 @@ react-native-device-info@0.12.1:
|
|||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.12.1.tgz#659ed145caa5a86a4815904381b25e9ebe0c24f6"
|
||||
|
||||
react-native-doc-viewer@2.4.9:
|
||||
version "2.4.9"
|
||||
resolved "https://registry.yarnpkg.com/react-native-doc-viewer/-/react-native-doc-viewer-2.4.9.tgz#60edb4372a96acdb5fa343c75600decb843c7011"
|
||||
|
||||
react-native-drawer@2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-drawer/-/react-native-drawer-2.5.0.tgz#022cba5c0516126a9fc9cce3185cb46c644b51c4"
|
||||
|
|
@ -5147,6 +5151,12 @@ react-native-sentry@0.15.1:
|
|||
sentry-cli-binary "^1.16.0"
|
||||
xcode "0.9.3"
|
||||
|
||||
react-native-slider@0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-slider/-/react-native-slider-0.11.0.tgz#b68a0bc43c8422b24cd57947cc5ac2bcdb58fadc"
|
||||
dependencies:
|
||||
prop-types "^15.5.6"
|
||||
|
||||
react-native-status-bar-size@0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-status-bar-size/-/react-native-status-bar-size-0.3.2.tgz#e3ffd906f021220c256857a8de6b945f8ea3fabc"
|
||||
|
|
@ -5175,6 +5185,13 @@ react-native-vector-icons@4.4.2:
|
|||
prop-types "^15.5.10"
|
||||
yargs "^8.0.2"
|
||||
|
||||
react-native-video@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-video/-/react-native-video-2.0.0.tgz#f33fa6fb7e7e3c93ab5787944cefd58bf7355867"
|
||||
dependencies:
|
||||
keymirror "0.1.1"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-native-youtube@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-youtube/-/react-native-youtube-1.0.1.tgz#8d6ee69d88b913b4fa23ed64f99ae8812507745f"
|
||||
|
|
|
|||