[IMPROVEMENT] Markdown refactored (#1151)

This commit is contained in:
Djorkaeff Alexandre 2019-08-27 09:25:38 -03:00 committed by Diego Mello
parent 664563bd6f
commit c78732729d
31 changed files with 6658 additions and 2752 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Markdown from '../message/Markdown'; import Markdown from '../markdown';
import { getCustomEmoji } from '../message/utils'; import { getCustomEmoji } from '../message/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
@ -50,6 +50,7 @@ const styles = StyleSheet.create({
class ReplyPreview extends Component { class ReplyPreview extends Component {
static propTypes = { static propTypes = {
useMarkdown: PropTypes.bool,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired, close: PropTypes.func.isRequired,
@ -68,7 +69,7 @@ class ReplyPreview extends Component {
render() { render() {
const { const {
message, Message_TimeFormat, baseUrl, username message, Message_TimeFormat, baseUrl, username, useMarkdown
} = this.props; } = this.props;
const time = moment(message.ts).format(Message_TimeFormat); const time = moment(message.ts).format(Message_TimeFormat);
return ( return (
@ -78,7 +79,7 @@ class ReplyPreview extends Component {
<Text style={styles.username}>{message.u.username}</Text> <Text style={styles.username}>{message.u.username}</Text>
<Text style={styles.time}>{time}</Text> <Text style={styles.time}>{time}</Text>
</View> </View>
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} /> <Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} numberOfLines={1} useMarkdown={useMarkdown} />
</View> </View>
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} /> <CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
</View> </View>
@ -87,6 +88,7 @@ class ReplyPreview extends Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
useMarkdown: state.markdown.useMarkdown,
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}); });

View File

@ -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 (
<Text
style={mentionStyle}
onPress={handlePress}
>
{`@${ mention }`}
</Text>
);
});
AtMention.propTypes = {
mention: PropTypes.string,
username: PropTypes.string,
navToRoomInfo: PropTypes.func,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
export default AtMention;

View File

@ -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 }) => (
<View style={styles.container}>
<View style={styles.quote} />
<View style={styles.childContainer}>
{children}
</View>
</View>
));
BlockQuote.propTypes = {
children: PropTypes.node.isRequired
};
export default BlockQuote;

View File

@ -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 (
<CustomEmoji
baseUrl={baseUrl}
style={isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji}
emoji={emoji}
/>
);
}
return <Text style={isMessageContainsOnlyEmoji ? styles.textBig : styles.text}>{emojiUnicode}</Text>;
});
Emoji.propTypes = {
emojiName: PropTypes.string,
literal: PropTypes.string,
isMessageContainsOnlyEmoji: PropTypes.bool,
getCustomEmoji: PropTypes.func,
baseUrl: PropTypes.string
};
export default Emoji;

View File

@ -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 (
<Text
style={styles.mention}
onPress={handlePress}
>
{`#${ hashtag }`}
</Text>
);
}
return `#${ hashtag }`;
});
Hashtag.propTypes = {
hashtag: PropTypes.string,
navToRoomInfo: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
export default Hashtag;

View File

@ -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 (
<Text
onPress={handlePress}
style={styles.link}
>
{ childLength !== 0 ? children : link }
</Text>
);
});
Link.propTypes = {
children: PropTypes.node,
link: PropTypes.string
};
export default Link;

View File

@ -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;

View File

@ -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 (
<View style={style.container}>
<View style={[{ width: bulletWidth }, style.bullet]}>
<Text>
{bullet}
</Text>
</View>
<View style={style.contents}>
{children}
</View>
</View>
);
});
ListItem.propTypes = {
children: PropTypes.node,
bulletWidth: PropTypes.number,
level: PropTypes.number,
ordered: PropTypes.bool,
continue: PropTypes.bool,
index: PropTypes.number
};
export default ListItem;

View File

@ -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 (
<View style={tableStyle}>
{rows}
</View>
);
};
const onPress = () => Navigation.navigate('TableView', { renderRows, tableWidth: getTableWidth() });
return (
<TouchableOpacity onPress={onPress}>
<ScrollView
contentContainerStyle={{ width: getTableWidth() }}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
style={[styles.containerTable, { maxWidth: getTableWidth(), maxHeight: MAX_HEIGHT }]}
>
{renderRows(false)}
</ScrollView>
<Text style={styles.textInfo}>{I18n.t('Full_table')}</Text>
</TouchableOpacity>
);
});
Table.propTypes = {
children: PropTypes.node.isRequired,
numColumns: PropTypes.number.isRequired
};
export default Table;

