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,
+ },
+ };
+});