[MM-9526] Preview text files (#1485)

* Add text preview screen

* Add text/plain mime in supported docs format

* Add previewTextFile method to view text file in TextPreview Screen

* Fix delay for ui to update progress

* Add styling for text preview screen

* Align line numbers on right

* Start read file for preview before delay
This commit is contained in:
Gajanan Patil 2018-03-14 20:01:06 +05:30 committed by Harrison Healey
parent c20a01a0a2
commit f1ea485ba0
6 changed files with 185 additions and 4 deletions

View file

@ -24,6 +24,7 @@ export default class FileAttachment extends PureComponent {
onInfoPress: PropTypes.func,
onPreviewPress: PropTypes.func,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
};
static defaultProps = {
@ -61,7 +62,7 @@ export default class FileAttachment extends PureComponent {
}
render() {
const {file, onInfoPress, theme} = this.props;
const {file, onInfoPress, theme, navigator} = this.props;
const style = getStyleSheet(theme);
let mime = file.mime_type;
@ -86,6 +87,7 @@ export default class FileAttachment extends PureComponent {
<FileAttachmentDocument
file={file}
theme={theme}
navigator={navigator}
/>
);
} else {

View file

@ -34,6 +34,7 @@ export const SUPPORTED_DOCS_FORMAT = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/xml',
'text/csv',
'text/plain',
];
export default class FileAttachmentDocument extends PureComponent {
@ -42,6 +43,7 @@ export default class FileAttachmentDocument extends PureComponent {
iconWidth: PropTypes.number,
file: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
wrapperHeight: PropTypes.number,
wrapperWidth: PropTypes.number,
};
@ -105,9 +107,15 @@ export default class FileAttachmentDocument extends PureComponent {
path,
};
const mime = file.mime_type.split(';')[0];
let openDocument = this.openDocument;
if (mime === 'text/plain') {
openDocument = this.previewTextFile;
}
const exist = await RNFetchBlob.fs.exists(path);
if (exist) {
this.openDocument(file, 0);
openDocument(file, 0);
} else {
this.setState({downloading: true});
this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(file.id));
@ -124,7 +132,7 @@ export default class FileAttachmentDocument extends PureComponent {
progress: 100,
}, () => {
// need to wait a bit for the progress circle UI to update to the give progress
this.openDocument(file);
openDocument(file);
});
}
}
@ -153,6 +161,36 @@ export default class FileAttachmentDocument extends PureComponent {
}
};
previewTextFile = (file, delay = 2000) => {
const {navigator, theme} = this.props;
const prefix = Platform.OS === 'android' ? 'file:/' : '';
const path = `${DOCUMENTS_PATH}/${file.name}`;
const readFile = RNFetchBlob.fs.readFile(`${prefix}${path}`, 'utf8');
setTimeout(async () => {
try {
const content = await readFile;
navigator.push({
screen: 'TextPreview',
title: file.name,
animated: true,
backButtonTitle: '',
passProps: {
content,
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
this.setState({downloading: false, progress: 0});
} catch (error) {
RNFetchBlob.fs.unlink(path);
}
}, delay);
};
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

View file

@ -84,7 +84,7 @@ export default class FileAttachmentList extends Component {
};
render() {
const {fileIds, files, isFailed} = this.props;
const {fileIds, files, isFailed, navigator} = this.props;
let fileAttachments;
if (!files.length && fileIds.length > 0) {
@ -106,6 +106,7 @@ export default class FileAttachmentList extends Component {
onPressOut={this.handlePressOut}
>
<FileAttachment
navigator={navigator}
addFileToFetchCache={this.props.actions.addFileToFetchCache}
fetchCache={this.props.fetchCache}
file={file}

View file

@ -22,6 +22,7 @@ import EditChannel from 'app/screens/edit_channel';
import EditPost from 'app/screens/edit_post';
import EditProfile from 'app/screens/edit_profile';
import ImagePreview from 'app/screens/image_preview';
import TextPreview from 'app/screens/text_preview';
import Login from 'app/screens/login';
import LoginOptions from 'app/screens/login_options';
import Mfa from 'app/screens/mfa';
@ -80,6 +81,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(EditProfile), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(TextPreview), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider);
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(LoginOptions), store, Provider);
Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider);

View file

@ -0,0 +1,16 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import TextPreview from './text_preview';
function mapStateToProps(state) {
return {
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(TextPreview);

View file

@ -0,0 +1,122 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import {getCodeFont} from 'app/utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
export default class TextPreview extends React.PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
content: PropTypes.string.isRequired,
};
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
}
countLines = (content) => {
return content.split('\n').length;
};
render() {
const style = getStyleSheet(this.props.theme);
const numberOfLines = this.countLines(this.props.content);
let lineNumbers = '1';
for (let i = 1; i < numberOfLines; i++) {
const line = (i + 1).toString();
lineNumbers += '\n' + line;
}
let lineNumbersStyle;
if (numberOfLines >= 10) {
lineNumbersStyle = [style.lineNumbers, style.lineNumbersRight];
} else {
lineNumbersStyle = style.lineNumbers;
}
return (
<ScrollView
style={style.scrollContainer}
contentContainerStyle={style.container}
>
<View style={lineNumbersStyle}>
<Text style={style.lineNumbersText}>
{lineNumbers}
</Text>
</View>
<ScrollView
style={style.codeContainer}
horizontal={true}
contentContainerStyle={style.code}
>
<Text
style={style.codeText}
selectable={true}
>
{this.props.content}
</Text>
</ScrollView>
</ScrollView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
scrollContainer: {
flex: 1,
},
container: {
minHeight: '100%',
flexDirection: 'row',
},
lineNumbers: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRightWidth: StyleSheet.hairlineWidth,
flexDirection: 'column',
justifyContent: 'flex-start',
paddingHorizontal: 6,
paddingVertical: 4,
},
lineNumbersRight: {
alignItems: 'flex-end',
},
lineNumbersText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 12,
lineHeight: 18,
textAlign: 'right',
},
codeContainer: {
flexGrow: 0,
flexShrink: 1,
width: '100%',
},
code: {
paddingHorizontal: 6,
paddingVertical: 4,
},
codeText: {
color: changeOpacity(theme.centerChannelColor, 0.65),
fontFamily: getCodeFont(),
fontSize: 12,
lineHeight: 18,
},
};
});