View File

@ -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 (
<View style={[...cellStyle, { width: CELL_WIDTH }]}>
<Text style={textStyle}>
{children}
</Text>
</View>
);
});
TableCell.propTypes = {
align: PropTypes.oneOf(['', 'left', 'center', 'right']),
children: PropTypes.node,
isLastCell: PropTypes.bool
};
export default TableCell;

View File

@ -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 <View style={rowStyle}>{children}</View>;
});
TableRow.propTypes = {
children: PropTypes.node,
isLastRow: PropTypes.bool
};
export default TableRow;

View File

@ -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 <http://link|Text>
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 (
<Text
style={[
this.isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
...context.map(type => styles[type])
]}
numberOfLines={numberOfLines}
>
{literal}
</Text>
);
}
renderCodeInline = ({ literal }) => <Text style={styles.codeInline}>{literal}</Text>;
renderCodeBlock = ({ literal }) => <Text style={styles.codeBlock}>{literal}</Text>;
renderBreak = () => {
const { tmid } = this.props;
return <Text>{tmid ? ' ' : '\n'}</Text>;
}
renderParagraph = ({ children }) => {
const { numberOfLines } = this.props;
if (!children || children.length === 0) {
return null;
}
return (
<View style={styles.block}>
<Text numberOfLines={numberOfLines}>
{children}
</Text>
</View>
);
};
renderLink = ({ children, href }) => (
<MarkdownLink link={href}>
{children}
</MarkdownLink>
);
renderHashtag = ({ hashtag }) => {
const { channels, navToRoomInfo } = this.props;
return (
<MarkdownHashtag
hashtag={hashtag}
channels={channels}
navToRoomInfo={navToRoomInfo}
/>
);
}
renderAtMention = ({ mentionName }) => {
const { username, mentions, navToRoomInfo } = this.props;
return (
<MarkdownAtMention
mentions={mentions}
mention={mentionName}
username={username}
navToRoomInfo={navToRoomInfo}
/>
);
}
renderEmoji = ({ emojiName, literal }) => {
const { getCustomEmoji, baseUrl } = this.props;
return (
<MarkdownEmoji
emojiName={emojiName}
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
/>
);
}
renderImage = ({ src }) => <Image style={styles.inlineImage} source={{ uri: src }} />;
renderEditedIndicator = () => <Text style={styles.edited}> ({I18n.t('edited')})</Text>;
renderHeading = ({ children, level }) => {
const textStyle = styles[`heading${ level }Text`];
return (
<Text style={textStyle}>
{children}
</Text>
);
};
renderList = ({
children, start, tight, type
}) => (
<MarkdownList
ordered={type !== 'bullet'}
start={start}
tight={tight}
>
{children}
</MarkdownList>
);
renderListItem = ({
children, context, ...otherProps
}) => {
const level = context.filter(type => type === 'list').length;
return (
<MarkdownListItem
level={level}
{...otherProps}
>
{children}
</MarkdownListItem>
);
};
renderBlockQuote = ({ children }) => (
<MarkdownBlockQuote>
{children}
</MarkdownBlockQuote>
);
renderTable = ({ children, numColumns }) => (
<MarkdownTable numColumns={numColumns}>
{children}
</MarkdownTable>
);
renderTableRow = args => <MarkdownTableRow {...args} />;
renderTableCell = args => <MarkdownTableCell {...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 <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
}
const ast = this.parser.parse(m);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
this.editedMessage(ast);
return this.renderer.render(ast);
}
}

View File

@ -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'
}
});

View File

