* Use cache only for http URIs * Style fixes * No need for cache prop * Remove redundant absolute positioning * Cap at two lines Co-authored-by: Miguel Alatzar <this.migbot@gmail.com>
662 lines
19 KiB
JavaScript
662 lines
19 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {PureComponent} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
Alert,
|
|
Animated,
|
|
Platform,
|
|
SafeAreaView,
|
|
StatusBar,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
findNodeHandle,
|
|
} from 'react-native';
|
|
import RNFetchBlob from 'rn-fetch-blob';
|
|
import Icon from 'react-native-vector-icons/Ionicons';
|
|
import LinearGradient from 'react-native-linear-gradient';
|
|
import {intlShape} from 'react-intl';
|
|
import Permissions from 'react-native-permissions';
|
|
import Gallery from 'react-native-image-gallery';
|
|
import DeviceInfo from 'react-native-device-info';
|
|
import FastImage from 'react-native-fast-image';
|
|
|
|
import EventEmitter from '@mm-redux/utils/event_emitter';
|
|
|
|
import FileAttachmentDocument from 'app/components/file_attachment_list/file_attachment_document';
|
|
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
|
import {DeviceTypes, NavigationTypes} from 'app/constants';
|
|
import {getLocalFilePathFromFile, isDocument, isVideo} from 'app/utils/file';
|
|
import {emptyFunction} from 'app/utils/general';
|
|
import {calculateDimensions} from 'app/utils/images';
|
|
import {t} from 'app/utils/i18n';
|
|
import BottomSheet from 'app/utils/bottom_sheet';
|
|
import {
|
|
dismissModal,
|
|
mergeNavigationOptions,
|
|
} from 'app/actions/navigation';
|
|
|
|
import Downloader from './downloader';
|
|
import VideoPreview from './video_preview';
|
|
|
|
const {VIDEOS_PATH} = DeviceTypes;
|
|
const {View: AnimatedView} = Animated;
|
|
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
|
const HEADER_HEIGHT = 48;
|
|
const ANIM_CONFIG = {duration: 300, userNativeDriver: true};
|
|
|
|
export default class ImagePreview extends PureComponent {
|
|
static propTypes = {
|
|
componentId: PropTypes.string.isRequired,
|
|
canDownloadFiles: PropTypes.bool.isRequired,
|
|
deviceHeight: PropTypes.number.isRequired,
|
|
deviceWidth: PropTypes.number.isRequired,
|
|
files: PropTypes.array,
|
|
getItemMeasures: PropTypes.func.isRequired,
|
|
index: PropTypes.number.isRequired,
|
|
origin: PropTypes.object,
|
|
target: PropTypes.object,
|
|
theme: PropTypes.object.isRequired,
|
|
};
|
|
|
|
static defaultProps = {
|
|
files: [],
|
|
};
|
|
|
|
static contextTypes = {
|
|
intl: intlShape,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const options = {
|
|
layout: {
|
|
backgroundColor: '#000',
|
|
},
|
|
};
|
|
mergeNavigationOptions(props.componentId, options);
|
|
|
|
this.openAnim = new Animated.Value(0);
|
|
this.headerFooterAnim = new Animated.Value(1);
|
|
this.documents = [];
|
|
|
|
this.state = {
|
|
index: props.index,
|
|
origin: props.origin,
|
|
showDownloader: false,
|
|
target: props.target,
|
|
};
|
|
|
|
this.headerRef = React.createRef();
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.startOpenAnimation();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
StatusBar.setHidden(false, 'fade');
|
|
}
|
|
|
|
setDocumentRef = (ref) => {
|
|
this.documents[this.state.index] = ref;
|
|
}
|
|
|
|
setDownloaderRef = (ref) => {
|
|
this.downloaderRef = ref;
|
|
}
|
|
|
|
animateOpenAnimToValue = (toValue, onComplete) => {
|
|
Animated.timing(this.openAnim, {
|
|
...ANIM_CONFIG,
|
|
toValue,
|
|
}).start(() => {
|
|
this.setState({animating: false});
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
});
|
|
};
|
|
|
|
close = () => {
|
|
const {getItemMeasures, componentId} = this.props;
|
|
const {index} = this.state;
|
|
|
|
this.setState({animating: true});
|
|
const options = {
|
|
layout: {
|
|
backgroundColor: 'transparent',
|
|
},
|
|
};
|
|
mergeNavigationOptions(componentId, options);
|
|
|
|
getItemMeasures(index, (origin) => {
|
|
if (origin) {
|
|
this.setState(origin);
|
|
}
|
|
|
|
this.animateOpenAnimToValue(0, () => {
|
|
dismissModal();
|
|
});
|
|
});
|
|
};
|
|
|
|
handleChangeImage = (index) => {
|
|
this.setState({index});
|
|
};
|
|
|
|
handleSwipedVertical = (evt, gestureState) => {
|
|
if (Math.abs(gestureState.dy) > 150) {
|
|
this.close();
|
|
}
|
|
};
|
|
|
|
handleTapped = () => {
|
|
const {showHeaderFooter} = this.state;
|
|
this.setHeaderAndFooterVisible(!showHeaderFooter);
|
|
};
|
|
|
|
hideDownloader = (hideFileInfo = true) => {
|
|
this.setState({showDownloader: false});
|
|
if (hideFileInfo) {
|
|
this.setHeaderAndFooterVisible(true);
|
|
}
|
|
};
|
|
|
|
getCurrentFile = () => {
|
|
const {files} = this.props;
|
|
const {index} = this.state;
|
|
const file = files[index];
|
|
|
|
return file;
|
|
};
|
|
|
|
getFullscreenOpacity = () => {
|
|
const {target} = this.props;
|
|
|
|
return {
|
|
opacity: this.openAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0, target.opacity],
|
|
}),
|
|
};
|
|
};
|
|
|
|
getHeaderFooterStyle = () => {
|
|
return {
|
|
start: this.headerFooterAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [-80, 0],
|
|
}),
|
|
opacity: this.headerFooterAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0, 1],
|
|
}),
|
|
};
|
|
};
|
|
|
|
renderAttachmentDocument = (file) => {
|
|
const {canDownloadFiles, theme} = this.props;
|
|
|
|
return (
|
|
<View style={[style.flex, style.center]}>
|
|
<FileAttachmentDocument
|
|
ref={this.setDocumentRef}
|
|
backgroundColor='transparent'
|
|
canDownloadFiles={canDownloadFiles}
|
|
file={file}
|
|
iconHeight={200}
|
|
iconWidth={200}
|
|
theme={theme}
|
|
wrapperHeight={200}
|
|
wrapperWidth={200}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
renderAttachmentIcon = (file) => {
|
|
return (
|
|
<View style={[style.flex, style.center]}>
|
|
<FileAttachmentIcon
|
|
backgroundColor='transparent'
|
|
file={file}
|
|
theme={this.props.theme}
|
|
iconHeight={200}
|
|
iconWidth={200}
|
|
wrapperHeight={200}
|
|
wrapperWidth={200}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
renderDownloadButton = () => {
|
|
const {canDownloadFiles} = this.props;
|
|
const file = this.getCurrentFile();
|
|
|
|
if (file?.data?.localPath) {
|
|
// we already have the file locally we don't need to download it
|
|
return null;
|
|
}
|
|
|
|
if (file) {
|
|
let icon;
|
|
let action = emptyFunction;
|
|
if (canDownloadFiles) {
|
|
action = this.showDownloadOptions;
|
|
if (Platform.OS === 'android') {
|
|
icon = (
|
|
<Icon
|
|
name='md-more'
|
|
size={32}
|
|
color='#fff'
|
|
/>
|
|
);
|
|
} else if (file.source || isVideo(file.data)) {
|
|
icon = (
|
|
<Icon
|
|
name='ios-download'
|
|
size={26}
|
|
color='#fff'
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
ref={this.headerRef}
|
|
onPress={action}
|
|
style={style.headerIcon}
|
|
>
|
|
{icon}
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
renderDownloader = () => {
|
|
const {deviceHeight, deviceWidth} = this.props;
|
|
const file = this.getCurrentFile();
|
|
|
|
return (
|
|
<Downloader
|
|
ref={this.setDownloaderRef}
|
|
show={this.state.showDownloader}
|
|
file={file}
|
|
deviceHeight={deviceHeight}
|
|
deviceWidth={deviceWidth}
|
|
onDownloadCancel={this.hideDownloader}
|
|
onDownloadStart={this.hideDownloader}
|
|
onDownloadSuccess={this.hideDownloader}
|
|
/>
|
|
);
|
|
};
|
|
|
|
renderFooter = () => {
|
|
const {files} = this.props;
|
|
const {index} = this.state;
|
|
const footer = this.getHeaderFooterStyle();
|
|
return (
|
|
<Animated.View style={[{bottom: footer.start, opacity: footer.opacity}, style.footerContainer]}>
|
|
<LinearGradient
|
|
style={style.footer}
|
|
start={{x: 0.0, y: 0.0}}
|
|
end={{x: 0.0, y: 0.9}}
|
|
colors={['transparent', '#000000']}
|
|
pointerEvents='none'
|
|
>
|
|
<Text
|
|
style={style.filename}
|
|
numberOfLines={2}
|
|
ellipsizeMode='tail'
|
|
>
|
|
{(files[index] && files[index].caption) || ''}
|
|
</Text>
|
|
</LinearGradient>
|
|
</Animated.View>
|
|
);
|
|
};
|
|
|
|
renderGallery = () => {
|
|
return (
|
|
<Gallery
|
|
errorComponent={this.renderOtherItems}
|
|
imageComponent={this.renderImageComponent}
|
|
images={this.props.files}
|
|
initialPage={this.state.index}
|
|
onPageSelected={this.handleChangeImage}
|
|
onSingleTapConfirmed={this.handleTapped}
|
|
onSwipedVertical={this.handleSwipedVertical}
|
|
pageMargin={2}
|
|
style={style.flex}
|
|
/>
|
|
);
|
|
};
|
|
|
|
renderHeader = () => {
|
|
const {files} = this.props;
|
|
const {index} = this.state;
|
|
const header = this.getHeaderFooterStyle();
|
|
|
|
return (
|
|
<AnimatedView style={[style.headerContainer, {top: header.start, opacity: header.opacity}]}>
|
|
<View style={style.header}>
|
|
<View style={style.headerControls}>
|
|
<TouchableOpacity
|
|
onPress={this.close}
|
|
style={style.headerIcon}
|
|
>
|
|
<Icon
|
|
name='md-close'
|
|
size={26}
|
|
color='#fff'
|
|
/>
|
|
</TouchableOpacity>
|
|
<Text style={style.title}>
|
|
{`${index + 1}/${files.length}`}
|
|
</Text>
|
|
{this.renderDownloadButton()}
|
|
</View>
|
|
</View>
|
|
</AnimatedView>
|
|
);
|
|
};
|
|
|
|
renderImageComponent = (imageProps, imageDimensions) => {
|
|
if (imageDimensions) {
|
|
const {deviceHeight, deviceWidth} = this.props;
|
|
const {height, width} = imageDimensions;
|
|
const {style, source} = imageProps;
|
|
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
|
|
const flattenStyle = StyleSheet.flatten(style);
|
|
const calculatedDimensions = calculateDimensions(height, width, deviceWidth, deviceHeight - statusBar);
|
|
const imageStyle = {...flattenStyle, ...calculatedDimensions};
|
|
|
|
return (
|
|
<View style={[style, {justifyContent: 'center', alignItems: 'center'}]}>
|
|
<FastImage
|
|
source={source}
|
|
style={imageStyle}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
renderOtherItems = (index) => {
|
|
const {files} = this.props;
|
|
const file = files[index];
|
|
|
|
if (file.data) {
|
|
if (isDocument(file.data)) {
|
|
return this.renderAttachmentDocument(file);
|
|
} else if (isVideo(file.data)) {
|
|
return this.renderVideoPreview(file);
|
|
}
|
|
|
|
return this.renderAttachmentIcon(file.data);
|
|
}
|
|
|
|
return <View/>;
|
|
};
|
|
|
|
renderVideoPreview = (file) => {
|
|
const {deviceHeight, deviceWidth, theme} = this.props;
|
|
const statusBar = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 20;
|
|
|
|
return (
|
|
<VideoPreview
|
|
file={file}
|
|
onFullScreen={this.setHeaderAndFooterVisible}
|
|
deviceHeight={deviceHeight - statusBar}
|
|
deviceWidth={deviceWidth}
|
|
theme={theme}
|
|
/>
|
|
);
|
|
};
|
|
|
|
saveVideoIOS = () => {
|
|
const file = this.getCurrentFile();
|
|
|
|
if (this.downloaderRef) {
|
|
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
|
this.downloaderRef.saveVideo(getLocalFilePathFromFile(VIDEOS_PATH, file));
|
|
}
|
|
};
|
|
|
|
setHeaderAndFooterVisible = (show) => {
|
|
const toValue = show ? 1 : 0;
|
|
|
|
if (!show) {
|
|
this.hideDownloader();
|
|
}
|
|
|
|
this.setState({showHeaderFooter: show});
|
|
if (Platform.OS === 'ios') {
|
|
StatusBar.setHidden(!show, 'slide');
|
|
}
|
|
|
|
Animated.timing(this.headerFooterAnim, {
|
|
...ANIM_CONFIG,
|
|
toValue,
|
|
}).start();
|
|
};
|
|
|
|
showDownloader = () => {
|
|
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
|
|
|
|
this.setState({
|
|
showDownloader: true,
|
|
});
|
|
};
|
|
|
|
showDownloadOptions = () => {
|
|
if (Platform.OS === 'android') {
|
|
if (this.state.showDownloader) {
|
|
this.hideDownloader();
|
|
} else {
|
|
this.showDownloader();
|
|
}
|
|
} else {
|
|
this.showDownloadOptionsIOS();
|
|
}
|
|
};
|
|
|
|
showDownloadOptionsIOS = async () => {
|
|
const {formatMessage} = this.context.intl;
|
|
const file = this.getCurrentFile();
|
|
const options = [];
|
|
const actions = [];
|
|
let permissionRequest;
|
|
|
|
const photo = Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY;
|
|
const hasPermissionToStorage = await Permissions.check(photo);
|
|
|
|
switch (hasPermissionToStorage) {
|
|
case Permissions.RESULTS.DENIED:
|
|
permissionRequest = await Permissions.request(photo);
|
|
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
|
return;
|
|
}
|
|
break;
|
|
case Permissions.RESULTS.BLOCKED: {
|
|
const grantOption = {
|
|
text: formatMessage({id: 'mobile.permission_denied_retry', defaultMessage: 'Settings'}),
|
|
onPress: () => Permissions.openSettings(),
|
|
};
|
|
|
|
const applicationName = DeviceInfo.getApplicationName();
|
|
Alert.alert(
|
|
formatMessage({
|
|
id: 'mobile.photo_library_permission_denied_title',
|
|
defaultMessage: '{applicationName} would like to access your photo library',
|
|
}, {applicationName}),
|
|
formatMessage({
|
|
id: 'mobile.photo_library_permission_denied_description',
|
|
defaultMessage: 'To save images and videos to your library, please change your permission settings.',
|
|
}),
|
|
[
|
|
grantOption,
|
|
{text: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'})},
|
|
],
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (isVideo(file.data)) {
|
|
const path = getLocalFilePathFromFile(VIDEOS_PATH, file);
|
|
const exist = await RNFetchBlob.fs.exists(path);
|
|
if (exist) {
|
|
options.push(formatMessage({
|
|
id: t('mobile.image_preview.save_video'),
|
|
defaultMessage: 'Save Video',
|
|
}));
|
|
actions.push(this.saveVideoIOS);
|
|
} else {
|
|
this.showVideoDownloadRequiredAlertIOS();
|
|
}
|
|
} else {
|
|
options.push(formatMessage({
|
|
id: t('mobile.image_preview.save'),
|
|
defaultMessage: 'Save Image',
|
|
}));
|
|
actions.push(this.showDownloader);
|
|
}
|
|
|
|
if (options.length) {
|
|
options.push(formatMessage({
|
|
id: 'mobile.post.cancel',
|
|
defaultMessage: 'Cancel',
|
|
}));
|
|
actions.push(emptyFunction);
|
|
|
|
BottomSheet.showBottomSheetWithOptions({
|
|
options,
|
|
cancelButtonIndex: options.length - 1,
|
|
title: file.caption,
|
|
anchor: this.headerRef.current ? findNodeHandle(this.headerRef.current) : null,
|
|
}, (buttonIndex) => {
|
|
actions[buttonIndex]();
|
|
});
|
|
}
|
|
};
|
|
|
|
showVideoDownloadRequiredAlertIOS = () => {
|
|
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',
|
|
}),
|
|
}],
|
|
);
|
|
};
|
|
|
|
startOpenAnimation = () => {
|
|
this.animateOpenAnimToValue(1);
|
|
};
|
|
|
|
render() {
|
|
const opacity = this.getFullscreenOpacity();
|
|
|
|
return (
|
|
<AnimatedSafeAreaView style={[style.container, opacity]}>
|
|
<AnimatedView style={style.container}>
|
|
{this.renderGallery()}
|
|
{this.renderHeader()}
|
|
{this.renderFooter()}
|
|
</AnimatedView>
|
|
{this.renderDownloader()}
|
|
</AnimatedSafeAreaView>
|
|
);
|
|
}
|
|
}
|
|
|
|
const style = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#000',
|
|
},
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
center: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
fullWidth: {
|
|
width: '100%',
|
|
},
|
|
headerContainer: {
|
|
position: 'absolute',
|
|
height: HEADER_HEIGHT,
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
},
|
|
header: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
height: HEADER_HEIGHT,
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
},
|
|
headerControls: {
|
|
alignItems: 'center',
|
|
justifyContent: 'space-around',
|
|
flexDirection: 'row',
|
|
},
|
|
headerIcon: {
|
|
height: 44,
|
|
width: 48,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
title: {
|
|
flex: 1,
|
|
marginHorizontal: 10,
|
|
color: 'white',
|
|
fontSize: 15,
|
|
textAlign: 'center',
|
|
},
|
|
footerContainer: {
|
|
minHeight: 32,
|
|
maxHeight: 64,
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
position: 'absolute',
|
|
width: '100%',
|
|
},
|
|
footer: {
|
|
maxHeight: 64,
|
|
justifyContent: 'flex-end',
|
|
paddingHorizontal: 24,
|
|
paddingBottom: 0,
|
|
},
|
|
filename: {
|
|
color: 'white',
|
|
fontSize: 14,
|
|
marginBottom: 10,
|
|
},
|
|
});
|