From f1ea485ba0badfceec1a739bd954d5992a20b479 Mon Sep 17 00:00:00 2001 From: Gajanan Patil Date: Wed, 14 Mar 2018 20:01:06 +0530 Subject: [PATCH] [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 --- .../file_attachment_list/file_attachment.js | 4 +- .../file_attachment_document.js | 42 +++++- .../file_attachment_list.js | 3 +- app/screens/index.js | 2 + app/screens/text_preview/index.js | 16 +++ app/screens/text_preview/text_preview.js | 122 ++++++++++++++++++ 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 app/screens/text_preview/index.js create mode 100644 app/screens/text_preview/text_preview.js diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js index fba313e81..4723d1f86 100644 --- a/app/components/file_attachment_list/file_attachment.js +++ b/app/components/file_attachment_list/file_attachment.js @@ -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 { ); } else { diff --git a/app/components/file_attachment_list/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document.js index 1d0edf1e1..ffdedb3fd 100644 --- a/app/components/file_attachment_list/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document.js @@ -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 diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index f59f85653..86aca8458 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -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} > 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); diff --git a/app/screens/text_preview/index.js b/app/screens/text_preview/index.js new file mode 100644 index 000000000..94826c89a --- /dev/null +++ b/app/screens/text_preview/index.js @@ -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); diff --git a/app/screens/text_preview/text_preview.js b/app/screens/text_preview/text_preview.js new file mode 100644 index 000000000..600e27ae3 --- /dev/null +++ b/app/screens/text_preview/text_preview.js @@ -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 ( + + + + {lineNumbers} + + + + + {this.props.content} + + + + ); + } +} + +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, + }, + }; +});