@ -9,7 +9,7 @@ import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown from './Markdown'; import Markdown from '../markdown';
import { getInfoMessage } from './utils'; import { getInfoMessage } from './utils';
const Content = React.memo((props) => { const Content = React.memo((props) => {
@ -21,14 +21,15 @@ const Content = React.memo((props) => {
<Markdown <Markdown
msg={props.msg} msg={props.msg}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
getCustomEmoji={props.getCustomEmoji}
username={props.user.username} username={props.user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
mentions={props.mentions}
channels={props.channels}
numberOfLines={props.tmid ? 1 : 0} numberOfLines={props.tmid ? 1 : 0}
getCustomEmoji={props.getCustomEmoji} channels={props.channels}
mentions={props.mentions}
useMarkdown={props.useMarkdown} useMarkdown={props.useMarkdown}
navToRoomInfo={props.navToRoomInfo} navToRoomInfo={props.navToRoomInfo}
tmid={props.tmid}
/> />
); );
} }
@ -43,16 +44,16 @@ const Content = React.memo((props) => {
Content.propTypes = { Content.propTypes = {
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
isInfo: PropTypes.bool, isInfo: PropTypes.bool,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
tmid: PropTypes.string, tmid: PropTypes.string,
msg: PropTypes.string, msg: PropTypes.string,
isEdited: PropTypes.bool,
useMarkdown: PropTypes.bool,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
navToRoomInfo: PropTypes.func, mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
getCustomEmoji: PropTypes.func navToRoomInfo: PropTypes.func
}; };
Content.displayName = 'MessageContent'; Content.displayName = 'MessageContent';

View File

@ -5,7 +5,7 @@ import FastImage from 'react-native-fast-image';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';

View File

@ -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 <http://link|Text>
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 <Text style={styles.text} numberOfLines={numberOfLines}>{m}</Text>;
}
const isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;
return (
<MarkdownRenderer
rules={{
paragraph: (node, children) => (
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
{children}
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
</Text>
),
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 (
<Text
style={mentionStyle}
key={key}
onPress={() => navToRoomInfo(navParam)}
>
&nbsp;{content}&nbsp;
</Text>
);
}
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 (
<Text
key={key}
style={styles.mention}
onPress={() => navToRoomInfo(navParam)}
>
&nbsp;#{content}&nbsp;
</Text>
);
}
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 (
<CustomEmoji
key={node.key}
baseUrl={baseUrl}
style={isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji}
emoji={emoji}
/>
);
}
return <Text key={node.key}>:{content}:</Text>;
}
return null;
},
hardbreak: () => null,
blocklink: () => null,
image: node => (
<Image key={node.key} style={styles.inlineImage} source={{ uri: node.attributes.src }} />
),
...rules
}}
style={{
paragraph: styles.paragraph,
text: isMessageContainsOnlyEmoji ? styles.textBig : styles.text,
codeInline: styles.codeInline,
codeBlock: styles.codeBlock,
link: styles.link,
...style
}}
plugins={plugins}
>{m}
</MarkdownRenderer>
);
});
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;

View File

@ -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);
}

View File

@ -5,7 +5,7 @@ import moment from 'moment';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Markdown from './Markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';

View File

@ -4,7 +4,7 @@ import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Markdown from './Markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';

View File

