diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index 0a4af2921..25a914b01 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -24,6 +24,10 @@ import MarkdownImage from './markdown_image';
import MarkdownLink from './markdown_link';
import MarkdownList from './markdown_list';
import MarkdownListItem from './markdown_list_item';
+import MarkdownTable from './markdown_table';
+import MarkdownTableImage from './markdown_table_image';
+import MarkdownTableRow from './markdown_table_row';
+import MarkdownTableCell from './markdown_table_cell';
import {addListItemIndices, pullOutImages} from './transform';
export default class Markdown extends PureComponent {
@@ -83,6 +87,10 @@ export default class Markdown extends PureComponent {
htmlBlock: this.renderHtml,
htmlInline: this.renderHtml,
+ table: this.renderTable,
+ table_row: MarkdownTableRow,
+ table_cell: MarkdownTableCell,
+
editedIndicator: this.renderEditedIndicator
},
renderParagraphsInLists: true,
@@ -124,6 +132,21 @@ export default class Markdown extends PureComponent {
}
renderImage = ({linkDestination, reactChildren, context, src}) => {
+ if (context.indexOf('table') !== -1) {
+ // We have enough problems rendering images as is, so just render a link inside of a table
+ return (
+
+ {reactChildren}
+
+ );
+ }
+
return (
{
+ return (
+
+ {children}
+
+ );
+ }
+
renderLink = ({children, href}) => {
return (
{this.renderer.render(ast)};
+
+ return this.renderer.render(ast);
}
}
diff --git a/app/components/markdown/markdown_code_block/markdown_code_block.js b/app/components/markdown/markdown_code_block/markdown_code_block.js
index 66c3f2cc3..976a7d570 100644
--- a/app/components/markdown/markdown_code_block/markdown_code_block.js
+++ b/app/components/markdown/markdown_code_block/markdown_code_block.js
@@ -145,7 +145,7 @@ class MarkdownCodeBlock extends React.PureComponent {
{
+ const {navigator, theme} = this.props;
+
+ navigator.push({
+ screen: 'Table',
+ title: this.context.intl.formatMessage({
+ id: 'mobile.routes.table',
+ defaultMessage: 'Table'
+ }),
+ animated: true,
+ backButtonTitle: '',
+ passProps: {
+ renderRows: this.renderRows
+ },
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg
+ }
+ });
+ });
+
+ handleContentHeightChange = (contentWidth, contentHeight) => {
+ this.setState({
+ contentCutOff: contentHeight >= MAX_HEIGHT
+ });
+ }
+
+ renderRows = (drawBottomBorder = true) => {
+ const style = getStyleSheet(this.props.theme);
+
+ const tableStyle = [style.table];
+ if (drawBottomBorder) {
+ tableStyle.push(style.tableBottomBorder);
+ }
+
+ // Add an extra prop to the last row of the table so that it knows not to render a bottom border
+ // since the container should be rendering that
+ const rows = React.Children.toArray(this.props.children);
+ rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], {
+ isLastRow: true
+ });
+
+ return (
+
+ {rows}
+
+ );
+ }
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ let moreIndicator = null;
+ if (this.state.contentCutOff) {
+ moreIndicator = (
+
+ );
+ }
+
+ return (
+
+
+ {this.renderRows(false)}
+
+ {moreIndicator}
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ borderBottomWidth: 1,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2),
+ maxHeight: MAX_HEIGHT
+ },
+ table: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderLeftWidth: 1, // The right border is drawn by the MarkdownTableCell component
+ borderTopWidth: 1,
+ flex: 1
+ },
+ tableBottomBorder: {
+ borderBottomWidth: 1,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2)
+ },
+ moreIndicator: {
+ bottom: 0,
+ height: 20,
+ position: 'absolute',
+ right: 0,
+ width: '100%'
+ }
+ };
+});
diff --git a/app/components/markdown/markdown_table_cell/index.js b/app/components/markdown/markdown_table_cell/index.js
new file mode 100644
index 000000000..83ec705f8
--- /dev/null
+++ b/app/components/markdown/markdown_table_cell/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import MarkdownTableCell from './markdown_table_cell';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(MarkdownTableCell);
diff --git a/app/components/markdown/markdown_table_cell/markdown_table_cell.js b/app/components/markdown/markdown_table_cell/markdown_table_cell.js
new file mode 100644
index 000000000..445700cce
--- /dev/null
+++ b/app/components/markdown/markdown_table_cell/markdown_table_cell.js
@@ -0,0 +1,46 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {Text, View} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class MarkdownTableCell extends React.PureComponent {
+ static propTypes = {
+ children: PropTypes.node,
+ isHeading: PropTypes.bool.isRequired,
+ theme: PropTypes.object.isRequired
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ const cellStyle = [style.cell];
+ if (this.props.isHeading) {
+ cellStyle.push(style.heading);
+ }
+
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ cell: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderRightWidth: 1,
+ flex: 1,
+ justifyContent: 'flex-start',
+ paddingHorizontal: 13,
+ paddingVertical: 6
+ }
+ };
+});
diff --git a/app/components/markdown/markdown_table_image/index.js b/app/components/markdown/markdown_table_image/index.js
new file mode 100644
index 000000000..4e7400b3f
--- /dev/null
+++ b/app/components/markdown/markdown_table_image/index.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import MarkdownTableImage from './markdown_table_image';
+
+function mapStateToProps(state) {
+ return {
+ serverURL: getCurrentUrl(state),
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(MarkdownTableImage);
diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.js b/app/components/markdown/markdown_table_image/markdown_table_image.js
new file mode 100644
index 000000000..6b02951e2
--- /dev/null
+++ b/app/components/markdown/markdown_table_image/markdown_table_image.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {PropTypes} from 'prop-types';
+import React from 'react';
+import {intlShape} from 'react-intl';
+import {Text} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+import {wrapWithPreventDoubleTap} from 'app/utils/tap';
+
+export default class MarkdownTableImage extends React.PureComponent {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ source: PropTypes.string.isRequired,
+ textStyle: CustomPropTypes.Style.isRequired,
+ navigator: PropTypes.object.isRequired,
+ serverURL: PropTypes.string.isRequired,
+ theme: PropTypes.object.isRequired
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired
+ };
+
+ handlePress = wrapWithPreventDoubleTap(() => {
+ const {navigator, theme} = this.props;
+
+ navigator.push({
+ screen: 'TableImage',
+ title: this.context.intl.formatMessage({
+ id: 'mobile.routes.tableImage',
+ defaultMessage: 'Image'
+ }),
+ animated: true,
+ backButtonTitle: '',
+ passProps: {
+ imageSource: this.getImageSource()
+ },
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg
+ }
+ });
+ });
+
+ getImageSource = () => {
+ let source = this.props.source;
+
+ if (source.startsWith('/')) {
+ source = `${this.props.serverURL}/${source}`;
+ }
+
+ return source;
+ };
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/app/components/markdown/markdown_table_row/index.js b/app/components/markdown/markdown_table_row/index.js
new file mode 100644
index 000000000..9cae21d02
--- /dev/null
+++ b/app/components/markdown/markdown_table_row/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import MarkdownTableRow from './markdown_table_row';
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(MarkdownTableRow);
diff --git a/app/components/markdown/markdown_table_row/markdown_table_row.js b/app/components/markdown/markdown_table_row/markdown_table_row.js
new file mode 100644
index 000000000..8ff62657d
--- /dev/null
+++ b/app/components/markdown/markdown_table_row/markdown_table_row.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {View} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class MarkdownTableRow extends React.PureComponent {
+ static propTypes = {
+ children: PropTypes.node,
+ isLastRow: PropTypes.bool,
+ theme: PropTypes.object.isRequired
+ };
+
+ render() {
+ const style = getStyleSheet(this.props.theme);
+
+ const rowStyle = [style.row];
+ if (!this.props.isLastRow) {
+ rowStyle.push(style.rowBottomBorder);
+ }
+
+ return {this.props.children};
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ row: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'flex-start'
+ },
+ rowBottomBorder: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderBottomWidth: 1
+ }
+ };
+});
diff --git a/app/components/markdown/transform.js b/app/components/markdown/transform.js
index fb68ed315..0b01ddd4e 100644
--- a/app/components/markdown/transform.js
+++ b/app/components/markdown/transform.js
@@ -27,134 +27,24 @@ export function addListItemIndices(ast) {
return ast;
}
-// Take all images and move them to be children of the root document node. When this happens, their
-// parent nodes are split into two, if necessary, with the version that follows the image having its
-// "continue" field set to true to indicate that things like bullet points don't need to be rendered.
+// Take all images, that aren't inside of tables, and move them to be children of the root document node.
+// When this happens, their parent nodes are split into two, if necessary, with the version that follows
+// the image having its "continue" field set to true to indicate that things like bullet points don't
+// need to be rendered.
export function pullOutImages(ast) {
for (let block = ast.firstChild; block !== null; block = block.next) {
+ // Skip tables because we'll render images inside of those as links
+ if (block.type === 'table') {
+ continue;
+ }
+
let node = block.firstChild;
let cameFromChild = false;
while (node && node !== block) {
if (node.type === 'image' && node.parent.type !== 'document') {
- const image = node;
-
- let parent = image.parent;
- let prev = image.prev;
- let next = image.next;
-
- // Remove image from its siblings
- if (prev) {
- prev._next = next;
- }
- if (next) {
- next._prev = prev;
-
- // Since the following text will be on a new line, a preceding space would cause the
- // alignment to be incorrect
- if (next.type === 'text' && next.literal.startsWith(' ')) {
- next.literal = next.literal.substring(1);
- }
- }
-
- // And from its parents
- if (parent._firstChild === image) {
- // image was the first child (ie prev is null), so the next sibling is now the first child
- parent._firstChild = next;
- }
- if (parent._lastChild === image) {
- // image was the last child (ie next is null), so the previous sibling is now the last child
- parent._lastChild = prev;
- }
-
- // Split the tree between the previous and next siblings, where the image would've been
- while (parent && parent.type !== 'document') {
- // We only need to split the parent if there's anything on the right of where we're splitting
- // in the current branch
- let parentCopy = null;
-
- // Split if we have children to the right of the split (next) or if we have any siblings to the
- // right of the parent (parent.next)
- if (next) {
- parentCopy = copyNodeWithoutNeighbors(parent);
-
- // Set an additional flag so we know not to re-render things like bullet points
- parentCopy.continue = true;
-
- // Re-assign the children to the right of the split to belong to the copy
- parentCopy._firstChild = next;
- parentCopy._lastChild = getLastSibling(next);
-
- if (parent._firstChild === next) {
- parent._firstChild = null;
- parent._lastChild = null;
- } else {
- parent._lastChild = prev;
- }
-
- // And re-assign the parent of all of those to be the copy
- for (let child = parentCopy.firstChild; child; child = child.next) {
- child._parent = parentCopy;
- }
-
- // Insert the copy as parent's next sibling
- if (parent.next) {
- parent.next._prev = parentCopy;
- parentCopy._next = parent.next;
- parent._next = parentCopy;
- } else /* if (parent.parent.lastChild === parent) */ {
- // Since parent has no next sibling, parent is the last child of its parent, so
- // we need to set the copy as the last child
- parent.parent.lastChild = parentCopy;
- }
- }
-
- // Change prev and next to no longer be siblings
- if (prev) {
- prev._next = null;
- }
-
- if (next) {
- next._prev = null;
- }
-
- // This image is part of a link so include the destination along with it
- if (parent.type === 'link') {
- image.linkDestination = parent.destination;
- }
-
- // Move up the tree
- next = parentCopy || parent.next;
- prev = parent;
- parent = parent.parent;
- }
-
- // Re-insert the image now that we have a tree split down to the root with the image's ancestors.
- // Note that parent is the root node, prev is the ancestor of image, and next is the ancestor of the copy
-
- // Add image to its parent
- image._parent = parent;
- if (next) {
- parent._lastChild = next;
- } else {
- // image is the last child of the root node now
- parent._lastChild = image;
- }
-
- // Add image to its siblings
- image._prev = prev;
- prev._next = image;
-
- image._next = next;
- if (next) {
- next._prev = image;
- }
-
- // The copy still needs its parent set to the root node
- if (next) {
- next._parent = parent;
- }
+ pullOutImage(node);
}
// Walk through tree to next node
@@ -174,6 +64,124 @@ export function pullOutImages(ast) {
return ast;
}
+function pullOutImage(image) {
+ let parent = image.parent;
+ let prev = image.prev;
+ let next = image.next;
+
+ // Remove image from its siblings
+ if (prev) {
+ prev._next = next;
+ }
+ if (next) {
+ next._prev = prev;
+
+ // Since the following text will be on a new line, a preceding space would cause the
+ // alignment to be incorrect
+ if (next.type === 'text' && next.literal.startsWith(' ')) {
+ next.literal = next.literal.substring(1);
+ }
+ }
+
+ // And from its parents
+ if (parent._firstChild === image) {
+ // image was the first child (ie prev is null), so the next sibling is now the first child
+ parent._firstChild = next;
+ }
+ if (parent._lastChild === image) {
+ // image was the last child (ie next is null), so the previous sibling is now the last child
+ parent._lastChild = prev;
+ }
+
+ // Split the tree between the previous and next siblings, where the image would've been
+ while (parent && parent.type !== 'document') {
+ // We only need to split the parent if there's anything on the right of where we're splitting
+ // in the current branch
+ let parentCopy = null;
+
+ // Split if we have children to the right of the split (next) or if we have any siblings to the
+ // right of the parent (parent.next)
+ if (next) {
+ parentCopy = copyNodeWithoutNeighbors(parent);
+
+ // Set an additional flag so we know not to re-render things like bullet points
+ parentCopy.continue = true;
+
+ // Re-assign the children to the right of the split to belong to the copy
+ parentCopy._firstChild = next;
+ parentCopy._lastChild = getLastSibling(next);
+
+ if (parent._firstChild === next) {
+ parent._firstChild = null;
+ parent._lastChild = null;
+ } else {
+ parent._lastChild = prev;
+ }
+
+ // And re-assign the parent of all of those to be the copy
+ for (let child = parentCopy.firstChild; child; child = child.next) {
+ child._parent = parentCopy;
+ }
+
+ // Insert the copy as parent's next sibling
+ if (parent.next) {
+ parent.next._prev = parentCopy;
+ parentCopy._next = parent.next;
+ parent._next = parentCopy;
+ } else /* if (parent.parent.lastChild === parent) */ {
+ // Since parent has no next sibling, parent is the last child of its parent, so
+ // we need to set the copy as the last child
+ parent.parent.lastChild = parentCopy;
+ }
+ }
+
+ // Change prev and next to no longer be siblings
+ if (prev) {
+ prev._next = null;
+ }
+
+ if (next) {
+ next._prev = null;
+ }
+
+ // This image is part of a link so include the destination along with it
+ if (parent.type === 'link') {
+ image.linkDestination = parent.destination;
+ }
+
+ // Move up the tree
+ next = parentCopy || parent.next;
+ prev = parent;
+ parent = parent.parent;
+ }
+
+ // Re-insert the image now that we have a tree split down to the root with the image's ancestors.
+ // Note that parent is the root node, prev is the ancestor of image, and next is the ancestor of the copy
+
+ // Add image to its parent
+ image._parent = parent;
+ if (next) {
+ parent._lastChild = next;
+ } else {
+ // image is the last child of the root node now
+ parent._lastChild = image;
+ }
+
+ // Add image to its siblings
+ image._prev = prev;
+ prev._next = image;
+
+ image._next = next;
+ if (next) {
+ next._prev = image;
+ }
+
+ // The copy still needs its parent set to the root node
+ if (next) {
+ next._parent = parent;
+ }
+}
+
// Copies a Node without its parent, children, or siblings
function copyNodeWithoutNeighbors(node) {
// commonmark uses classes so it takes a bit of work to copy them
diff --git a/app/screens/index.js b/app/screens/index.js
index 053d1f428..4d2619ff3 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -40,6 +40,8 @@ import Search from 'app/screens/search';
import SelectServer from 'app/screens/select_server';
import SelectTeam from 'app/screens/select_team';
import Settings from 'app/screens/settings/general';
+import Table from 'app/screens/table';
+import TableImage from 'app/screens/table_image';
import Thread from 'app/screens/thread';
import UserProfile from 'app/screens/user_profile';
@@ -95,6 +97,8 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider);
Navigation.registerComponent('Settings', () => wrapWithContextProvider(Settings), store, Provider);
Navigation.registerComponent('SSO', () => wrapWithContextProvider(SSO), store, Provider);
+ Navigation.registerComponent('Table', () => wrapWithContextProvider(Table), store, Provider);
+ Navigation.registerComponent('TableImage', () => wrapWithContextProvider(TableImage), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(Thread), store, Provider);
Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(UserProfile), store, Provider);
}
diff --git a/app/screens/table/index.js b/app/screens/table/index.js
new file mode 100644
index 000000000..c2442f17a
--- /dev/null
+++ b/app/screens/table/index.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Table from './table';
+
+export default Table;
diff --git a/app/screens/table/table.js b/app/screens/table/table.js
new file mode 100644
index 000000000..6e4cc6d8a
--- /dev/null
+++ b/app/screens/table/table.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {ScrollView, StyleSheet} from 'react-native';
+
+export default class Table extends React.PureComponent {
+ static propTypes = {
+ renderRows: PropTypes.func.isRequired
+ };
+
+ render() {
+ return (
+
+ {this.props.renderRows()}
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ scrollContainer: {
+ flex: 1
+ },
+ container: {
+ flexDirection: 'row'
+ }
+});
diff --git a/app/screens/table_image/index.js b/app/screens/table_image/index.js
new file mode 100644
index 000000000..53359b43e
--- /dev/null
+++ b/app/screens/table_image/index.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getDimensions} from 'app/selectors/device';
+
+import TableImage from './table_image';
+
+function mapStateToProps(state) {
+ return {
+ deviceWidth: getDimensions(state).deviceWidth
+ };
+}
+
+export default connect(mapStateToProps)(TableImage);
diff --git a/app/screens/table_image/table_image.js b/app/screens/table_image/table_image.js
new file mode 100644
index 000000000..575502a25
--- /dev/null
+++ b/app/screens/table_image/table_image.js
@@ -0,0 +1,102 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+ ActivityIndicator,
+ Image,
+ ScrollView,
+ StyleSheet,
+ View
+} from 'react-native';
+
+export default class TableImage extends React.PureComponent {
+ static propTypes = {
+ deviceWidth: PropTypes.number.isRequired,
+ imageSource: PropTypes.string.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ width: -1,
+ height: -1
+ };
+ }
+
+ componentWillMount() {
+ this.getImageSize(this.props.imageSource);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.imageSource !== nextProps.imageSource) {
+ this.setState({
+ width: -1,
+ height: -1
+ });
+
+ this.getImageSize(nextProps.imageSource);
+ }
+ }
+
+ getImageSize = (imageSource) => {
+ Image.getSize(imageSource, (width, height) => {
+ this.setState({
+ width,
+ height
+ });
+ });
+ }
+
+ render() {
+ let {width, height} = this.state;
+
+ if (width === -1 || height === -1) {
+ return (
+
+
+
+ );
+ }
+
+ if (width > this.props.deviceWidth) {
+ height = (height / width) * this.props.deviceWidth;
+ width = this.props.deviceWidth;
+ }
+
+ return (
+
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ scrollContainer: {
+ flex: 1
+ },
+ container: {
+ flex: 1,
+ flexDirection: 'column'
+ },
+ image: {
+ resizeMode: 'contain'
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center'
+ }
+});
diff --git a/app/utils/markdown.js b/app/utils/markdown.js
index 73e0ca58e..7940fd8c7 100644
--- a/app/utils/markdown.js
+++ b/app/utils/markdown.js
@@ -92,6 +92,9 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
},
error: {
color: theme.errorTextColor
+ },
+ table_header_row: {
+ fontWeight: '700'
}
};
});
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index b2df740b8..5c8c11bdb 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2058,7 +2058,7 @@
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.secured_by": "Secured by {vendor}",
"mobile.markdown.code.copy_code": "Copy Code",
- "mobile.markdown.code.plusMoreLines": "+{count, number} more lines",
+ "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}",
"mobile.markdown.image.error": "Image failed to load:",
"mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:",
"mobile.markdown.link.copy_url": "Copy URL",
@@ -2147,6 +2147,8 @@
"mobile.routes.selectTeam": "Select Team",
"mobile.routes.settings": "Settings",
"mobile.routes.sso": "Single Sign-On",
+ "mobile.routes.table": "Table",
+ "mobile.routes.tableImage": "Image",
"mobile.routes.thread": "{channelName} Thread",
"mobile.routes.thread_dm": "Direct Message Thread",
"mobile.routes.user_profile": "Profile",
diff --git a/package.json b/package.json
index 97eb71abd..f9b4f131c 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,8 @@
"dependencies": {
"analytics-react-native": "1.2.0",
"babel-polyfill": "6.26.0",
- "commonmark": "hmhealey/commonmark.js#dda6d89198252d5fd4bb04c4cc0668766c22fd77",
- "commonmark-react-renderer": "hmhealey/commonmark-react-renderer#1f078d53b7993d30b2762dd7d78fdd21ee84dd21",
+ "commonmark": "hmhealey/commonmark.js",
+ "commonmark-react-renderer": "hmhealey/commonmark-react-renderer",
"deep-equal": "1.0.1",
"fuse.js": "^3.2.0",
"intl": "1.2.5",
diff --git a/test/app/components/markdown/transform.test.js b/test/app/components/markdown/transform.test.js
index 7bdb0da7f..12befa5a1 100644
--- a/test/app/components/markdown/transform.test.js
+++ b/test/app/components/markdown/transform.test.js
@@ -338,6 +338,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -348,6 +349,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -383,6 +385,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -434,7 +437,8 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
- assert.deepEqual(actual, expected);
+ assert.equal(astToString(actual), astToString(expected));
+ assert.deepStrictEqual(actual, expected);
});
it('paragraph with multiple images', () => {
@@ -487,6 +491,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -643,6 +648,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -684,6 +690,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -745,6 +752,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -844,6 +852,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -917,6 +926,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -1131,6 +1141,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -1397,6 +1408,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -1699,7 +1711,8 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
- assert.deepEqual(actual, expected);
+ assert.equal(astToString(actual), astToString(expected));
+ assert.deepStrictEqual(actual, expected);
});
it('simple link', () => {
@@ -1743,6 +1756,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@@ -1808,6 +1822,157 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
+ assert.deepStrictEqual(actual, expected);
+ });
+
+ it('table', () => {
+ const input = makeAst({
+ type: 'document',
+ children: [{
+ type: 'table',
+ children: [{
+ type: 'table_row',
+ isHeading: true,
+ children: [{
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }]
+ }, {
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'text',
+ literal: 'This is '
+ }, {
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }, {
+ type: 'text',
+ literal: ' in a sentence.'
+ }]
+ }]
+ }, {
+ type: 'table_row',
+ isHeading: true,
+ children: [{
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'text',
+ literal: 'This is '
+ }, {
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }, {
+ type: 'text',
+ literal: ' in a sentence.'
+ }]
+ }, {
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }]
+ }]
+ }]
+ }]
+ });
+ const expected = makeAst({
+ type: 'document',
+ children: [{
+ type: 'table',
+ children: [{
+ type: 'table_row',
+ isHeading: true,
+ children: [{
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }]
+ }, {
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'text',
+ literal: 'This is '
+ }, {
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }, {
+ type: 'text',
+ literal: ' in a sentence.'
+ }]
+ }]
+ }, {
+ type: 'table_row',
+ isHeading: true,
+ children: [{
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'text',
+ literal: 'This is '
+ }, {
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }, {
+ type: 'text',
+ literal: ' in a sentence.'
+ }]
+ }, {
+ type: 'table_cell',
+ isHeading: true,
+ children: [{
+ type: 'image',
+ destination: 'http://example.com/image',
+ children: [{
+ type: 'text',
+ literal: 'image1'
+ }]
+ }]
+ }]
+ }]
+ }]
+ });
+ const actual = pullOutImages(input);
+
+ assert.ok(verifyAst(actual));
+ assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
});
diff --git a/yarn.lock b/yarn.lock
index 83ff5b6aa..6da55b474 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1708,9 +1708,9 @@ commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
-commonmark-react-renderer@hmhealey/commonmark-react-renderer#1f078d53b7993d30b2762dd7d78fdd21ee84dd21:
+commonmark-react-renderer@hmhealey/commonmark-react-renderer:
version "4.3.3"
- resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/1f078d53b7993d30b2762dd7d78fdd21ee84dd21"
+ resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/86fa63f898802953842526c2030f3b63c5d1ae7a"
dependencies:
in-publish "^2.0.0"
lodash.assign "^4.2.0"
@@ -1718,9 +1718,9 @@ commonmark-react-renderer@hmhealey/commonmark-react-renderer#1f078d53b7993d30b27
pascalcase "^0.1.1"
xss-filters "^1.2.6"
-commonmark@hmhealey/commonmark.js#dda6d89198252d5fd4bb04c4cc0668766c22fd77:
+commonmark@hmhealey/commonmark.js:
version "0.28.0"
- resolved "https://codeload.github.com/hmhealey/commonmark.js/tar.gz/dda6d89198252d5fd4bb04c4cc0668766c22fd77"
+ resolved "https://codeload.github.com/hmhealey/commonmark.js/tar.gz/f7bc747151e6d856ceffa196044112e23d04e541"
dependencies:
entities "~ 1.1.1"
mdurl "~ 1.0.1"