diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
new file mode 100644
index 000000000..34994e390
--- /dev/null
+++ b/app/components/markdown/markdown.js
@@ -0,0 +1,195 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {Parser} from 'commonmark';
+import Renderer from 'commonmark-react-renderer';
+import React, {PropTypes, PureComponent} from 'react';
+import {
+ StyleSheet,
+ Text,
+ View
+} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+import {concatStyles} from 'app/utils/theme';
+
+import MarkdownBlockQuote from './markdown_block_quote';
+import MarkdownCodeBlock from './markdown_code_block';
+import MarkdownLink from './markdown_link';
+import MarkdownList from './markdown_list';
+import MarkdownListItem from './markdown_list_item';
+
+export default class Markdown extends PureComponent {
+ static propTypes = {
+ baseTextStyle: CustomPropTypes.Style,
+ textStyles: PropTypes.object,
+ blockStyles: PropTypes.object,
+ value: PropTypes.string.isRequired
+ };
+
+ static defaultProps = {
+ textStyles: {},
+ blockStyles: {}
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.parser = new Parser();
+ this.renderer = this.createRenderer();
+ }
+
+ createRenderer = () => {
+ return new Renderer({
+ renderers: {
+ text: this.renderText,
+
+ emph: Renderer.forwardChildren,
+ strong: Renderer.forwardChildren,
+ code: this.renderCodeSpan,
+ link: MarkdownLink,
+ image: this.renderImage,
+
+ paragraph: this.renderParagraph,
+ heading: this.renderHeading,
+ codeBlock: this.renderCodeBlock,
+ blockQuote: this.renderBlockQuote,
+
+ list: this.renderList,
+ item: this.renderListItem,
+
+ hardBreak: this.renderHardBreak,
+ thematicBreak: this.renderThematicBreak,
+ softBreak: this.renderSoftBreak,
+
+ htmlBlock: this.renderHtml,
+ htmlInline: this.renderHtml
+ },
+ renderParagraphsInLists: true
+ });
+ }
+
+ computeTextStyle = (baseStyle, context) => {
+ return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type]));
+ }
+
+ renderText = ({context, literal}) => {
+ // Construct the text style based off of the parents of this node since RN's inheritance is limited
+ return {literal};
+ }
+
+ renderCodeSpan = ({context, literal}) => {
+ return {literal};
+ }
+
+ renderImage = ({children, context, src}) => {
+ // TODO This will be properly implemented for PLT-5736
+ return (
+
+ {''}
+
+ );
+ }
+
+ renderParagraph = ({children}) => {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ renderHeading = ({children, level}) => {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ renderCodeBlock = (props) => {
+ // These sometimes include a trailing newline
+ const contents = props.literal.replace(/\n$/, '');
+
+ return (
+
+ {contents}
+
+ );
+ }
+
+ renderBlockQuote = ({children, ...otherProps}) => {
+ return (
+
+ {children}
+
+ );
+ }
+
+ renderList = ({children, start, tight, type}) => {
+ return (
+
+ {children}
+
+ );
+ }
+
+ renderListItem = ({children, ...otherProps}) => {
+ return (
+
+ {children}
+
+ );
+ }
+
+ renderHardBreak = () => {
+ return ;
+ }
+
+ renderThematicBreak = () => {
+ return ;
+ }
+
+ renderSoftBreak = () => {
+ return {'\n'};
+ }
+
+ renderHtml = () => {
+ return null;
+ }
+
+ render() {
+ const ast = this.parser.parse(this.props.value);
+
+ return {this.renderer.render(ast)};
+ }
+}
+
+const style = StyleSheet.create({
+ block: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ flexWrap: 'wrap'
+ }
+});
diff --git a/app/components/markdown/markdown_block_quote.js b/app/components/markdown/markdown_block_quote.js
new file mode 100644
index 000000000..7856972ce
--- /dev/null
+++ b/app/components/markdown/markdown_block_quote.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import {
+ StyleSheet,
+ Text,
+ View
+} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+
+export default class MarkdownBlockQuote extends PureComponent {
+ static propTypes = {
+ blockStyle: CustomPropTypes.Style,
+ bulletStyle: CustomPropTypes.Style,
+ children: CustomPropTypes.Children.isRequired
+ };
+
+ render() {
+ return (
+
+
+
+ {'> '}
+
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'flex-start'
+ }
+});
diff --git a/app/components/markdown/markdown_code_block.js b/app/components/markdown/markdown_code_block.js
new file mode 100644
index 000000000..55e5702f7
--- /dev/null
+++ b/app/components/markdown/markdown_code_block.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 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_image.js b/app/components/markdown/markdown_image.js
new file mode 100644
index 000000000..b9c0895ab
--- /dev/null
+++ b/app/components/markdown/markdown_image.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {Image} from 'react-native';
+
+export default class MarkdownLink extends PureComponent {
+ static propTypes = {
+ src: PropTypes.string.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ width: 10000,
+ maxWidth: 10000,
+ height: 0
+ };
+ }
+
+ componentWillMount() {
+ Image.getSize(this.props.src, this.handleSizeReceived, this.handleSizeFailed);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.src !== nextProps.src) {
+ Image.getSize(nextProps.src, this.handleSizeReceived, this.handleSizeFailed);
+ }
+ }
+
+ handleSizeReceived = (width, height) => {
+ this.setState({
+ width,
+ height
+ });
+ };
+
+ handleSizeFailed = () => {
+ this.setState({
+ width: 0,
+ height: 0
+ });
+ }
+
+ handleLayout = (event) => {
+ this.setState({
+ maxWidth: event.nativeEvent.layout.width
+ });
+ }
+
+ render() {
+ let {width, maxWidth, height} = this.state; // eslint-disable-line prefer-const
+
+ if (width > maxWidth) {
+ height = height * (maxWidth / width);
+ width = maxWidth;
+ }
+
+ // React Native complains if we try to pass resizeMode into a StyleSheet
+ return (
+
+ );
+ }
+}
diff --git a/app/components/markdown/markdown_link.js b/app/components/markdown/markdown_link.js
new file mode 100644
index 000000000..c6f751b2b
--- /dev/null
+++ b/app/components/markdown/markdown_link.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {Linking, Text} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+
+export default class MarkdownLink extends PureComponent {
+ static propTypes = {
+ children: CustomPropTypes.Children.isRequired,
+ href: PropTypes.string.isRequired
+ };
+
+ handlePress = () => {
+ const url = this.props.href;
+
+ Linking.canOpenURL(url).then((supported) => {
+ if (supported) {
+ Linking.openURL(url);
+ }
+ });
+ };
+
+ render() {
+ return {this.props.children};
+ }
+}
diff --git a/app/components/markdown/markdown_list.js b/app/components/markdown/markdown_list.js
new file mode 100644
index 000000000..4ea3fe346
--- /dev/null
+++ b/app/components/markdown/markdown_list.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {View} from 'react-native';
+
+export default class MarkdownList extends PureComponent {
+ static propTypes = {
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]).isRequired,
+ ordered: PropTypes.bool.isRequired,
+ startAt: PropTypes.number,
+ tight: PropTypes.bool
+ };
+
+ render() {
+ const children = React.Children.map(this.props.children, (child, i) => {
+ return React.cloneElement(child, {
+ ordered: this.props.ordered,
+ startAt: this.props.startAt,
+ index: i,
+ tight: this.props.tight
+ });
+ });
+
+ return (
+
+ {children}
+
+ );
+ }
+}
diff --git a/app/components/markdown/markdown_list_item.js b/app/components/markdown/markdown_list_item.js
new file mode 100644
index 000000000..19bb442f8
--- /dev/null
+++ b/app/components/markdown/markdown_list_item.js
@@ -0,0 +1,55 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {
+ StyleSheet,
+ Text,
+ View
+} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+
+export default class MarkdownListItem extends PureComponent {
+ static propTypes = {
+ children: CustomPropTypes.Children.isRequired,
+ ordered: PropTypes.bool.isRequired,
+ startAt: PropTypes.number,
+ index: PropTypes.number.isRequired,
+ tight: PropTypes.bool,
+ bulletStyle: CustomPropTypes.Style
+ };
+
+ static defaultProps = {
+ startAt: 1
+ };
+
+ render() {
+ let bullet;
+ if (this.props.ordered) {
+ bullet = (this.props.startAt + this.props.index) + '. ';
+ } else {
+ bullet = '• ';
+ }
+
+ return (
+
+
+
+ {bullet}
+
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'flex-start'
+ }
+});
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 87411074a..56a77516e 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -3,8 +3,8 @@
import React, {Component, PropTypes} from 'react';
import {
- Platform,
Image,
+ Platform,
StyleSheet,
Text,
TouchableHighlight,
@@ -15,9 +15,10 @@ import {
import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import MattermostIcon from 'app/components/mattermost_icon';
+import Markdown from 'app/components/markdown/markdown';
import ProfilePicture from 'app/components/profile_picture';
import FileAttachmentList from 'app/components/file_attachment_list/file_attachment_list_container';
-import {makeStyleSheetFromTheme} from 'app/utils/theme';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {isSystemMessage} from 'mattermost-redux/utils/post_utils.js';
@@ -117,15 +118,25 @@ export default class Post extends Component {
};
renderMessage = (style, messageStyle, replyBar = false) => {
+ let contents;
+ if (this.props.post.message.length > 0) {
+ const theme = this.props.theme;
+
+ contents = (
+
+ );
+ }
+
return (
{replyBar && this.renderReplyBar(style)}
- {this.props.post.message.length > 0 &&
-
- {this.props.post.message}
-
- }
+ {contents}
{this.renderFileAttachments()}
@@ -133,7 +144,9 @@ export default class Post extends Component {
};
render() {
- const style = getStyleSheet(this.props.theme);
+ const theme = this.props.theme;
+ const style = getStyleSheet(theme);
+
const PROFILE_PICTURE_SIZE = 32;
let profilePicture;
@@ -178,7 +191,8 @@ export default class Post extends Component {
{this.props.post.props.override_username}
);
- messageStyle = [style.message, style.webhookMessage];
+
+ messageStyle = style.message;
} else {
profilePicture = (
@@ -323,16 +337,81 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
message: {
color: theme.centerChannelColor,
fontSize: 14,
- lineHeight: 21,
- marginBottom: 10
+ lineHeight: 21
},
systemMessage: {
opacity: 0.5
- },
- webhookMessage: {
- color: theme.centerChannelColor,
- fontSize: 16,
- fontWeight: '600'
+ }
+ });
+});
+
+const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
+ const codeFont = Platform.OS === 'ios' ? 'Courier New' : 'monospace';
+
+ return StyleSheet.create({
+ emph: {
+ fontStyle: 'italic'
+ },
+ strong: {
+ fontWeight: 'bold'
+ },
+ link: {
+ color: theme.linkColor
+ },
+ heading1: {
+ fontSize: 30,
+ lineHeight: 45
+ },
+ heading2: {
+ fontSize: 24,
+ lineHeight: 36
+ },
+ heading3: {
+ fontSize: 20,
+ lineHeight: 30
+ },
+ heading4: {
+ fontSize: 16,
+ lineHeight: 24
+ },
+ heading5: {
+ fontSize: 14,
+ lineHeight: 21
+ },
+ heading6: {
+ fontSize: 14,
+ lineHeight: 21,
+ opacity: 0.8
+ },
+ code: {
+ alignSelf: 'center',
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ fontFamily: codeFont,
+ paddingHorizontal: 4,
+ paddingVertical: 2
+ },
+ codeBlock: {
+ fontFamily: codeFont
+ },
+ horizontalRule: {
+ backgroundColor: theme.centerChannelColor,
+ height: StyleSheet.hairlineWidth,
+ flex: 1,
+ marginVertical: 10
+ }
+ });
+});
+
+const getMarkdownBlockStyles = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ codeBlock: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ borderRadius: 4,
+ paddingHorizontal: 4,
+ paddingVertical: 2
+ },
+ horizontalRule: {
+ backgroundColor: theme.centerChannelColor
}
});
});
diff --git a/app/constants/custom_prop_types.js b/app/constants/custom_prop_types.js
new file mode 100644
index 000000000..fdf0e72a6
--- /dev/null
+++ b/app/constants/custom_prop_types.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {PropTypes} from 'react';
+
+export const Children = PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]);
+
+export const Style = PropTypes.oneOfType([
+ PropTypes.object, // inline style
+ PropTypes.number, // style sheet entry
+ PropTypes.array
+]);
+
+export default {
+ Children,
+ Style
+};
diff --git a/app/utils/theme.js b/app/utils/theme.js
index f46af4f18..5852c570e 100644
--- a/app/utils/theme.js
+++ b/app/utils/theme.js
@@ -36,3 +36,7 @@ export function changeOpacity(oldColor, opacity) {
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
}
+
+export function concatStyles(...styles) {
+ return [].concat(styles);
+}
diff --git a/package.json b/package.json
index a086868fb..2e6f827d9 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,8 @@
"version": "0.0.1",
"private": true,
"dependencies": {
+ "commonmark": "0.27.0",
+ "commonmark-react-renderer": "hmhealey/commonmark-react-renderer#2e8ea6ac67b683b44782f403b69a06bb0e5c63e6",
"deep-equal": "1.0.1",
"harmony-reflect": "1.5.1",
"intl": "1.2.5",