ICU-624 Added markdown tables (#1434)
* ICU-624 Added initial table rendering support * ICU-624 Added full table view * Added proper pluralization to +X more lines in code blocks * ICU-624 Added support for images in tables * Stopped using injectIntl for tables * ICU-624 Updated commonmark-react-renderer * ICU-624 Hid scroll indicator in collapsed table view * Addressed feedback
This commit is contained in:
parent
84a2a0fab8
commit
ede4d22f75
21 changed files with 869 additions and 131 deletions
|
|
@ -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 (
|
||||
<MarkdownTableImage
|
||||
linkDestination={linkDestination}
|
||||
onLongPress={this.props.onLongPress}
|
||||
source={src}
|
||||
textStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.link]}
|
||||
navigator={this.props.navigator}
|
||||
>
|
||||
{reactChildren}
|
||||
</MarkdownTableImage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownImage
|
||||
linkDestination={linkDestination}
|
||||
|
|
@ -284,6 +307,14 @@ export default class Markdown extends PureComponent {
|
|||
return rendered;
|
||||
}
|
||||
|
||||
renderTable = ({children}) => {
|
||||
return (
|
||||
<MarkdownTable navigator={this.props.navigator}>
|
||||
{children}
|
||||
</MarkdownTable>
|
||||
);
|
||||
}
|
||||
|
||||
renderLink = ({children, href}) => {
|
||||
return (
|
||||
<MarkdownLink
|
||||
|
|
@ -337,7 +368,8 @@ export default class Markdown extends PureComponent {
|
|||
ast.appendChild(node);
|
||||
}
|
||||
}
|
||||
return <View>{this.renderer.render(ast)}</View>;
|
||||
|
||||
return this.renderer.render(ast);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class MarkdownCodeBlock extends React.PureComponent {
|
|||
<FormattedText
|
||||
style={style.plusMoreLinesText}
|
||||
id='mobile.markdown.code.plusMoreLines'
|
||||
defaultMessage='+{count, number} more lines'
|
||||
defaultMessage='+{count, number} more {count, plural, one {line} other {lines}}'
|
||||
values={{
|
||||
count: numberOfLines - MAX_LINES
|
||||
}}
|
||||
|
|
|
|||
16
app/components/markdown/markdown_table/index.js
Normal file
16
app/components/markdown/markdown_table/index.js
Normal file
|
|
@ -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 MarkdownTable from './markdown_table';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(MarkdownTable);
|
||||
146
app/components/markdown/markdown_table/markdown_table.js
Normal file
146
app/components/markdown/markdown_table/markdown_table.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {PropTypes} from 'prop-types';
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const MAX_HEIGHT = 120;
|
||||
|
||||
export default class MarkdownTable extends React.PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contentCutOff: true
|
||||
};
|
||||
}
|
||||
|
||||
handlePress = wrapWithPreventDoubleTap(() => {
|
||||
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 (
|
||||
<View style={tableStyle}>
|
||||
{rows}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
let moreIndicator = null;
|
||||
if (this.state.contentCutOff) {
|
||||
moreIndicator = (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
changeOpacity(this.props.theme.centerChannelColor, 0.0),
|
||||
changeOpacity(this.props.theme.centerChannelColor, 0.1)
|
||||
]}
|
||||
style={style.moreIndicator}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={this.handlePress}>
|
||||
<ScrollView
|
||||
onContentSizeChange={this.handleContentHeightChange}
|
||||
style={style.container}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{this.renderRows(false)}
|
||||
</ScrollView>
|
||||
{moreIndicator}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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%'
|
||||
}
|
||||
};
|
||||
});
|
||||
16
app/components/markdown/markdown_table_cell/index.js
Normal file
16
app/components/markdown/markdown_table_cell/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<View style={cellStyle}>
|
||||
<Text>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
cell: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 13,
|
||||
paddingVertical: 6
|
||||
}
|
||||
};
|
||||
});
|
||||
18
app/components/markdown/markdown_table_image/index.js
Normal file
18
app/components/markdown/markdown_table_image/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 (
|
||||
<Text
|
||||
onPress={this.handlePress}
|
||||
style={this.props.textStyle}
|
||||
>
|
||||
{this.props.children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
16
app/components/markdown/markdown_table_row/index.js
Normal file
16
app/components/markdown/markdown_table_row/index.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 <View style={rowStyle}>{this.props.children}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start'
|
||||
},
|
||||
rowBottomBorder: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
6
app/screens/table/index.js
Normal file
6
app/screens/table/index.js
Normal file
|
|
@ -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;
|
||||
32
app/screens/table/table.js
Normal file
32
app/screens/table/table.js
Normal file
|
|
@ -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 (
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
{this.props.renderRows()}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flex: 1
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
});
|
||||
16
app/screens/table_image/index.js
Normal file
16
app/screens/table_image/index.js
Normal file
|
|
@ -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);
|
||||
102
app/screens/table_image/table_image.js
Normal file
102
app/screens/table_image/table_image.js
Normal file
|
|
@ -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 (
|
||||
<View style={style.loadingContainer}>
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='large'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (width > this.props.deviceWidth) {
|
||||
height = (height / width) * this.props.deviceWidth;
|
||||
width = this.props.deviceWidth;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
contentContainerStyle={style.container}
|
||||
>
|
||||
<Image
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: this.props.imageSource}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
scrollContainer: {
|
||||
flex: 1
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
image: {
|
||||
resizeMode: 'contain'
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
|
@ -92,6 +92,9 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
|
|||
},
|
||||
error: {
|
||||
color: theme.errorTextColor
|
||||
},
|
||||
table_header_row: {
|
||||
fontWeight: '700'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue