diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index a5c96752f..332732a14 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -201,35 +201,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -426,35 +431,40 @@ exports[`Storyshots Message list 1`] = ` - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -651,35 +661,40 @@ exports[`Storyshots Message list 1`] = ` - - … - + ... @@ -858,35 +873,40 @@ exports[`Storyshots Message list 1`] = ` - - Different user - + Different user @@ -947,35 +967,40 @@ exports[`Storyshots Message list 1`] = ` > - - This is the third message - + This is the third message @@ -1036,35 +1061,40 @@ exports[`Storyshots Message list 1`] = ` > - - This is the second message - + This is the second message @@ -1243,35 +1273,40 @@ exports[`Storyshots Message list 1`] = ` - - This is the first message - + This is the first message @@ -1350,35 +1385,40 @@ exports[`Storyshots Message list 1`] = ` > - - Message - + Message @@ -1589,35 +1629,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -1810,35 +1855,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -2035,35 +2085,576 @@ exports[`Storyshots Message list 1`] = ` + + Message + + + + + + + + + + + Block Quote + + + + + + + + + + + + + + + + - + + + 10:00 AM + + + + + + + - - Message + + + Testing block quote + + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + + + + Testing block quote + + + + + + + + + Testing block quote @@ -2073,6 +2664,1160 @@ exports[`Storyshots Message list 1`] = ` + + Lists + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + • + + + + + + + Dogs + + + + + + + ◦ + + + + + + + cats + + + + + + + + + ◦ + + + + + + + cats + + + + + + + + + + + + + + + Numerated lists + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + 1. + + + + + + + Dogs + + + + + + + + + 2. + + + + + + + Cats + + + + + + + + + + + + + Numerated lists in separated messages + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + 1. + + + + + + + Dogs + + + + + + + + + + + + + + + + + + + + + 2. + + + + + + + Cats + + + + + + + + + + + - - Message - + Message @@ -2485,35 +4235,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -2710,124 +4465,187 @@ exports[`Storyshots Message list 1`] = ` - + -   - rocket.cat -   - - - - - + + + -   - diego.mello -   - - - - - + @diego.mello + + -   - all -   - - - - - + + + -   - here -   - - - - - + @all + + + + + -  # - general -   - + } + > + @here + + + + + + #general @@ -3024,35 +4842,40 @@ exports[`Storyshots Message list 1`] = ` - - 👊🤙👏 - + 👊🤙👏 @@ -3249,35 +5072,40 @@ exports[`Storyshots Message list 1`] = ` - - 👏 - + 👏 @@ -3474,78 +5302,104 @@ exports[`Storyshots Message list 1`] = ` - + - - - - - - - - - + + + + + + @@ -3741,46 +5595,30 @@ exports[`Storyshots Message list 1`] = ` - - - + /> @@ -3976,49 +5814,54 @@ exports[`Storyshots Message list 1`] = ` - - 🤙 - - + 🤙 + @@ -4214,51 +6057,77 @@ exports[`Storyshots Message list 1`] = ` - - 🤙 - - + - - 🤙🤙 - + } + /> + + 🤙🤙 @@ -4455,35 +6324,40 @@ exports[`Storyshots Message list 1`] = ` - - Testing - + Testing @@ -4680,35 +6554,40 @@ exports[`Storyshots Message list 1`] = ` - - Reactions - + Reactions @@ -5205,35 +7084,40 @@ exports[`Storyshots Message list 1`] = ` - - Multiple Reactions - + Multiple Reactions @@ -6103,35 +7987,40 @@ exports[`Storyshots Message list 1`] = ` - - Fourth message - + Fourth message @@ -6310,35 +8199,40 @@ exports[`Storyshots Message list 1`] = ` - - Third message - + Third message @@ -6517,35 +8411,40 @@ exports[`Storyshots Message list 1`] = ` - - Second message - + Second message @@ -6724,35 +8623,40 @@ exports[`Storyshots Message list 1`] = ` - - First message - + First message @@ -6949,35 +8853,40 @@ exports[`Storyshots Message list 1`] = ` - - Fourth message - + Fourth message @@ -7221,35 +9130,40 @@ exports[`Storyshots Message list 1`] = ` - - Third message - + Third message @@ -7357,35 +9271,40 @@ exports[`Storyshots Message list 1`] = ` > - - Second message - + Second message @@ -7564,35 +9483,40 @@ exports[`Storyshots Message list 1`] = ` - - Second message - + Second message @@ -7810,35 +9734,40 @@ exports[`Storyshots Message list 1`] = ` - - First message - + First message @@ -8090,34 +10019,37 @@ exports[`Storyshots Message list 1`] = ` /> - + } + > + - - This is a description - + This is a description @@ -8352,48 +10284,51 @@ exports[`Storyshots Message list 1`] = ` /> - + } + > + - - This is a description - - + This is a description + @@ -8637,48 +10572,51 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - This is a description - - + This is a description + @@ -9289,48 +11227,51 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - This is a description - - + This is a description + @@ -9389,35 +11330,40 @@ exports[`Storyshots Message list 1`] = ` > - - First message - + First message @@ -9674,34 +11620,37 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - This is a description - + This is a description @@ -10399,35 +12348,88 @@ exports[`Storyshots Message list 1`] = ` - - I’m fine! - + I + + + 'm fine + + + ! @@ -10507,34 +12509,37 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - How are you? - + How are you? @@ -10714,35 +12719,88 @@ exports[`Storyshots Message list 1`] = ` - - I’m fine! - + I + + + 'm fine + + + ! @@ -10822,48 +12880,51 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - How are you? - - + How are you? + @@ -11060,35 +13121,88 @@ exports[`Storyshots Message list 1`] = ` - - I’m fine! - + I + + + 'm fine + + + ! @@ -11149,35 +13263,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -11356,35 +13523,88 @@ exports[`Storyshots Message list 1`] = ` - - I’m fine! - + I + + + 'm fine + + + ! @@ -11467,35 +13687,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -11714,35 +13987,40 @@ exports[`Storyshots Message list 1`] = ` - - How are you? - + How are you? @@ -12002,35 +14280,40 @@ exports[`Storyshots Message list 1`] = ` - - How are you? - + How are you? @@ -12320,35 +14603,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -12557,35 +14893,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -12794,35 +15183,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -13031,35 +15473,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -13268,35 +15763,40 @@ exports[`Storyshots Message list 1`] = ` > - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -13505,35 +16005,40 @@ exports[`Storyshots Message list 1`] = ` > - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -13947,35 +16452,40 @@ exports[`Storyshots Message list 1`] = ` - - How are you? - + How are you? @@ -14190,35 +16700,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -14352,35 +16915,40 @@ exports[`Storyshots Message list 1`] = ` > - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -14749,35 +17317,88 @@ exports[`Storyshots Message list 1`] = ` > - - I’m fine! - + I + + + 'm fine + + + ! @@ -14911,35 +17532,64 @@ exports[`Storyshots Message list 1`] = ` > - - Cool! - + Cool + + + ! @@ -15073,35 +17723,40 @@ exports[`Storyshots Message list 1`] = ` > - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. @@ -16968,49 +19623,54 @@ exports[`Storyshots Message list 1`] = ` - - Message - - + Message + @@ -17391,35 +20051,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -17499,34 +20164,37 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - Custom fields - + Custom fields @@ -17934,35 +20602,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -18042,34 +20715,37 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - Custom fields - + Custom fields @@ -18240,34 +20916,37 @@ exports[`Storyshots Message list 1`] = ` - + } + > + - - Custom fields 2 - + Custom fields 2 @@ -18555,35 +21234,40 @@ exports[`Storyshots Message list 1`] = ` - - Broadcasted message - + Broadcasted message @@ -18859,35 +21543,40 @@ exports[`Storyshots Message list 1`] = ` - - This message is inside an archived room - + This message is inside an archived room @@ -19115,35 +21804,40 @@ exports[`Storyshots Message list 1`] = ` - - This message has error - + This message has error @@ -19284,35 +21978,40 @@ exports[`Storyshots Message list 1`] = ` - - This message has error too - + This message has error too @@ -19514,35 +22213,40 @@ exports[`Storyshots Message list 1`] = ` } > - - Temp message - + Temp message @@ -19739,35 +22443,40 @@ exports[`Storyshots Message list 1`] = ` - - Message being edited - + Message being edited @@ -22354,35 +25063,40 @@ exports[`Storyshots Message list 1`] = ` - - Message - + Message @@ -22579,102 +25293,325 @@ exports[`Storyshots Message list 1`] = ` - - Italic with - - + - - asterisks - - - - or - - + underscore + + + or double + + - - underscores - - - - . Bold with - - + underscores + + + . Bold with single + + - - asterisks - - - - or - - + asterisk + + + or double + + - - underscores - - - - . - - + asterisks + + + . Strikethrough with single + + - - Strikethrough - - + }, + ] + } + > + Strikethrough + + + or double + + + Strikethrough @@ -22870,184 +25807,150 @@ exports[`Storyshots Message list 1`] = ` - - + - - - H1 - - - - + + + - - - H2 - - - - + + + - - - H3 - - - - + + + - - - H4 - - - - + + + - - - H5 - - - - + + + - - - H6 - - - - + H6 + + @@ -23241,70 +26144,281 @@ exports[`Storyshots Message list 1`] = ` + Support + + - - Support - - - Google - + Google - - - - + + + + + - - I\`m an inline-style link - + I - - https://google.com + + \` + + + m an inline-style link + + + + + + + + https://google.com @@ -23502,20 +26616,16 @@ exports[`Storyshots Message list 1`] = ` + + Inline + - - Inline - - + - code - - - has - - + has + + - back-ticks around - - - it. - + } + > + back-ticks around + + + it. - - Code block - - + + Code block + + @@ -23999,47 +27156,69 @@ exports[`Storyshots Message list 1`] = ` + - - + Quote - + @@ -24235,99 +27414,52 @@ exports[`Storyshots Message list 1`] = ` - - - - - - - First Header - - - - - - - Second Header - - - - - - - - Content from cell 1 + + + First Header + - + + + + + Second Header + + + - - - Content from cell 2 + + + Content from cell 1 + - - - - - - + - - Content in the first column + + + Content from cell 2 + - + - - - Content in the second column + + + Content in the first column + - + + + + + Content in the second column + + + - + + + Click to see full table + diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js index 7b821172c..15c070938 100644 --- a/app/containers/MessageBox/ReplyPreview.js +++ b/app/containers/MessageBox/ReplyPreview.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import { connect } from 'react-redux'; -import Markdown from '../message/Markdown'; +import Markdown from '../markdown'; import { getCustomEmoji } from '../message/utils'; import { CustomIcon } from '../../lib/Icons'; import sharedStyles from '../../views/Styles'; @@ -50,6 +50,7 @@ const styles = StyleSheet.create({ class ReplyPreview extends Component { static propTypes = { + useMarkdown: PropTypes.bool, message: PropTypes.object.isRequired, Message_TimeFormat: PropTypes.string.isRequired, close: PropTypes.func.isRequired, @@ -68,7 +69,7 @@ class ReplyPreview extends Component { render() { const { - message, Message_TimeFormat, baseUrl, username + message, Message_TimeFormat, baseUrl, username, useMarkdown } = this.props; const time = moment(message.ts).format(Message_TimeFormat); return ( @@ -78,7 +79,7 @@ class ReplyPreview extends Component { {message.u.username} {time} - + @@ -87,6 +88,7 @@ class ReplyPreview extends Component { } const mapStateToProps = state => ({ + useMarkdown: state.markdown.useMarkdown, Message_TimeFormat: state.settings.Message_TimeFormat, baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' }); diff --git a/app/containers/markdown/AtMention.js b/app/containers/markdown/AtMention.js new file mode 100644 index 000000000..375d5d6fe --- /dev/null +++ b/app/containers/markdown/AtMention.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text } from 'react-native'; + +import styles from './styles'; + +const AtMention = React.memo(({ + mention, mentions, username, navToRoomInfo +}) => { + let mentionStyle = styles.mention; + if (mention === 'all' || mention === 'here') { + mentionStyle = { + ...mentionStyle, + ...styles.mentionAll + }; + } else if (mention === username) { + mentionStyle = { + ...mentionStyle, + ...styles.mentionLoggedUser + }; + } + + const handlePress = () => { + if (mentions && mentions.length && mentions.findIndex(m => m.username === mention) !== -1) { + const index = mentions.findIndex(m => m.username === mention); + const navParam = { + t: 'd', + rid: mentions[index]._id + }; + navToRoomInfo(navParam); + } + }; + + return ( + + {`@${ mention }`} + + ); +}); + +AtMention.propTypes = { + mention: PropTypes.string, + username: PropTypes.string, + navToRoomInfo: PropTypes.func, + mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +export default AtMention; diff --git a/app/containers/markdown/BlockQuote.js b/app/containers/markdown/BlockQuote.js new file mode 100644 index 000000000..65a9963d1 --- /dev/null +++ b/app/containers/markdown/BlockQuote.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View } from 'react-native'; + +import styles from './styles'; + +const BlockQuote = React.memo(({ children }) => ( + + + + {children} + + +)); + +BlockQuote.propTypes = { + children: PropTypes.node.isRequired +}; + +export default BlockQuote; diff --git a/app/containers/markdown/Emoji.js b/app/containers/markdown/Emoji.js new file mode 100644 index 000000000..f60c2cb9a --- /dev/null +++ b/app/containers/markdown/Emoji.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text } from 'react-native'; +import { emojify } from 'react-emojione'; + +import CustomEmoji from '../EmojiPicker/CustomEmoji'; + +import styles from './styles'; + +const Emoji = React.memo(({ + emojiName, literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl +}) => { + const emojiUnicode = emojify(literal, { output: 'unicode' }); + const emoji = getCustomEmoji && getCustomEmoji(emojiName); + if (emoji) { + return ( + + ); + } + return {emojiUnicode}; +}); + +Emoji.propTypes = { + emojiName: PropTypes.string, + literal: PropTypes.string, + isMessageContainsOnlyEmoji: PropTypes.bool, + getCustomEmoji: PropTypes.func, + baseUrl: PropTypes.string +}; + +export default Emoji; diff --git a/app/containers/markdown/Hashtag.js b/app/containers/markdown/Hashtag.js new file mode 100644 index 000000000..47e50cb3b --- /dev/null +++ b/app/containers/markdown/Hashtag.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Text } from 'react-native'; + +import styles from './styles'; + +const Hashtag = React.memo(({ + hashtag, channels, navToRoomInfo +}) => { + const handlePress = () => { + const index = channels.findIndex(channel => channel.name === hashtag); + const navParam = { + t: 'c', + rid: channels[index]._id + }; + navToRoomInfo(navParam); + }; + + if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) { + return ( + + {`#${ hashtag }`} + + ); + } + return `#${ hashtag }`; +}); + +Hashtag.propTypes = { + hashtag: PropTypes.string, + navToRoomInfo: PropTypes.func, + channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +export default Hashtag; diff --git a/app/containers/markdown/Link.js b/app/containers/markdown/Link.js new file mode 100644 index 000000000..79b1486c5 --- /dev/null +++ b/app/containers/markdown/Link.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text } from 'react-native'; + +import styles from './styles'; +import openLink from '../../utils/openLink'; + +const Link = React.memo(({ + children, link +}) => { + const handlePress = () => { + if (!link) { + return; + } + openLink(link); + }; + + const childLength = React.Children.toArray(children).filter(o => o).length; + + // if you have a [](https://rocket.chat) render https://rocket.chat + return ( + + { childLength !== 0 ? children : link } + + ); +}); + +Link.propTypes = { + children: PropTypes.node, + link: PropTypes.string +}; + +export default Link; diff --git a/app/containers/markdown/List.js b/app/containers/markdown/List.js new file mode 100644 index 000000000..951b1f306 --- /dev/null +++ b/app/containers/markdown/List.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const List = React.memo(({ + children, ordered, start, tight +}) => { + let bulletWidth = 15; + + if (ordered) { + const lastNumber = (start + children.length) - 1; + bulletWidth = (9 * lastNumber.toString().length) + 7; + } + + const _children = React.Children.map(children, (child, index) => React.cloneElement(child, { + bulletWidth, + ordered, + tight, + index: start + index + })); + + return ( + <> + {_children} + + ); +}); + +List.propTypes = { + children: PropTypes.node, + ordered: PropTypes.bool, + start: PropTypes.number, + tight: PropTypes.bool +}; + +List.defaultProps = { + start: 1 +}; + +export default List; diff --git a/app/containers/markdown/ListItem.js b/app/containers/markdown/ListItem.js new file mode 100644 index 000000000..301c8fed9 --- /dev/null +++ b/app/containers/markdown/ListItem.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + StyleSheet, + Text, + View +} from 'react-native'; + +const style = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'flex-start' + }, + bullet: { + alignItems: 'flex-end', + marginRight: 5 + }, + contents: { + flex: 1 + } +}); + +const ListItem = React.memo(({ + children, level, bulletWidth, continue: _continue, ordered, index +}) => { + let bullet; + if (_continue) { + bullet = ''; + } else if (ordered) { + bullet = `${ index }.`; + } else if (level % 2 === 0) { + bullet = '◦'; + } else { + bullet = '•'; + } + + return ( + + + + {bullet} + + + + {children} + + + ); +}); + +ListItem.propTypes = { + children: PropTypes.node, + bulletWidth: PropTypes.number, + level: PropTypes.number, + ordered: PropTypes.bool, + continue: PropTypes.bool, + index: PropTypes.number +}; + +export default ListItem; diff --git a/app/containers/markdown/Table.js b/app/containers/markdown/Table.js new file mode 100644 index 000000000..30d6521b7 --- /dev/null +++ b/app/containers/markdown/Table.js @@ -0,0 +1,62 @@ +import { PropTypes } from 'prop-types'; +import React from 'react'; +import { + ScrollView, + TouchableOpacity, + View, + Text +} from 'react-native'; + +import { CELL_WIDTH } from './TableCell'; +import styles from './styles'; +import Navigation from '../../lib/Navigation'; +import I18n from '../../i18n'; + +const MAX_HEIGHT = 300; + +const Table = React.memo(({ + children, numColumns +}) => { + const getTableWidth = () => numColumns * CELL_WIDTH; + + const renderRows = (drawExtraBorders = true) => { + const tableStyle = [styles.table]; + if (drawExtraBorders) { + tableStyle.push(styles.tableExtraBorders); + } + + const rows = React.Children.toArray(children); + rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], { + isLastRow: true + }); + + return ( + + {rows} + + ); + }; + + const onPress = () => Navigation.navigate('TableView', { renderRows, tableWidth: getTableWidth() }); + + return ( + + + {renderRows(false)} + + {I18n.t('Full_table')} + + ); +}); + +Table.propTypes = { + children: PropTypes.node.isRequired, + numColumns: PropTypes.number.isRequired +}; + +export default Table; diff --git a/app/containers/markdown/TableCell.js b/app/containers/markdown/TableCell.js new file mode 100644 index 000000000..56c6a11de --- /dev/null +++ b/app/containers/markdown/TableCell.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Text, View } from 'react-native'; + +import styles from './styles'; + +export const CELL_WIDTH = 100; + +const TableCell = React.memo(({ + isLastCell, align, children +}) => { + const cellStyle = [styles.cell]; + if (!isLastCell) { + cellStyle.push(styles.cellRightBorder); + } + + let textStyle = null; + if (align === 'center') { + textStyle = styles.alignCenter; + } else if (align === 'right') { + textStyle = styles.alignRight; + } + + return ( + + + {children} + + + ); +}); + +TableCell.propTypes = { + align: PropTypes.oneOf(['', 'left', 'center', 'right']), + children: PropTypes.node, + isLastCell: PropTypes.bool +}; + +export default TableCell; diff --git a/app/containers/markdown/TableRow.js b/app/containers/markdown/TableRow.js new file mode 100644 index 000000000..6a9789804 --- /dev/null +++ b/app/containers/markdown/TableRow.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { View } from 'react-native'; + +import styles from './styles'; + +const TableRow = React.memo(({ + isLastRow, children: _children +}) => { + const rowStyle = [styles.row]; + if (!isLastRow) { + rowStyle.push(styles.rowBottomBorder); + } + + const children = React.Children.toArray(_children); + children[children.length - 1] = React.cloneElement(children[children.length - 1], { + isLastCell: true + }); + + return {children}; +}); + +TableRow.propTypes = { + children: PropTypes.node, + isLastRow: PropTypes.bool +}; + +export default TableRow; diff --git a/app/containers/markdown/index.js b/app/containers/markdown/index.js new file mode 100644 index 000000000..43e35c86c --- /dev/null +++ b/app/containers/markdown/index.js @@ -0,0 +1,295 @@ +import React, { PureComponent } from 'react'; +import { View, Text, Image } from 'react-native'; +import { Parser, Node } from 'commonmark'; +import Renderer from 'commonmark-react-renderer'; +import PropTypes from 'prop-types'; + +import I18n from '../../i18n'; + +import MarkdownLink from './Link'; +import MarkdownList from './List'; +import MarkdownListItem from './ListItem'; +import MarkdownAtMention from './AtMention'; +import MarkdownHashtag from './Hashtag'; +import MarkdownBlockQuote from './BlockQuote'; +import MarkdownEmoji from './Emoji'; +import MarkdownTable from './Table'; +import MarkdownTableRow from './TableRow'; +import MarkdownTableCell from './TableCell'; + +import styles from './styles'; + +// Support +const formatText = text => text.replace( + new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'), + (match, url, title) => `[${ title }](${ url })` +); + +const emojiRanges = [ + '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421 + ':.{1,40}:', // custom emoji + ' |\n' // allow spaces and line breaks +].join('|'); + +const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), ''); + +const isOnlyEmoji = str => !removeAllEmoji(str).length; + +const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), ''); + +const emojiCount = (str) => { + let oldLength = 0; + let counter = 0; + + while (oldLength !== str.length) { + oldLength = str.length; + str = removeOneEmoji(str); + if (oldLength !== str.length) { + counter += 1; + } + } + + return counter; +}; + +export default class Markdown extends PureComponent { + static propTypes = { + msg: PropTypes.string, + getCustomEmoji: PropTypes.func, + baseUrl: PropTypes.string, + username: PropTypes.string, + tmid: PropTypes.string, + isEdited: PropTypes.bool, + numberOfLines: PropTypes.number, + useMarkdown: PropTypes.bool, + channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + navToRoomInfo: PropTypes.func + }; + + constructor(props) { + super(props); + + this.parser = this.createParser(); + this.renderer = this.createRenderer(); + } + + createParser = () => new Parser(); + + createRenderer = () => new Renderer({ + renderers: { + text: this.renderText, + + emph: Renderer.forwardChildren, + strong: Renderer.forwardChildren, + del: Renderer.forwardChildren, + code: this.renderCodeInline, + link: this.renderLink, + image: this.renderImage, + atMention: this.renderAtMention, + emoji: this.renderEmoji, + hashtag: this.renderHashtag, + + paragraph: this.renderParagraph, + heading: this.renderHeading, + codeBlock: this.renderCodeBlock, + blockQuote: this.renderBlockQuote, + + list: this.renderList, + item: this.renderListItem, + + hardBreak: this.renderBreak, + thematicBreak: this.renderBreak, + softBreak: this.renderBreak, + + htmlBlock: this.renderText, + htmlInline: this.renderText, + + table: this.renderTable, + table_row: this.renderTableRow, + table_cell: this.renderTableCell, + + editedIndicator: this.renderEditedIndicator + }, + renderParagraphsInLists: true + }); + + editedMessage = (ast) => { + const { isEdited } = this.props; + if (isEdited) { + const editIndicatorNode = new Node('edited_indicator'); + if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { + ast.lastChild.appendChild(editIndicatorNode); + } else { + const node = new Node('paragraph'); + node.appendChild(editIndicatorNode); + + ast.appendChild(node); + } + } + }; + + renderText = ({ context, literal }) => { + const { numberOfLines } = this.props; + return ( + styles[type]) + ]} + numberOfLines={numberOfLines} + > + {literal} + + ); + } + + renderCodeInline = ({ literal }) => {literal}; + + renderCodeBlock = ({ literal }) => {literal}; + + renderBreak = () => { + const { tmid } = this.props; + return {tmid ? ' ' : '\n'}; + } + + renderParagraph = ({ children }) => { + const { numberOfLines } = this.props; + + if (!children || children.length === 0) { + return null; + } + return ( + + + {children} + + + ); + }; + + renderLink = ({ children, href }) => ( + + {children} + + ); + + renderHashtag = ({ hashtag }) => { + const { channels, navToRoomInfo } = this.props; + return ( + + ); + } + + renderAtMention = ({ mentionName }) => { + const { username, mentions, navToRoomInfo } = this.props; + return ( + + ); + } + + renderEmoji = ({ emojiName, literal }) => { + const { getCustomEmoji, baseUrl } = this.props; + return ( + + ); + } + + renderImage = ({ src }) => ; + + renderEditedIndicator = () => ({I18n.t('edited')}); + + renderHeading = ({ children, level }) => { + const textStyle = styles[`heading${ level }Text`]; + return ( + + {children} + + ); + }; + + renderList = ({ + children, start, tight, type + }) => ( + + {children} + + ); + + renderListItem = ({ + children, context, ...otherProps + }) => { + const level = context.filter(type => type === 'list').length; + + return ( + + {children} + + ); + }; + + renderBlockQuote = ({ children }) => ( + + {children} + + ); + + renderTable = ({ children, numColumns }) => ( + + {children} + + ); + + renderTableRow = args => ; + + renderTableCell = args => ; + + render() { + const { + msg, useMarkdown = true, numberOfLines + } = this.props; + + if (!msg) { + return null; + } + + let m = formatText(msg); + + // Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test' + // Return: 'Test' + m = m.replace(/^\[([\s]]*)\]\(([^)]*)\)\s/, '').trim(); + + if (!useMarkdown) { + return {m}; + } + + const ast = this.parser.parse(m); + this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; + + this.editedMessage(ast); + + return this.renderer.render(ast); + } +} diff --git a/app/containers/markdown/styles.js b/app/containers/markdown/styles.js new file mode 100644 index 000000000..b2642fdc6 --- /dev/null +++ b/app/containers/markdown/styles.js @@ -0,0 +1,183 @@ +import { StyleSheet, Platform } from 'react-native'; + +import sharedStyles from '../../views/Styles'; +import { + COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER +} from '../../constants/colors'; + +const codeFontFamily = Platform.select({ + ios: { fontFamily: 'Courier New' }, + android: { fontFamily: 'monospace' } +}); + +export default StyleSheet.create({ + container: { + alignItems: 'flex-start', + flexDirection: 'row' + }, + childContainer: { + flex: 1 + }, + block: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap' + }, + emph: { + fontStyle: 'italic' + }, + strong: { + fontWeight: 'bold' + }, + del: { + textDecorationLine: 'line-through' + }, + text: { + fontSize: 16, + ...sharedStyles.textColorNormal, + ...sharedStyles.textRegular + }, + textInfo: { + fontStyle: 'italic', + fontSize: 16, + ...sharedStyles.textColorDescription, + ...sharedStyles.textRegular + }, + textBig: { + fontSize: 30, + ...sharedStyles.textColorNormal, + ...sharedStyles.textRegular + }, + customEmoji: { + width: 20, + height: 20 + }, + customEmojiBig: { + width: 30, + height: 30 + }, + temp: { opacity: 0.3 }, + mention: { + fontSize: 16, + color: '#0072FE', + padding: 5, + ...sharedStyles.textMedium, + backgroundColor: '#E8F2FF' + }, + mentionLoggedUser: { + color: COLOR_WHITE, + backgroundColor: COLOR_PRIMARY + }, + mentionAll: { + color: COLOR_WHITE, + backgroundColor: '#FF5B5A' + }, + paragraph: { + marginTop: 0, + marginBottom: 0, + flexWrap: 'wrap', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start' + }, + inlineImage: { + width: 300, + height: 300, + resizeMode: 'contain' + }, + codeInline: { + ...sharedStyles.textRegular, + ...codeFontFamily, + borderWidth: 1, + backgroundColor: COLOR_BACKGROUND_CONTAINER, + borderRadius: 4 + }, + codeBlock: { + ...sharedStyles.textRegular, + ...codeFontFamily, + backgroundColor: COLOR_BACKGROUND_CONTAINER, + borderColor: COLOR_BORDER, + borderWidth: 1, + borderRadius: 4, + padding: 4 + }, + link: { + fontSize: 16, + color: COLOR_PRIMARY, + ...sharedStyles.textRegular + }, + edited: { + fontSize: 14, + ...sharedStyles.textColorDescription, + ...sharedStyles.textRegular + }, + heading1: { + ...sharedStyles.textBold, + fontSize: 24 + }, + heading2: { + ...sharedStyles.textBold, + fontSize: 22 + }, + heading3: { + ...sharedStyles.textSemibold, + fontSize: 20 + }, + heading4: { + ...sharedStyles.textSemibold, + fontSize: 18 + }, + heading5: { + ...sharedStyles.textMedium, + fontSize: 16 + }, + heading6: { + ...sharedStyles.textMedium, + fontSize: 14 + }, + quote: { + height: '100%', + width: 2, + backgroundColor: COLOR_BORDER, + marginRight: 5 + }, + touchableTable: { + justifyContent: 'center' + }, + containerTable: { + borderBottomWidth: 1, + borderColor: COLOR_BORDER, + borderRightWidth: 1 + }, + table: { + borderColor: COLOR_BORDER, + borderLeftWidth: 1, + borderTopWidth: 1 + }, + tableExtraBorders: { + borderBottomWidth: 1, + borderRightWidth: 1 + }, + row: { + flexDirection: 'row' + }, + rowBottomBorder: { + borderColor: COLOR_BORDER, + borderBottomWidth: 1 + }, + cell: { + borderColor: COLOR_BORDER, + justifyContent: 'flex-start', + paddingHorizontal: 13, + paddingVertical: 6 + }, + cellRightBorder: { + borderRightWidth: 1 + }, + alignCenter: { + textAlign: 'center' + }, + alignRight: { + textAlign: 'right' + } +}); diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index e78e310f1..4d411df9d 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -9,7 +9,7 @@ import moment from 'moment'; import equal from 'deep-equal'; import Touchable from 'react-native-platform-touchable'; -import Markdown from './Markdown'; +import Markdown from '../markdown'; import { CustomIcon } from '../../lib/Icons'; import sharedStyles from '../../views/Styles'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors'; diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js index a9d899ab1..efcf43c8c 100644 --- a/app/containers/message/Content.js +++ b/app/containers/message/Content.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import I18n from '../../i18n'; import styles from './styles'; -import Markdown from './Markdown'; +import Markdown from '../markdown'; import { getInfoMessage } from './utils'; const Content = React.memo((props) => { @@ -21,14 +21,15 @@ const Content = React.memo((props) => { ); } @@ -43,16 +44,16 @@ const Content = React.memo((props) => { Content.propTypes = { isTemp: PropTypes.bool, isInfo: PropTypes.bool, - isEdited: PropTypes.bool, - useMarkdown: PropTypes.bool, tmid: PropTypes.string, msg: PropTypes.string, + isEdited: PropTypes.bool, + useMarkdown: PropTypes.bool, baseUrl: PropTypes.string, user: PropTypes.object, - mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + getCustomEmoji: PropTypes.func, channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - navToRoomInfo: PropTypes.func, - getCustomEmoji: PropTypes.func + mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + navToRoomInfo: PropTypes.func }; Content.displayName = 'MessageContent'; diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 7848f43a4..e4af59257 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -5,7 +5,7 @@ import FastImage from 'react-native-fast-image'; import equal from 'deep-equal'; import Touchable from 'react-native-platform-touchable'; -import Markdown from './Markdown'; +import Markdown from '../markdown'; import styles from './styles'; import { formatAttachmentUrl } from '../../lib/utils'; diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js deleted file mode 100644 index b7622b0b2..000000000 --- a/app/containers/message/Markdown.js +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import { Text, Image } from 'react-native'; -import PropTypes from 'prop-types'; -import { emojify } from 'react-emojione'; -import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer'; -import MarkdownFlowdock from 'markdown-it-flowdock'; - -import styles from './styles'; -import CustomEmoji from '../EmojiPicker/CustomEmoji'; -import MarkdownEmojiPlugin from './MarkdownEmojiPlugin'; -import I18n from '../../i18n'; - -const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin); -const MentionsPlugin = new PluginContainer(MarkdownFlowdock); -const plugins = [EmojiPlugin, MentionsPlugin]; - -// Support -const formatText = text => text.replace( - new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'), - (match, url, title) => `[${ title }](${ url })` -); - -const emojiRanges = [ - '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421 - ':.{1,40}:', // custom emoji - ' |\n' // allow spaces and line breaks -].join('|'); - -const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), ''); - -const isOnlyEmoji = str => !removeAllEmoji(str).length; - -const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), ''); - -const emojiCount = (str) => { - let oldLength = 0; - let counter = 0; - - while (oldLength !== str.length) { - oldLength = str.length; - str = removeOneEmoji(str); - if (oldLength !== str.length) { - counter += 1; - } - } - - return counter; -}; - -const Markdown = React.memo(({ - msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true, navToRoomInfo -}) => { - if (!msg) { - return null; - } - let m = formatText(msg); - if (m) { - m = emojify(m, { output: 'unicode' }); - } - m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)\s/, '').trim(); - if (numberOfLines > 0) { - m = m.replace(/[\n]+/g, '\n').trim(); - } - - if (!useMarkdown) { - return {m}; - } - - const isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; - - return ( - ( - - {children} - {isEdited ? ({I18n.t('edited')}) : null} - - ), - mention: (node) => { - const { content, key } = node; - let mentionStyle = styles.mention; - if (content === 'all' || content === 'here') { - mentionStyle = { - ...mentionStyle, - ...styles.mentionAll - }; - } else if (content === username) { - mentionStyle = { - ...mentionStyle, - ...styles.mentionLoggedUser - }; - } - if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) { - const index = mentions.findIndex(mention => mention.username === content); - const navParam = { - t: 'd', - rid: mentions[index]._id - }; - return ( - navToRoomInfo(navParam)} - > -  {content}  - - ); - } - return `@${ content }`; - }, - hashtag: (node) => { - const { content, key } = node; - if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) { - const index = channels.findIndex(channel => channel.name === content); - const navParam = { - t: 'c', - rid: channels[index]._id - }; - return ( - navToRoomInfo(navParam)} - > -  #{content}  - - ); - } - return `#${ content }`; - }, - emoji: (node) => { - if (node.children && node.children.length && node.children[0].content) { - const { content } = node.children[0]; - const emoji = getCustomEmoji && getCustomEmoji(content); - if (emoji) { - return ( - - ); - } - return :{content}:; - } - return null; - }, - hardbreak: () => null, - blocklink: () => null, - image: node => ( - - ), - ...rules - }} - style={{ - paragraph: styles.paragraph, - text: isMessageContainsOnlyEmoji ? styles.textBig : styles.text, - codeInline: styles.codeInline, - codeBlock: styles.codeBlock, - link: styles.link, - ...style - }} - plugins={plugins} - >{m} - - ); -}); - -Markdown.propTypes = { - msg: PropTypes.string, - username: PropTypes.string, - baseUrl: PropTypes.string, - style: PropTypes.any, - rules: PropTypes.object, - isEdited: PropTypes.bool, - numberOfLines: PropTypes.number, - useMarkdown: PropTypes.bool, - mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - getCustomEmoji: PropTypes.func, - navToRoomInfo: PropTypes.func -}; -Markdown.displayName = 'MessageMarkdown'; - -export default Markdown; diff --git a/app/containers/message/MarkdownEmojiPlugin.js b/app/containers/message/MarkdownEmojiPlugin.js deleted file mode 100644 index 920c27b51..000000000 --- a/app/containers/message/MarkdownEmojiPlugin.js +++ /dev/null @@ -1,78 +0,0 @@ -export default function(md) { - function tokenize(state, silent) { - let token; - const start = state.pos; - const marker = state.src.charCodeAt(start); - - if (silent) { - return false; - } - - // : - if (marker !== 58) { - return false; - } - - const scanned = state.scanDelims(state.pos, true); - const len = scanned.length; - const ch = String.fromCharCode(marker); - - for (let i = 0; i < len; i += 1) { - token = state.push('text', '', 0); - token.content = ch; - - state.delimiters.push({ - marker, - jump: i, - token: state.tokens.length - 1, - level: state.level, - end: -1, - open: scanned.can_open, - close: scanned.can_close - }); - } - - state.pos += scanned.length; - return true; - } - - function postProcess(state) { - let startDelim; - let endDelim; - let token; - const { delimiters } = state; - const max = delimiters.length; - - for (let i = 0; i < max; i += 1) { - startDelim = delimiters[i]; - - // : - if (startDelim.marker !== 58) { - continue; // eslint-disable-line - } - - if (startDelim.end === -1) { - continue; // eslint-disable-line - } - - endDelim = delimiters[startDelim.end]; - - token = state.tokens[startDelim.token]; - token.type = 'emoji_open'; - token.tag = 'emoji'; - token.nesting = 1; - token.markup = ':'; - token.content = ''; - - token = state.tokens[endDelim.token]; - token.type = 'emoji_close'; - token.tag = 'emoji'; - token.nesting = -1; - token.markup = ':'; - token.content = ''; - } - } - - md.inline.ruler.before('emphasis', 'emoji', tokenize); - md.inline.ruler2.before('emphasis', 'emoji', postProcess); -} diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js index efa980b33..4b2af8e58 100644 --- a/app/containers/message/Reply.js +++ b/app/containers/message/Reply.js @@ -5,7 +5,7 @@ import moment from 'moment'; import Touchable from 'react-native-platform-touchable'; import isEqual from 'deep-equal'; -import Markdown from './Markdown'; +import Markdown from '../markdown'; import openLink from '../../utils/openLink'; import sharedStyles from '../../views/Styles'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors'; diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js index b56c41ac3..d67107795 100644 --- a/app/containers/message/Video.js +++ b/app/containers/message/Video.js @@ -4,7 +4,7 @@ import { StyleSheet } from 'react-native'; import Touchable from 'react-native-platform-touchable'; import isEqual from 'deep-equal'; -import Markdown from './Markdown'; +import Markdown from '../markdown'; import openLink from '../../utils/openLink'; import { isIOS } from '../../utils/deviceInfo'; import { CustomIcon } from '../../lib/Icons'; diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 605bc5c2e..3113f542a 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -1,15 +1,10 @@ -import { StyleSheet, Platform } from 'react-native'; +import { StyleSheet } from 'react-native'; import sharedStyles from '../../views/Styles'; import { - COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER + COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; -const codeFontFamily = Platform.select({ - ios: { fontFamily: 'Courier New' }, - android: { fontFamily: 'monospace' } -}); - export default StyleSheet.create({ root: { flexDirection: 'row' @@ -34,30 +29,6 @@ export default StyleSheet.create({ flexDirection: 'row' // flex: 1 }, - text: { - fontSize: 16, - ...sharedStyles.textColorNormal, - ...sharedStyles.textRegular - }, - textBig: { - fontSize: 30, - ...sharedStyles.textColorNormal, - ...sharedStyles.textRegular - }, - textInfo: { - fontStyle: 'italic', - fontSize: 16, - ...sharedStyles.textColorDescription, - ...sharedStyles.textRegular - }, - customEmoji: { - width: 20, - height: 20 - }, - customEmojiBig: { - width: 30, - height: 30 - }, temp: { opacity: 0.3 }, marginTop: { marginTop: 6 @@ -143,28 +114,6 @@ export default StyleSheet.create({ fontSize: 14, ...sharedStyles.textMedium }, - mention: { - ...sharedStyles.textMedium, - color: '#0072FE', - padding: 5, - backgroundColor: '#E8F2FF' - }, - mentionLoggedUser: { - color: COLOR_WHITE, - backgroundColor: COLOR_PRIMARY - }, - mentionAll: { - color: COLOR_WHITE, - backgroundColor: '#FF5B5A' - }, - paragraph: { - marginTop: 0, - marginBottom: 0, - flexWrap: 'wrap', - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'flex-start' - }, imageContainer: { // flex: 1, flexDirection: 'column', @@ -186,29 +135,15 @@ export default StyleSheet.create({ height: 300, resizeMode: 'contain' }, - edited: { - fontSize: 14, - ...sharedStyles.textColorDescription, + text: { + fontSize: 16, + ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, - codeInline: { - ...sharedStyles.textRegular, - ...codeFontFamily, - borderWidth: 1, - backgroundColor: COLOR_BACKGROUND_CONTAINER, - borderRadius: 4 - }, - codeBlock: { - ...sharedStyles.textRegular, - ...codeFontFamily, - backgroundColor: COLOR_BACKGROUND_CONTAINER, - borderColor: COLOR_BORDER, - borderWidth: 1, - borderRadius: 4, - padding: 4 - }, - link: { - color: COLOR_PRIMARY, + textInfo: { + fontStyle: 'italic', + fontSize: 16, + ...sharedStyles.textColorDescription, ...sharedStyles.textRegular }, startedDiscussion: { diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index 0bd3e712f..cf1803758 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -181,6 +181,7 @@ export default { Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.', Forgot_password: 'Forgot password', Forgot_Password: 'Forgot Password', + Full_table: 'Click to see full table', Group_by_favorites: 'Group favorites', Group_by_type: 'Group by type', Hide: 'Hide', @@ -360,6 +361,7 @@ export default { Start_of_conversation: 'Start of conversation', Started_discussion: 'Started a discussion:', Submit: 'Submit', + Table: 'Table', Take_a_photo: 'Take a photo', Take_a_video: 'Take a video', tap_to_change_status: 'tap to change status', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 2e8ea6051..6653c136a 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -178,6 +178,7 @@ export default { Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.', Forgot_password: 'Esqueci minha senha', Forgot_Password: 'Esqueci minha senha', + Full_table: 'Clique para ver a tabela completa', Group_by_favorites: 'Agrupar favoritos', Group_by_type: 'Agrupar por tipo', Has_joined_the_channel: 'Entrou no canal', @@ -326,6 +327,7 @@ export default { Start_of_conversation: 'Início da conversa', Started_discussion: 'Iniciou uma discussão:', Submit: 'Enviar', + Table: 'Tabela', Take_a_photo: 'Tirar uma foto', Take_a_video: 'Gravar um vídeo', Terms_of_Service: ' Termos de Serviço ', diff --git a/app/index.js b/app/index.js index 0a4f584c9..2b33616af 100644 --- a/app/index.js +++ b/app/index.js @@ -122,6 +122,9 @@ const ChatsStack = createStackNavigator({ DirectoryView: { getScreen: () => require('./views/DirectoryView').default }, + TableView: { + getScreen: () => require('./views/TableView').default + }, NotificationPrefView: { getScreen: () => require('./views/NotificationPreferencesView').default } diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index 4465cd23b..2ac7a50b6 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -8,7 +8,7 @@ import equal from 'deep-equal'; import RCTextInput from '../../containers/TextInput'; import RCActivityIndicator from '../../containers/ActivityIndicator'; import styles from './styles'; -import Markdown from '../../containers/message/Markdown'; +import Markdown from '../../containers/markdown'; import debounce from '../../utils/debounce'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message/Message'; diff --git a/app/views/TableView.js b/app/views/TableView.js new file mode 100644 index 000000000..6f61fee43 --- /dev/null +++ b/app/views/TableView.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import PropTypes from 'prop-types'; + +import I18n from '../i18n'; +import { isIOS } from '../utils/deviceInfo'; + +export default class TableView extends React.Component { + static navigationOptions = () => ({ + title: I18n.t('Table') + }); + + static propTypes = { + navigation: PropTypes.object + } + + render() { + const { navigation } = this.props; + const renderRows = navigation.getParam('renderRows'); + const tableWidth = navigation.getParam('tableWidth'); + + if (isIOS) { + return ( + + {renderRows()} + + ); + } + + return ( + + + {renderRows()} + + + ); + } +} diff --git a/package.json b/package.json index 33aa7a88f..ee4f5cf50 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ }, "dependencies": { "@rocket.chat/sdk": "1.0.0-alpha.30", + "commonmark": "git+https://github.com/RocketChat/commonmark.js.git", + "commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git", "bugsnag-react-native": "^2.22.3", "deep-equal": "^1.0.1", "ejson": "2.2.0", @@ -34,7 +36,6 @@ "js-base64": "^2.5.1", "js-sha256": "^0.9.0", "lodash": "4.17.15", - "markdown-it-flowdock": "0.3.8", "moment": "^2.24.0", "prop-types": "15.7.2", "react": "16.8.6", @@ -56,7 +57,6 @@ "react-native-keyboard-input": "^5.3.1", "react-native-keyboard-tracking-view": "^5.5.0", "react-native-localize": "^1.1.4", - "react-native-markdown-renderer": "^3.2.8", "react-native-mime-types": "^2.2.1", "react-native-modal": "^11.3.0", "react-native-notifications": "^2.0.6", @@ -90,7 +90,8 @@ "rn-user-defaults": "^1.3.5", "semver": "6.3.0", "snyk": "1.210.0", - "strip-ansi": "5.2.0" + "strip-ansi": "5.2.0", + "url-parse": "^1.4.7" }, "devDependencies": { "@babel/core": "^7.5.5", diff --git a/patches/react-native-markdown-renderer+3.2.8.patch b/patches/react-native-markdown-renderer+3.2.8.patch deleted file mode 100644 index 02379a174..000000000 --- a/patches/react-native-markdown-renderer+3.2.8.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/node_modules/react-native-markdown-renderer/src/index.js b/node_modules/react-native-markdown-renderer/src/index.js -index 653bba2..e5cb521 100644 ---- a/node_modules/react-native-markdown-renderer/src/index.js -+++ b/node_modules/react-native-markdown-renderer/src/index.js -@@ -88,9 +88,15 @@ export default class Markdown extends Component { - }), - }; - -- copy = ''; -- renderer = null; -- markdownParser = null; -+ constructor(props) { -+ super(props); -+ this.copy = ''; -+ this.renderer = null; -+ this.markdownParser = null; -+ } -+ // copy = ''; -+ // renderer = null; -+ // markdownParser = null; - - /** - * Only when the copy changes will the markdown render again. diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index 79511c744..8aca2ef6b 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -91,6 +91,20 @@ export default ( + + + Testing block quote\nTesting block quote'} /> + + + + + + + + + + + - + =0.50.0": - version "0.57.33" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.57.33.tgz#1d35a883c6e30d6f0b40385230fde2d8213b4dca" - integrity sha512-mn6u8aeh7nxBGO82z/vQeFrlfkBIAAk69MIxSK0aIn8cQnaFqmsoaeSBPhc1K+oIbMXytfehl0w5U1H20OIk+A== - dependencies: - "@types/prop-types" "*" - "@types/react" "*" - -"@types/react@*": - version "16.7.22" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.22.tgz#5bc6d166d5ac34b835756f0b736c7b1af0043e81" - integrity sha512-j/3tVoY09kHcTfbia4l67ofQn9xvktUvlC/4QN0KuBHAXlbU/wuGKMb8WfEb/vIcWxsOxHv559uYprkFDFfP8Q== - dependencies: - "@types/prop-types" "*" - csstype "^2.2.0" - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -3180,6 +3154,25 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +"commonmark-react-renderer@git+https://github.com/RocketChat/commonmark-react-renderer.git": + version "4.3.4" + resolved "git+https://github.com/RocketChat/commonmark-react-renderer.git#1264ac7b1c13d9be3e2f67eec6702a3132f4fac2" + dependencies: + lodash.assign "^4.2.0" + lodash.isplainobject "^4.0.6" + pascalcase "^0.1.1" + xss-filters "^1.2.6" + +"commonmark@git+https://github.com/RocketChat/commonmark.js.git": + version "0.29.0" + resolved "git+https://github.com/RocketChat/commonmark.js.git#005849af59002665dea50353ae9991c49abb1380" + dependencies: + entities "~ 1.1.1" + mdurl "~ 1.0.1" + minimist "~ 1.2.0" + string.prototype.repeat "^0.2.0" + xregexp "4.1.1" + compare-versions@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.4.0.tgz#e0747df5c9cb7f054d6d3dc3e1dbc444f9e92b26" @@ -3473,11 +3466,6 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" -csstype@^2.2.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" - integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== - csstype@^2.5.7: version "2.6.6" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" @@ -3940,7 +3928,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -entities@~1.1.1: +"entities@~ 1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== @@ -7262,13 +7250,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -linkify-it@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db" - integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg== - dependencies: - uc.micro "^1.0.1" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -7363,6 +7344,11 @@ lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7492,22 +7478,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it-flowdock@0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/markdown-it-flowdock/-/markdown-it-flowdock-0.3.8.tgz#fb768485e648d90f596c579d51aa70397d33d916" - integrity sha512-VcI3/ZPC9Gb72KUKmf4VvVA9atvpbt+Hame8dMtyEXsp0Cdw6RF/wbgjcjh1+7EVvcvASm2Gw3zjXio8S3evJg== - -markdown-it@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" - integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== - dependencies: - argparse "^1.0.7" - entities "~1.1.1" - linkify-it "^2.0.0" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdown-to-jsx@^6.9.1: version "6.10.3" resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.3.tgz#7f0946684acd321125ff2de7fd258a9b9c7c40b7" @@ -7530,7 +7500,7 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdurl@^1.0.1: +"mdurl@~ 1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= @@ -7962,7 +7932,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, "minimist@~ 1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -9391,6 +9361,11 @@ querystringify@^2.0.0: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" @@ -9597,13 +9572,6 @@ react-native-firebase@^5.5.5: opencollective-postinstall "^2.0.0" prop-types "^15.7.2" -react-native-fit-image@^1.5.2: - version "1.5.4" - resolved "https://registry.yarnpkg.com/react-native-fit-image/-/react-native-fit-image-1.5.4.tgz#73d2fccc7ad902cf2ffcd008a2a74749ad50134a" - integrity sha512-wNHlGdDWsUU31qlM5SsvZrMH4eXBZt586FQNXFRFuOiXVqdA++6Xait7aiZ+5vxglgqLf+zzSnoICn0NEvDfrw== - dependencies: - prop-types "^15.5.10" - react-native-gesture-handler@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-1.3.0.tgz#d0386f565928ccc1849537f03f2e37fd5f6ad43f" @@ -9660,17 +9628,6 @@ react-native-localize@^1.1.4: resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-1.1.4.tgz#d48aa4f75afd39a42dcd0bdf40f7f44a8ccd604c" integrity sha512-NHsA812yvoH9ktPl1IqIxwDDwykipyH7K4zeW/nnipZuQb2g73SQEB3ryqKHmRASWD0DZl0hIxHr9IszzG5W5w== -react-native-markdown-renderer@^3.2.8: - version "3.2.8" - resolved "https://registry.yarnpkg.com/react-native-markdown-renderer/-/react-native-markdown-renderer-3.2.8.tgz#217046cf198eca632a65f93cdf7dd7766f718070" - integrity sha512-gDT5r3lwecNsEfpKagSaidEGfmCbpVcmV+HHLjaGYRALJoHkpOFni0rJZW1rCerOR9sjaUNGXE66U7BUrlEw0w== - dependencies: - "@types/markdown-it" "^0.0.4" - "@types/react-native" ">=0.50.0" - markdown-it "^8.4.0" - prop-types "^15.5.10" - react-native-fit-image "^1.5.2" - react-native-mime-types@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-native-mime-types/-/react-native-mime-types-2.2.1.tgz#a9760e9916e4e7df03512c60516668f23543f2c0" @@ -11442,6 +11399,11 @@ string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string.prototype.repeat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" + integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= + string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -12003,11 +11965,6 @@ ua-parser-js@^0.7.19: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098" integrity sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== - uglify-es@^3.1.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -12235,6 +12192,14 @@ url-parse@^1.4.4: querystringify "^2.0.0" requires-port "^1.0.0" +url-parse@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + urlgrey@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f" @@ -12605,6 +12570,16 @@ xregexp@2.0.0: resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= +xregexp@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.1.1.tgz#eb8a032aa028d403f7b1b22c47a5f16c24b21d8d" + integrity sha512-QJ1gfSUV7kEOLfpKFCjBJRnfPErUzkNKFMso4kDSmGpp3x6ZgkyKf74inxI7PnnQCFYq5TqYJCd7DrgDN8Q05A== + +xss-filters@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/xss-filters/-/xss-filters-1.2.7.tgz#59fa1de201f36f2f3470dcac5f58ccc2830b0a9a" + integrity sha1-Wfod4gHzby80cNysX1jMwoMLCpo= + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"