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
This commit is contained in:
enahum 2017-11-20 18:28:20 -03:00 committed by GitHub
parent 7fb4b4f8ed
commit bc1554e1bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1898 additions and 260 deletions

View file

@ -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.

View file

@ -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'

View file

@ -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()
);
}

View file

@ -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'

View file

@ -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'
}
};
});

View 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
}
});

View file

@ -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'
}
});

View file

@ -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),

View file

@ -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});
}
});

View 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
}
});

View file

@ -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`
};

View file

@ -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');
};

View file

@ -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
}
});

View file

@ -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,

View file

@ -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,

View 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'
}
});

View file

@ -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
}
};
});

View file

@ -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>

View file

@ -44,6 +44,10 @@ export default makeStyleSheetFromTheme((theme) => {
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
},
arrowContainer: {
justifyContent: 'center',
paddingRight: 15
}
};
});

View file

@ -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;
}

View file

@ -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\"?",

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -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;

View file

@ -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>

View file

@ -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",

View file

@ -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'

View file

@ -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"