diff --git a/app/components/markdown/index.js b/app/components/markdown/index.js index d4d8e33f2..dcbd63e96 100644 --- a/app/components/markdown/index.js +++ b/app/components/markdown/index.js @@ -1,411 +1,16 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {Parser, Node} from 'commonmark'; -import Renderer from 'commonmark-react-renderer'; -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import { - Platform, - Text, - View -} from 'react-native'; +import {connect} from 'react-redux'; -import AtMention from 'app/components/at_mention'; -import ChannelLink from 'app/components/channel_link'; -import Emoji from 'app/components/emoji'; -import FormattedText from 'app/components/formatted_text'; -import CustomPropTypes from 'app/constants/custom_prop_types'; -import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import MarkdownBlockQuote from './markdown_block_quote'; -import MarkdownCodeBlock from './markdown_code_block'; -import MarkdownImage from './markdown_image'; -import MarkdownLink from './markdown_link'; -import MarkdownList from './markdown_list'; -import MarkdownListItem from './markdown_list_item'; -import {addListItemIndices, pullOutImages} from './transform'; +import Markdown from './markdown'; -export default class Markdown extends PureComponent { - static propTypes = { - baseTextStyle: CustomPropTypes.Style, - blockStyles: PropTypes.object, - emojiSizes: PropTypes.object, - fontSizes: PropTypes.object, - isEdited: PropTypes.bool, - isSearchResult: PropTypes.bool, - navigator: PropTypes.object.isRequired, - onLongPress: PropTypes.func, - onPostPress: PropTypes.func, - textStyles: PropTypes.object, - theme: PropTypes.object.isRequired, - value: PropTypes.string.isRequired +function mapStateToProps(state) { + return { + theme: getTheme(state) }; - - static defaultProps = { - textStyles: {}, - blockStyles: {}, - emojiSizes: { - ...Platform.select({ - ios: { - heading1: 25, - heading2: 25, - heading3: 25, - heading4: 25, - heading5: 25, - heading6: 25, - text: 20 - }, - android: { - heading1: 60, - heading2: 60, - heading3: 60, - heading4: 60, - heading5: 60, - heading6: 60, - text: 45 - } - }) - }, - fontSizes: { - heading1: 17, - heading2: 17, - heading3: 17, - heading4: 17, - heading5: 17, - heading6: 17, - text: 15 - }, - onLongPress: () => true - }; - - 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, - del: Renderer.forwardChildren, - code: this.renderCodeSpan, - link: this.renderLink, - image: this.renderImage, - atMention: this.renderAtMention, - channelLink: this.renderChannelLink, - emoji: this.renderEmoji, - - 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, - - editedIndicator: this.renderEditedIndicator - }, - renderParagraphsInLists: true, - getExtraPropsForNode: this.getExtraPropsForNode - }); - } - - getExtraPropsForNode = (node) => { - const extraProps = { - continue: node.continue, - index: node.index - }; - - if (node.type === 'image') { - extraProps.reactChildren = node.react.children; - extraProps.linkDestination = node.linkDestination; - } - - return extraProps; - } - - computeTextStyle = (baseStyle, context) => { - return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type])); - } - - renderText = ({context, literal}) => { - if (context.indexOf('image') !== -1) { - // If this text is displayed, it will be styled by the image component - return {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 = ({linkDestination, reactChildren, context, src}) => { - return ( - - {reactChildren} - - ); - } - - renderAtMention = ({context, mentionName}) => { - return ( - - ); - } - - renderChannelLink = ({context, channelName}) => { - return ( - - ); - } - - renderEmoji = ({context, emojiName, literal}) => { - let size; - let fontSize; - const headingType = context.find((type) => type.startsWith('heading')); - if (headingType) { - size = this.props.emojiSizes[headingType]; - fontSize = this.props.fontSizes[headingType]; - } else { - size = this.props.emojiSizes.text; - fontSize = this.props.fontSizes.text; - } - - return ( - - ); - } - - renderParagraph = ({children, first}) => { - if (!children || children.length === 0) { - return null; - } - - const style = getStyleSheet(this.props.theme); - const blockStyle = [style.block]; - if (!first) { - blockStyle.push(this.props.blockStyles.adjacentParagraph); - } - - return ( - - - {children} - - - ); - } - - renderHeading = ({children, level}) => { - const style = getStyleSheet(this.props.theme); - - return ( - - - {children} - - - ); - } - - renderCodeBlock = (props) => { - // These sometimes include a trailing newline - const content = props.literal.replace(/\n$/, ''); - - return ( - - ); - } - - renderBlockQuote = ({children, ...otherProps}) => { - return ( - - {children} - - ); - } - - renderList = ({children, tight, type}) => { - return ( - - {children} - - ); - } - - renderListItem = ({children, context, ...otherProps}) => { - const level = context.filter((type) => type === 'list').length; - - return ( - - {children} - - ); - } - - renderHardBreak = () => { - return {'\n'}; - } - - renderThematicBreak = () => { - return ; - } - - renderSoftBreak = () => { - return {'\n'}; - } - - renderHtml = (props) => { - let rendered = this.renderText(props); - - if (props.isBlock) { - const style = getStyleSheet(this.props.theme); - - rendered = ( - - {rendered} - - ); - } - - return rendered; - } - - renderLink = ({children, href}) => { - return ( - - {children} - - ); - } - - renderEditedIndicator = ({context}) => { - let spacer = ''; - if (context[0] === 'paragraph') { - spacer = ' '; - } - - const style = getStyleSheet(this.props.theme); - const styles = [ - this.props.baseTextStyle, - style.editedIndicatorText - ]; - - return ( - - {spacer} - - - ); - } - - render() { - let ast = this.parser.parse(this.props.value); - - ast = addListItemIndices(ast); - ast = pullOutImages(ast); - - if (this.props.isEdited) { - const editIndicatorNode = new Node('edited_indicator'); - if (['heading', 'paragraph'].includes(ast.lastChild.type)) { - ast.lastChild.appendChild(editIndicatorNode); - } else { - const node = new Node('paragraph'); - node.appendChild(editIndicatorNode); - - ast.appendChild(node); - } - } - - return {this.renderer.render(ast)}; - } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - // Android has trouble giving text transparency depending on how it's nested, - // so we calculate the resulting colour manually - const editedOpacity = Platform.select({ - ios: 0.3, - android: 1.0 - }); - const editedColor = Platform.select({ - ios: theme.centerChannelColor, - android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3) - }); - - return { - block: { - alignItems: 'flex-start', - flexDirection: 'row', - flexWrap: 'wrap' - }, - editedIndicatorText: { - color: editedColor, - fontSize: 14, - opacity: editedOpacity - } - }; -}); +export default connect(mapStateToProps)(Markdown); diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js new file mode 100644 index 000000000..d4d8e33f2 --- /dev/null +++ b/app/components/markdown/markdown.js @@ -0,0 +1,411 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Parser, Node} from 'commonmark'; +import Renderer from 'commonmark-react-renderer'; +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + Text, + View +} from 'react-native'; + +import AtMention from 'app/components/at_mention'; +import ChannelLink from 'app/components/channel_link'; +import Emoji from 'app/components/emoji'; +import FormattedText from 'app/components/formatted_text'; +import CustomPropTypes from 'app/constants/custom_prop_types'; +import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import MarkdownBlockQuote from './markdown_block_quote'; +import MarkdownCodeBlock from './markdown_code_block'; +import MarkdownImage from './markdown_image'; +import MarkdownLink from './markdown_link'; +import MarkdownList from './markdown_list'; +import MarkdownListItem from './markdown_list_item'; +import {addListItemIndices, pullOutImages} from './transform'; + +export default class Markdown extends PureComponent { + static propTypes = { + baseTextStyle: CustomPropTypes.Style, + blockStyles: PropTypes.object, + emojiSizes: PropTypes.object, + fontSizes: PropTypes.object, + isEdited: PropTypes.bool, + isSearchResult: PropTypes.bool, + navigator: PropTypes.object.isRequired, + onLongPress: PropTypes.func, + onPostPress: PropTypes.func, + textStyles: PropTypes.object, + theme: PropTypes.object.isRequired, + value: PropTypes.string.isRequired + }; + + static defaultProps = { + textStyles: {}, + blockStyles: {}, + emojiSizes: { + ...Platform.select({ + ios: { + heading1: 25, + heading2: 25, + heading3: 25, + heading4: 25, + heading5: 25, + heading6: 25, + text: 20 + }, + android: { + heading1: 60, + heading2: 60, + heading3: 60, + heading4: 60, + heading5: 60, + heading6: 60, + text: 45 + } + }) + }, + fontSizes: { + heading1: 17, + heading2: 17, + heading3: 17, + heading4: 17, + heading5: 17, + heading6: 17, + text: 15 + }, + onLongPress: () => true + }; + + 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, + del: Renderer.forwardChildren, + code: this.renderCodeSpan, + link: this.renderLink, + image: this.renderImage, + atMention: this.renderAtMention, + channelLink: this.renderChannelLink, + emoji: this.renderEmoji, + + 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, + + editedIndicator: this.renderEditedIndicator + }, + renderParagraphsInLists: true, + getExtraPropsForNode: this.getExtraPropsForNode + }); + } + + getExtraPropsForNode = (node) => { + const extraProps = { + continue: node.continue, + index: node.index + }; + + if (node.type === 'image') { + extraProps.reactChildren = node.react.children; + extraProps.linkDestination = node.linkDestination; + } + + return extraProps; + } + + computeTextStyle = (baseStyle, context) => { + return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type])); + } + + renderText = ({context, literal}) => { + if (context.indexOf('image') !== -1) { + // If this text is displayed, it will be styled by the image component + return {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 = ({linkDestination, reactChildren, context, src}) => { + return ( + + {reactChildren} + + ); + } + + renderAtMention = ({context, mentionName}) => { + return ( + + ); + } + + renderChannelLink = ({context, channelName}) => { + return ( + + ); + } + + renderEmoji = ({context, emojiName, literal}) => { + let size; + let fontSize; + const headingType = context.find((type) => type.startsWith('heading')); + if (headingType) { + size = this.props.emojiSizes[headingType]; + fontSize = this.props.fontSizes[headingType]; + } else { + size = this.props.emojiSizes.text; + fontSize = this.props.fontSizes.text; + } + + return ( + + ); + } + + renderParagraph = ({children, first}) => { + if (!children || children.length === 0) { + return null; + } + + const style = getStyleSheet(this.props.theme); + const blockStyle = [style.block]; + if (!first) { + blockStyle.push(this.props.blockStyles.adjacentParagraph); + } + + return ( + + + {children} + + + ); + } + + renderHeading = ({children, level}) => { + const style = getStyleSheet(this.props.theme); + + return ( + + + {children} + + + ); + } + + renderCodeBlock = (props) => { + // These sometimes include a trailing newline + const content = props.literal.replace(/\n$/, ''); + + return ( + + ); + } + + renderBlockQuote = ({children, ...otherProps}) => { + return ( + + {children} + + ); + } + + renderList = ({children, tight, type}) => { + return ( + + {children} + + ); + } + + renderListItem = ({children, context, ...otherProps}) => { + const level = context.filter((type) => type === 'list').length; + + return ( + + {children} + + ); + } + + renderHardBreak = () => { + return {'\n'}; + } + + renderThematicBreak = () => { + return ; + } + + renderSoftBreak = () => { + return {'\n'}; + } + + renderHtml = (props) => { + let rendered = this.renderText(props); + + if (props.isBlock) { + const style = getStyleSheet(this.props.theme); + + rendered = ( + + {rendered} + + ); + } + + return rendered; + } + + renderLink = ({children, href}) => { + return ( + + {children} + + ); + } + + renderEditedIndicator = ({context}) => { + let spacer = ''; + if (context[0] === 'paragraph') { + spacer = ' '; + } + + const style = getStyleSheet(this.props.theme); + const styles = [ + this.props.baseTextStyle, + style.editedIndicatorText + ]; + + return ( + + {spacer} + + + ); + } + + render() { + let ast = this.parser.parse(this.props.value); + + ast = addListItemIndices(ast); + ast = pullOutImages(ast); + + if (this.props.isEdited) { + const editIndicatorNode = new Node('edited_indicator'); + if (['heading', 'paragraph'].includes(ast.lastChild.type)) { + ast.lastChild.appendChild(editIndicatorNode); + } else { + const node = new Node('paragraph'); + node.appendChild(editIndicatorNode); + + ast.appendChild(node); + } + } + + return {this.renderer.render(ast)}; + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + // Android has trouble giving text transparency depending on how it's nested, + // so we calculate the resulting colour manually + const editedOpacity = Platform.select({ + ios: 0.3, + android: 1.0 + }); + const editedColor = Platform.select({ + ios: theme.centerChannelColor, + android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3) + }); + + return { + block: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap' + }, + editedIndicatorText: { + color: editedColor, + fontSize: 14, + opacity: editedOpacity + } + }; +}); diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 4478faa53..9fa6df11e 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -233,7 +233,6 @@ class PostBody extends PureComponent { onLongPress={this.showOptionsContext} onPostPress={onPress} textStyles={textStyles} - theme={theme} value={message} />