mattermost-mobile/app/screens/image_preview/downloader.ios.js
enahum 4995a76f2c Add Landscape support for both platforms (#909)
* Landscape support

* Fix image rotation on Android

* Fix landscape mode for login and login options

* Fix previewer will receive props

* Move device dimensions and others to redux

* Fix unit tests

* Include orientation and tablet in the store
2017-09-20 12:54:24 -07:00

312 lines
9.2 KiB
JavaScript

// 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 {
Animated,
CameraRoll,
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 {Client4} from 'mattermost-redux/client';
import FormattedText from 'app/components/formatted_text';
import {emptyFunction} from 'app/utils/general';
const {View: AnimatedView} = Animated;
export default class Downloader extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
file: PropTypes.object.isRequired,
onDownloadCancel: PropTypes.func,
onDownloadSuccess: PropTypes.func,
show: PropTypes.bool
};
static defaultProps = {
onCancelPress: emptyFunction,
onDownloadSuccess: emptyFunction,
show: false
};
constructor(props) {
super(props);
this.state = {
downloaderTop: new Animated.Value(props.deviceHeight),
progress: 0
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
this.toggleDownloader();
this.setState({
didCancel: false,
progress: 0
});
} else if (!nextProps.show && this.props.show) {
this.toggleDownloader(false);
} else if (this.props.deviceHeight !== nextProps.deviceHeight) {
this.recenterDownloader(nextProps);
}
}
recenterDownloader = (props) => {
const {deviceHeight, show} = props;
const top = show ? (deviceHeight / 2) - 100 : deviceHeight;
Animated.sequence([
Animated.delay(200),
Animated.spring(this.state.downloaderTop, {
toValue: top,
tension: 8,
friction: 5
})
]).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 realFill = Number(fill.toFixed(0));
let component;
if (realFill === 100) {
component = (
<Icon
name='ios-checkmark'
size={64}
color='white'
/>
);
} else {
component = (
<View style={styles.progressCirclePercentage}>
<Text style={styles.progressText}>
{`${fill.toFixed(0)}%`}
</Text>
<TouchableOpacity
style={styles.cancelButton}
onPress={this.handleCancelDownload}
>
<FormattedText
id='channel_modal.cancel'
defaultMessage='Cancel'
style={styles.cancelText}
/>
</TouchableOpacity>
</View>
);
}
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}
/>
}
</View>
</View>
);
};
render() {
const {show} = this.props;
if (!show) {
return null;
}
const {didCancel, progress} = this.state;
const trueProgress = didCancel ? 0 : progress;
const containerHeight = show ? '100%' : 0;
return (
<View style={[styles.container, {height: containerHeight}]}>
<AnimatedView style={[styles.downloader, {top: this.state.downloaderTop}]}>
<View style={styles.progressCircleContent}>
<AnimatedCircularProgress
size={120}
fill={trueProgress}
width={4}
backgroundColor='rgba(255, 255, 255, 0.5)'
tintColor='white'
rotation={0}
style={styles.progressCircle}
>
{this.renderProgress}
</AnimatedCircularProgress>
</View>
</AnimatedView>
</View>
);
}
}
const styles = StyleSheet.create({
bottomContent: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
},
bottomText: {
color: 'white',
fontSize: 16,
fontWeight: '600'
},
cancelButton: {
height: 30,
width: 60,
alignItems: 'center',
justifyContent: 'center',
marginTop: 5
},
cancelText: {
color: 'white',
fontSize: 12
},
container: {
position: 'absolute',
top: 0,
left: 0,
width: '100%'
},
downloader: {
alignItems: 'center',
alignSelf: 'center',
height: 220,
width: 236,
borderRadius: 8,
backgroundColor: 'rgba(0, 0, 0, 0.8)'
},
progressContainer: {
flex: 1
},
progressContent: {
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
left: 0,
alignItems: 'center',
justifyContent: 'center'
},
progressCircle: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center'
},
progressCircleContent: {
width: 200,
height: 200,
alignItems: 'center',
justifyContent: 'center'
},
progressCirclePercentage: {
flex: 1,
alignItems: 'center',
marginTop: 80
},
progressText: {
color: 'white',
fontSize: 18
}
});