@ -1,15 +1,10 @@
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { import {
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE
} from '../../constants/colors'; } from '../../constants/colors';
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default StyleSheet.create({ export default StyleSheet.create({
root: { root: {
flexDirection: 'row' flexDirection: 'row'
@ -34,30 +29,6 @@ export default StyleSheet.create({
flexDirection: 'row' flexDirection: 'row'
// flex: 1 // 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 }, temp: { opacity: 0.3 },
marginTop: { marginTop: {
marginTop: 6 marginTop: 6
@ -143,28 +114,6 @@ export default StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textMedium ...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: { imageContainer: {
// flex: 1, // flex: 1,
flexDirection: 'column', flexDirection: 'column',
@ -186,29 +135,15 @@ export default StyleSheet.create({
height: 300, height: 300,
resizeMode: 'contain' resizeMode: 'contain'
}, },
edited: { text: {
fontSize: 14, fontSize: 16,
...sharedStyles.textColorDescription, ...sharedStyles.textColorNormal,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
codeInline: { textInfo: {
...sharedStyles.textRegular, fontStyle: 'italic',
...codeFontFamily, fontSize: 16,
borderWidth: 1, ...sharedStyles.textColorDescription,
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,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
startedDiscussion: { startedDiscussion: {

View File

@ -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_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',
Forgot_Password: 'Forgot Password', Forgot_Password: 'Forgot Password',
Full_table: 'Click to see full table',
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type', Group_by_type: 'Group by type',
Hide: 'Hide', Hide: 'Hide',
@ -360,6 +361,7 @@ export default {
Start_of_conversation: 'Start of conversation', Start_of_conversation: 'Start of conversation',
Started_discussion: 'Started a discussion:', Started_discussion: 'Started a discussion:',
Submit: 'Submit', Submit: 'Submit',
Table: 'Table',
Take_a_photo: 'Take a photo', Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video', Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status', tap_to_change_status: 'tap to change status',

View File

@ -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_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',
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_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo', Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal', Has_joined_the_channel: 'Entrou no canal',
@ -326,6 +327,7 @@ export default {
Start_of_conversation: 'Início da conversa', Start_of_conversation: 'Início da conversa',
Started_discussion: 'Iniciou uma discussão:', Started_discussion: 'Iniciou uma discussão:',
Submit: 'Enviar', Submit: 'Enviar',
Table: 'Tabela',
Take_a_photo: 'Tirar uma foto', Take_a_photo: 'Tirar uma foto',
Take_a_video: 'Gravar um vídeo', Take_a_video: 'Gravar um vídeo',
Terms_of_Service: ' Termos de Serviço ', Terms_of_Service: ' Termos de Serviço ',

View File

@ -122,6 +122,9 @@ const ChatsStack = createStackNavigator({
DirectoryView: { DirectoryView: {
getScreen: () => require('./views/DirectoryView').default getScreen: () => require('./views/DirectoryView').default
}, },
TableView: {
getScreen: () => require('./views/TableView').default
},
NotificationPrefView: { NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default getScreen: () => require('./views/NotificationPreferencesView').default
} }

View File

@ -8,7 +8,7 @@ import equal from 'deep-equal';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
import RCActivityIndicator from '../../containers/ActivityIndicator'; import RCActivityIndicator from '../../containers/ActivityIndicator';
import styles from './styles'; import styles from './styles';
import Markdown from '../../containers/message/Markdown'; import Markdown from '../../containers/markdown';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import Message from '../../containers/message/Message'; import Message from '../../containers/message/Message';

38
app/views/TableView.js Normal file
View File

@ -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 (
<ScrollView contentContainerStyle={{ width: tableWidth }}>
{renderRows()}
</ScrollView>
);
}
return (
<ScrollView>
<ScrollView horizontal>
{renderRows()}
</ScrollView>
</ScrollView>
);
}
}

View File

@ -24,6 +24,8 @@
}, },
"dependencies": { "dependencies": {
"@rocket.chat/sdk": "1.0.0-alpha.30", "@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", "bugsnag-react-native": "^2.22.3",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"ejson": "2.2.0", "ejson": "2.2.0",
@ -34,7 +36,6 @@
"js-base64": "^2.5.1", "js-base64": "^2.5.1",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"lodash": "4.17.15", "lodash": "4.17.15",
"markdown-it-flowdock": "0.3.8",
"moment": "^2.24.0", "moment": "^2.24.0",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react": "16.8.6", "react": "16.8.6",
@ -56,7 +57,6 @@
"react-native-keyboard-input": "^5.3.1", "react-native-keyboard-input": "^5.3.1",
"react-native-keyboard-tracking-view": "^5.5.0", "react-native-keyboard-tracking-view": "^5.5.0",
"react-native-localize": "^1.1.4", "react-native-localize": "^1.1.4",
"react-native-markdown-renderer": "^3.2.8",
"react-native-mime-types": "^2.2.1", "react-native-mime-types": "^2.2.1",
"react-native-modal": "^11.3.0", "react-native-modal": "^11.3.0",
"react-native-notifications": "^2.0.6", "react-native-notifications": "^2.0.6",
@ -90,7 +90,8 @@
"rn-user-defaults": "^1.3.5", "rn-user-defaults": "^1.3.5",
"semver": "6.3.0", "semver": "6.3.0",
"snyk": "1.210.0", "snyk": "1.210.0",
"strip-ansi": "5.2.0" "strip-ansi": "5.2.0",
"url-parse": "^1.4.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",

View File

@ -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.

View File

@ -91,6 +91,20 @@ export default (
<Separator title='Edited' /> <Separator title='Edited' />
<Message msg='Message' edited /> <Message msg='Message' edited />
<Separator title='Block Quote' />
<Message msg='> Testing block quote' />
<Message msg={'> Testing block quote\nTesting block quote'} />
<Separator title='Lists' />
<Message msg={'* Dogs\n * cats\n - cats'} />
<Separator title='Numerated lists' />
<Message msg={'1. Dogs \n 2. Cats'} />
<Separator title='Numerated lists in separated messages' />
<Message msg='1. Dogs' />
<Message msg='2. Cats' isHeader={false} />
<Separator title='Static avatar' /> <Separator title='Static avatar' />
<Message <Message
msg='Message' msg='Message'
@ -714,7 +728,7 @@ export default (
<Message msg='Message' style={[styles.normalize, { backgroundColor: '#ddd' }]} /> <Message msg='Message' style={[styles.normalize, { backgroundColor: '#ddd' }]} />
<Separator title='Markdown emphasis' /> <Separator title='Markdown emphasis' />
<Message msg='Italic with *asterisks* or _underscores_. Bold with **asterisks** or __underscores__. ~~Strikethrough~~' /> <Message msg='Italic with single _underscore_ or double __underscores__. Bold with single *asterisk* or double **asterisks**. Strikethrough with single ~Strikethrough~ or double ~~Strikethrough~~' />
<Separator title='Markdown headers' /> <Separator title='Markdown headers' />
<Message <Message

135
yarn.lock
View File

@ -1692,11 +1692,6 @@
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40"
integrity sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw== integrity sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw==
"@types/markdown-it@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.4.tgz#c5f67365916044b342dae8d702724788ba0b5b74"
integrity sha512-FWR7QB7EqBRq1s9BMk0ccOSOuRLfVEWYpHQYpFPaXtCoqN6dJx2ttdsdQbUxLLnAlKpYeVjveGGhQ3583TTa7g==
"@types/node@*": "@types/node@*":
version "12.6.9" version "12.6.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.9.tgz#ffeee23afdc19ab16e979338e7b536fdebbbaeaf" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.9.tgz#ffeee23afdc19ab16e979338e7b536fdebbbaeaf"
@ -1722,27 +1717,6 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/prop-types@*":
version "15.5.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"
integrity sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==
"@types/react-native@>=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": "@types/stack-utils@^1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" 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" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= 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: compare-versions@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.4.0.tgz#e0747df5c9cb7f054d6d3dc3e1dbc444f9e92b26" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.4.0.tgz#e0747df5c9cb7f054d6d3dc3e1dbc444f9e92b26"
@ -3473,11 +3466,6 @@ cssstyle@^1.0.0:
dependencies: dependencies:
cssom "0.3.x" 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: csstype@^2.5.7:
version "2.6.6" version "2.6.6"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" 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: dependencies:
once "^1.4.0" once "^1.4.0"
entities@~1.1.1: "entities@~ 1.1.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== 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" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= 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: load-json-file@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 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" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= 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: lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -7492,22 +7478,6 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" 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: markdown-to-jsx@^6.9.1:
version "6.10.3" version "6.10.3"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.3.tgz#7f0946684acd321125ff2de7fd258a9b9c7c40b7" 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" inherits "^2.0.1"
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
mdurl@^1.0.1: "mdurl@~ 1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
@ -7962,7 +7932,7 @@ minimist@0.0.8:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 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" version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
@ -9391,6 +9361,11 @@ querystringify@^2.0.0:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef"
integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== 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: random-bytes@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" 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" opencollective-postinstall "^2.0.0"
prop-types "^15.7.2" 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: react-native-gesture-handler@^1.3.0:
version "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" 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" resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-1.1.4.tgz#d48aa4f75afd39a42dcd0bdf40f7f44a8ccd604c"
integrity sha512-NHsA812yvoH9ktPl1IqIxwDDwykipyH7K4zeW/nnipZuQb2g73SQEB3ryqKHmRASWD0DZl0hIxHr9IszzG5W5w== 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: react-native-mime-types@^2.2.1:
version "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" 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" is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.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: string_decoder@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" 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" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"
integrity sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw== 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: uglify-es@^3.1.9:
version "3.3.9" version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" 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" querystringify "^2.0.0"
requires-port "^1.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: urlgrey@^0.4.4:
version "0.4.4" version "0.4.4"
resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f" 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" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= 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: xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"