diff --git a/app/components/markdown/index.js b/app/components/markdown/index.js
index e015d57e6..9ed89ddf4 100644
--- a/app/components/markdown/index.js
+++ b/app/components/markdown/index.js
@@ -201,15 +201,15 @@ export default class Markdown extends PureComponent {
renderCodeBlock = (props) => {
// These sometimes include a trailing newline
- const contents = props.literal.replace(/\n$/, '');
+ const content = props.literal.replace(/\n$/, '');
return (
- {contents}
-
+ navigator={this.props.navigator}
+ content={content}
+ language={props.language}
+ textStyle={this.props.textStyles.codeBlock}
+ />
);
}
diff --git a/app/components/markdown/markdown_code_block.js b/app/components/markdown/markdown_code_block.js
deleted file mode 100644
index e5ea95ef1..000000000
--- a/app/components/markdown/markdown_code_block.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import React, {PureComponent} from 'react';
-import {ScrollView, Text} from 'react-native';
-
-import CustomPropTypes from 'app/constants/custom_prop_types';
-
-export default class MarkdownCodeBlock extends PureComponent {
- static propTypes = {
- children: CustomPropTypes.Children,
- blockStyle: CustomPropTypes.Style,
- textStyle: CustomPropTypes.Style
- };
-
- render() {
- return (
-
-
- {this.props.children}
-
-
- );
- }
-}
diff --git a/app/components/markdown/markdown_code_block/index.js b/app/components/markdown/markdown_code_block/index.js
new file mode 100644
index 000000000..d71e7bc8a
--- /dev/null
+++ b/app/components/markdown/markdown_code_block/index.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from 'app/selectors/preferences';
+
+import MarkdownCodeBlock from './markdown_code_block';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ theme: getTheme(state),
+ ...ownProps
+ };
+}
+
+export default connect(mapStateToProps)(MarkdownCodeBlock);
diff --git a/app/components/markdown/markdown_code_block/markdown_code_block.js b/app/components/markdown/markdown_code_block/markdown_code_block.js
new file mode 100644
index 000000000..17fc7946d
--- /dev/null
+++ b/app/components/markdown/markdown_code_block/markdown_code_block.js
@@ -0,0 +1,215 @@
+// 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 {injectIntl, intlShape} from 'react-intl';
+import {
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+import FormattedText from 'app/components/formatted_text';
+import {getDisplayNameForLanguage} from 'app/utils/markdown';
+import {wrapWithPreventDoubleTap} from 'app/utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+const MAX_LINES = 4;
+
+class MarkdownCodeBlock extends React.PureComponent {
+ static propTypes = {
+ intl: intlShape.isRequired,
+ navigator: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired,
+ language: PropTypes.string,
+ content: PropTypes.string.isRequired,
+ textStyle: CustomPropTypes.Style
+ };
+
+ static defaultProps = {
+ language: ''
+ };
+
+ handlePress = wrapWithPreventDoubleTap(() => {
+ const {intl, navigator, theme} = this.props;
+
+ const languageDisplayName = getDisplayNameForLanguage(this.props.language);
+ let title;
+ if (languageDisplayName) {
+ title = intl.formatMessage(
+ {
+ id: 'mobile.routes.code',
+ defaultMessage: '{language} Code'
+ },
+ {
+ language: languageDisplayName
+ }
+ );
+ } else {
+ title = intl.formatMessage({
+ id: 'mobile.routes.code.noLanguage',
+ defaultMessage: 'Code'
+ });
+ }
+
+ navigator.push({
+ screen: 'Code',
+ title,
+ animated: true,
+ backButtonTitle: '',
+ passProps: {
+ content: this.props.content
+ },
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg
+ }
+ });
+ });
+
+ trimContent = (content) => {
+ const lines = content.split('\n');
+ const numberOfLines = lines.length;
+
+ if (numberOfLines > MAX_LINES) {
+ return {
+ content: lines.slice(0, MAX_LINES).join('\n'),
+ numberOfLines
+ };
+ }
+
+ return {
+ content,
+ numberOfLines
+ };
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ let language = null;
+ if (this.props.language) {
+ const languageDisplayName = getDisplayNameForLanguage(this.props.language);
+
+ if (languageDisplayName) {
+ language = (
+
+
+ {languageDisplayName}
+
+
+ );
+ }
+ }
+
+ const {content, numberOfLines} = this.trimContent(this.props.content);
+
+ let lineNumbers = '1';
+ for (let i = 1; i < Math.min(numberOfLines, MAX_LINES); i++) {
+ const line = (i + 1).toString();
+
+ lineNumbers += '\n' + line;
+ }
+
+ let plusMoreLines = null;
+ if (numberOfLines > MAX_LINES) {
+ plusMoreLines = (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {lineNumbers}
+
+
+
+
+
+ {content}
+
+
+ {plusMoreLines}
+
+ {language}
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.15),
+ borderRadius: 3,
+ borderWidth: StyleSheet.hairlineWidth,
+ 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',
+ paddingVertical: 4,
+ width: 21
+ },
+ lineNumbersText: {
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ fontSize: 12,
+ lineHeight: 18
+ },
+ rightColumn: {
+ flexDirection: 'column',
+ flex: 1,
+ paddingHorizontal: 6,
+ paddingVertical: 4
+ },
+ code: {
+ flexDirection: 'row',
+ overflow: 'scroll' // Doesn't actually cause a scrollbar, but stops text from wrapping
+ },
+ codeText: {
+ fontSize: 12,
+ lineHeight: 18
+ },
+ plusMoreLinesText: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 11,
+ marginTop: 2
+ },
+ language: {
+ alignItems: 'center',
+ backgroundColor: theme.sidebarHeaderBg,
+ justifyContent: 'center',
+ opacity: 0.8,
+ padding: 6,
+ position: 'absolute',
+ right: 0,
+ top: 0
+ },
+ languageText: {
+ color: theme.sidebarHeaderTextColor,
+ fontSize: 12
+ }
+ };
+});
+
+export default injectIntl(MarkdownCodeBlock);
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index 3972e49fe..280b53565 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -43,7 +43,8 @@ export default class PostList extends PureComponent {
};
static defaultProps = {
- channel: {}
+ channel: {},
+ channelIsLoading: false
};
getPostsWithDates = () => {
diff --git a/app/screens/code/code.js b/app/screens/code/code.js
new file mode 100644
index 000000000..b8d73ae37
--- /dev/null
+++ b/app/screens/code/code.js
@@ -0,0 +1,115 @@
+// 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 {
+ Dimensions,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View
+} from 'react-native';
+
+import {getCodeFont} from 'app/utils/markdown';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+const {
+ width: deviceWidth
+} = Dimensions.get('window');
+
+export default class Code extends React.PureComponent {
+ static propTypes = {
+ theme: PropTypes.object.isRequired,
+ content: PropTypes.string.isRequired
+ };
+
+ 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
+ },
+ codeContainer: {
+ flexGrow: 0,
+ flexShrink: 1,
+ width: deviceWidth
+ },
+ code: {
+ paddingHorizontal: 6,
+ paddingVertical: 4
+ },
+ codeText: {
+ fontFamily: getCodeFont(),
+ fontSize: 12,
+ lineHeight: 18
+ }
+ };
+});
diff --git a/app/screens/code/index.js b/app/screens/code/index.js
new file mode 100644
index 000000000..900b136bb
--- /dev/null
+++ b/app/screens/code/index.js
@@ -0,0 +1,25 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {getTheme} from 'app/selectors/preferences';
+
+import Code from './code';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ theme: getTheme(state),
+ ...ownProps
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Code);
diff --git a/app/screens/index.js b/app/screens/index.js
index 63c64a151..0f486d86e 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -12,6 +12,7 @@ import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
import ChannelMembers from 'app/screens/channel_members';
+import Code from 'app/screens/code';
import CreateChannel from 'app/screens/create_channel';
import EditPost from 'app/screens/edit_post';
import ImagePreview from 'app/screens/image_preview';
@@ -57,6 +58,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
+ Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider);
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
diff --git a/app/utils/markdown.js b/app/utils/markdown.js
index e27d60e00..69ff1f71c 100644
--- a/app/utils/markdown.js
+++ b/app/utils/markdown.js
@@ -4,8 +4,12 @@
import {Platform, StyleSheet} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+export function getCodeFont() {
+ return Platform.OS === 'ios' ? 'Menlo' : 'monospace';
+}
+
export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
- const codeFont = Platform.OS === 'ios' ? 'Menlo' : 'monospace';
+ const codeFont = getCodeFont();
return {
emph: {
@@ -75,12 +79,6 @@ export const getMarkdownBlockStyles = makeStyleSheetFromTheme((theme) => {
adjacentParagraph: {
marginTop: 6
},
- codeBlock: {
- backgroundColor: changeOpacity(theme.centerChannelColor, 0.07),
- borderRadius: 4,
- paddingHorizontal: 4,
- paddingVertical: 2
- },
horizontalRule: {
backgroundColor: theme.centerChannelColor,
height: StyleSheet.hairlineWidth,
@@ -94,7 +92,67 @@ export const getMarkdownBlockStyles = makeStyleSheetFromTheme((theme) => {
};
});
-export default {
- getMarkdownBlockStyles,
- getMarkdownTextStyles
+const languages = {
+ actionscript: 'ActionScript',
+ applescript: 'AppleScript',
+ bash: 'Bash',
+ clojure: 'Clojure',
+ coffeescript: 'CoffeeScript',
+ cpp: 'C++',
+ cs: 'C#',
+ css: 'CSS',
+ d: 'D',
+ dart: 'Dart',
+ delphi: 'Delphi',
+ diff: 'Diff',
+ django: 'Django',
+ dockerfile: 'Dockerfile',
+ erlang: 'Erlang',
+ fortran: 'Fortran',
+ fsharp: 'F#',
+ gcode: 'G-code',
+ go: 'Go',
+ groovy: 'Groovy',
+ handlebars: 'Handlebars',
+ haskell: 'Haskell',
+ haxe: 'Haxe',
+ html: 'HTML',
+ java: 'Java',
+ javascript: 'JavaScript',
+ json: 'JSON',
+ julia: 'Julia',
+ kotlin: 'Kotlin',
+ latex: 'LaTeX',
+ less: 'Less',
+ lisp: 'Lisp',
+ lua: 'Lua',
+ makefile: 'Makefile',
+ markdown: 'Markdown',
+ matlab: 'Matlab',
+ objectivec: 'Objective-C',
+ ocaml: 'OCaml',
+ perl: 'Perl',
+ php: 'PHP',
+ powershell: 'PowerShell',
+ puppet: 'Puppet',
+ python: 'Python',
+ r: 'R',
+ ruby: 'Ruby',
+ rust: 'Rust',
+ scala: 'Scala',
+ scheme: 'Scheme',
+ scss: 'SCSS',
+ smalltalk: 'Smalltalk',
+ sql: 'SQL',
+ swift: 'Swift',
+ tex: 'TeX',
+ vbnet: 'VB.NET',
+ vbscript: 'VBScript',
+ verilog: 'Verilog',
+ xml: 'XML',
+ yaml: 'YAML'
};
+
+export function getDisplayNameForLanguage(language) {
+ return languages[language.toLowerCase()] || '';
+}
diff --git a/app/utils/tap.js b/app/utils/tap.js
index e87255f62..d8df863df 100644
--- a/app/utils/tap.js
+++ b/app/utils/tap.js
@@ -1,6 +1,8 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+const doublePressDelay = 300;
+
let canPress = true;
export function preventDoubleTap(action, thisArg, ...args) {
if (canPress) {
@@ -9,6 +11,21 @@ export function preventDoubleTap(action, thisArg, ...args) {
setTimeout(() => {
canPress = true;
- }, 300);
+ }, doublePressDelay);
}
}
+
+export function wrapWithPreventDoubleTap(func) {
+ let canPressWrapped = true;
+
+ return (...args) => {
+ if (canPressWrapped) {
+ canPressWrapped = false;
+ func(...args);
+
+ setTimeout(() => {
+ canPressWrapped = true;
+ }, doublePressDelay);
+ }
+ };
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index de26667a7..ecae5a3df 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1849,6 +1849,7 @@
"mobile.managed.exit": "Exit",
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.secured_by": "Secured by {vendor}",
+ "mobile.markdown.code.plusMoreLines": "+{count, number} more lines",
"mobile.more_dms.start": "Start",
"mobile.more_dms.title": "New Conversation",
"mobile.notice_mobile_link": "mobile apps",
@@ -1876,6 +1877,8 @@
"mobile.routes.channel_members.action_message": "You must select at least one member to remove from the channel.",
"mobile.routes.channel_members.action_message_confirm": "Are you sure you want to remove the selected members from the channel?",
"mobile.routes.channels": "Channels",
+ "mobile.routes.code": "{language} Code",
+ "mobile.routes.code.noLanguage": "Code",
"mobile.routes.enterServerUrl": "Enter Server URL",
"mobile.routes.login": "Login",
"mobile.routes.loginOptions": "Login Chooser",