[MM-12353] Add feature to render jumbo emoji and unwrapped Emoji component with Text when rendering message containing emoji/s only (#2356)
* add feature to render jumbo emoji/s * fix bottom clipping of emoji on Android * updated per feedback
This commit is contained in:
parent
6df0f8b915
commit
d8abeabd70
8 changed files with 618 additions and 52 deletions
|
|
@ -0,0 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MarkdownEmoji should match snapshot 1`] = `
|
||||
<Unknown
|
||||
context={Array []}
|
||||
first={true}
|
||||
last={true}
|
||||
literal={null}
|
||||
nodeKey="5"
|
||||
>
|
||||
<Unknown
|
||||
context={
|
||||
Array [
|
||||
"paragraph",
|
||||
]
|
||||
}
|
||||
emojiName="smile"
|
||||
literal=":smile:"
|
||||
nodeKey="4"
|
||||
>
|
||||
<Unknown
|
||||
context={
|
||||
Array [
|
||||
"paragraph",
|
||||
"emoji",
|
||||
]
|
||||
}
|
||||
literal=":smile:"
|
||||
nodeKey="3"
|
||||
/>
|
||||
</Unknown>
|
||||
</Unknown>
|
||||
`;
|
||||
16
app/components/markdown/markdown_emoji/index.js
Normal file
16
app/components/markdown/markdown_emoji/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-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 MarkdownEmoji from './markdown_emoji';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(MarkdownEmoji);
|
||||
148
app/components/markdown/markdown_emoji/markdown_emoji.js
Normal file
148
app/components/markdown/markdown_emoji/markdown_emoji.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Node, Parser} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class MarkdownEmoji extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
isEdited: PropTypes.bool,
|
||||
shouldRenderJumboEmoji: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parser = this.createParser();
|
||||
this.renderer = this.createRenderer();
|
||||
}
|
||||
|
||||
createParser = () => {
|
||||
return new Parser();
|
||||
}
|
||||
|
||||
createRenderer = () => {
|
||||
return new Renderer({
|
||||
renderers: {
|
||||
editedIndicator: this.renderEditedIndicator,
|
||||
emoji: this.renderEmoji,
|
||||
paragraph: this.renderParagraph,
|
||||
text: this.renderText,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
computeTextStyle = (baseStyle) => {
|
||||
if (!this.props.shouldRenderJumboEmoji) {
|
||||
return baseStyle;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
return concatStyles(baseStyle, style.jumboEmoji);
|
||||
};
|
||||
|
||||
renderEmoji = ({context, emojiName, literal}) => {
|
||||
return (
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
literal={literal}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderParagraph = ({children}) => {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
return (
|
||||
<View style={style.block}>{children}</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderText = ({context, literal}) => {
|
||||
const style = this.computeTextStyle(this.props.baseTextStyle, context);
|
||||
|
||||
return <Text style={style}>{literal}</Text>;
|
||||
};
|
||||
|
||||
renderEditedIndicator = ({context}) => {
|
||||
let spacer = '';
|
||||
if (context[0] === 'paragraph') {
|
||||
spacer = ' ';
|
||||
}
|
||||
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
const styles = [
|
||||
this.props.baseTextStyle,
|
||||
style.editedIndicatorText,
|
||||
];
|
||||
|
||||
return (
|
||||
<Text style={styles}>
|
||||
{spacer}
|
||||
<FormattedText
|
||||
id='post_message_view.edited'
|
||||
defaultMessage='(edited)'
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const ast = this.parser.parse(this.props.value);
|
||||
|
||||
if (this.props.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);
|
||||
}
|
||||
}
|
||||
|
||||
return this.renderer.render(ast);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
// Android has trouble giving text transparency depending on how it's nested,
|
||||
// so we calculate the resulting colour manually
|
||||
const editedOpacity = Platform.select({
|
||||
ios: 0.3,
|
||||
android: 1.0,
|
||||
});
|
||||
const editedColor = Platform.select({
|
||||
ios: theme.centerChannelColor,
|
||||
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
|
||||
});
|
||||
|
||||
return {
|
||||
block: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
editedIndicatorText: {
|
||||
color: editedColor,
|
||||
opacity: editedOpacity,
|
||||
},
|
||||
jumboEmoji: {
|
||||
fontSize: 40,
|
||||
lineHeight: 50,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import MarkdownEmoji from './markdown_emoji';
|
||||
|
||||
describe('MarkdownEmoji', () => {
|
||||
const baseProps = {
|
||||
baseTextStyle: {color: '#3d3c40', fontSize: 15, lineHeight: 20},
|
||||
isEdited: false,
|
||||
shouldRenderJumboEmoji: true,
|
||||
theme: Preferences.THEMES.default,
|
||||
value: ':smile:',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<MarkdownEmoji {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
|||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {
|
||||
isEdited,
|
||||
|
|
@ -19,68 +21,79 @@ import {
|
|||
} from 'mattermost-redux/utils/post_utils';
|
||||
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {hasEmojisOnly} from 'app/utils/emoji_utils';
|
||||
|
||||
import PostBody from './post_body';
|
||||
|
||||
const POST_TIMEOUT = 20000;
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const post = getPost(state, ownProps.postId) || {};
|
||||
const channel = getChannel(state, post.channel_id) || {};
|
||||
function makeMapStateToProps() {
|
||||
const memoizeHasEmojisOnly = memoizeResult((message, customEmojis) => hasEmojisOnly(message, customEmojis));
|
||||
|
||||
let isFailed = post.failed;
|
||||
let isPending = post.id === post.pending_post_id;
|
||||
if (isPending && Date.now() - post.create_at > POST_TIMEOUT) {
|
||||
// Something has prevented the post from being set to failed, so it's safe to assume
|
||||
// that it has actually failed by this point
|
||||
isFailed = true;
|
||||
isPending = false;
|
||||
}
|
||||
return (state, ownProps) => {
|
||||
const post = getPost(state, ownProps.postId) || {};
|
||||
const channel = getChannel(state, post.channel_id) || {};
|
||||
|
||||
const isUserCanManageMembers = canManageChannelMembers(state);
|
||||
const isEphemeralPost = isPostEphemeral(post);
|
||||
let isFailed = post.failed;
|
||||
let isPending = post.id === post.pending_post_id;
|
||||
if (isPending && Date.now() - post.create_at > POST_TIMEOUT) {
|
||||
// Something has prevented the post from being set to failed, so it's safe to assume
|
||||
// that it has actually failed by this point
|
||||
isFailed = true;
|
||||
isPending = false;
|
||||
}
|
||||
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isSystemAdmin = checkIsSystemAdmin(roles);
|
||||
let canDelete = false;
|
||||
const isUserCanManageMembers = canManageChannelMembers(state);
|
||||
const isEphemeralPost = isPostEphemeral(post);
|
||||
|
||||
if (post && !ownProps.channelIsArchived) {
|
||||
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
|
||||
}
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
|
||||
const isAdmin = checkIsAdmin(roles);
|
||||
const isSystemAdmin = checkIsSystemAdmin(roles);
|
||||
let canDelete = false;
|
||||
|
||||
let isPostAddChannelMember = false;
|
||||
if (
|
||||
channel &&
|
||||
(channel.type === General.PRIVATE_CHANNEL || channel.type === General.OPEN_CHANNEL) &&
|
||||
isUserCanManageMembers &&
|
||||
isEphemeralPost &&
|
||||
post.props &&
|
||||
post.props.add_channel_member
|
||||
) {
|
||||
isPostAddChannelMember = true;
|
||||
}
|
||||
if (post && !ownProps.channelIsArchived) {
|
||||
canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin);
|
||||
}
|
||||
|
||||
return {
|
||||
postProps: post.props || {},
|
||||
postType: post.type || '',
|
||||
fileIds: post.file_ids,
|
||||
hasBeenDeleted: post.state === Posts.POST_DELETED,
|
||||
hasBeenEdited: isEdited(post),
|
||||
hasReactions: post.has_reactions,
|
||||
isFailed,
|
||||
isPending,
|
||||
isPostAddChannelMember,
|
||||
isPostEphemeral: isEphemeralPost,
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
theme: getTheme(state),
|
||||
canDelete,
|
||||
let isPostAddChannelMember = false;
|
||||
if (
|
||||
channel &&
|
||||
(channel.type === General.PRIVATE_CHANNEL || channel.type === General.OPEN_CHANNEL) &&
|
||||
isUserCanManageMembers &&
|
||||
isEphemeralPost &&
|
||||
post.props &&
|
||||
post.props.add_channel_member
|
||||
) {
|
||||
isPostAddChannelMember = true;
|
||||
}
|
||||
|
||||
const customEmojis = getCustomEmojisByName(state);
|
||||
const {isEmojiOnly, shouldRenderJumboEmoji} = memoizeHasEmojisOnly(post.message, customEmojis);
|
||||
|
||||
return {
|
||||
postProps: post.props || {},
|
||||
postType: post.type || '',
|
||||
fileIds: post.file_ids,
|
||||
hasBeenDeleted: post.state === Posts.POST_DELETED,
|
||||
hasBeenEdited: isEdited(post),
|
||||
hasReactions: post.has_reactions,
|
||||
isFailed,
|
||||
isPending,
|
||||
isPostAddChannelMember,
|
||||
isPostEphemeral: isEphemeralPost,
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
isEmojiOnly,
|
||||
shouldRenderJumboEmoji,
|
||||
theme: getTheme(state),
|
||||
canDelete,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, null, {withRef: true})(PostBody);
|
||||
export default connect(makeMapStateToProps, null, null, {withRef: true})(PostBody);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {Posts} from 'mattermost-redux/constants';
|
|||
import CombinedSystemMessage from 'app/components/combined_system_message';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import MarkdownEmoji from 'app/components/markdown/markdown_emoji';
|
||||
import ShowMoreButton from 'app/components/show_more_button';
|
||||
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
|
@ -58,6 +59,8 @@ export default class PostBody extends PureComponent {
|
|||
replyBarStyle: PropTypes.array,
|
||||
showAddReaction: PropTypes.bool,
|
||||
showLongPost: PropTypes.bool.isRequired,
|
||||
isEmojiOnly: PropTypes.bool.isRequired,
|
||||
shouldRenderJumboEmoji: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
|
|
@ -275,6 +278,7 @@ export default class PostBody extends PureComponent {
|
|||
hasBeenDeleted,
|
||||
hasBeenEdited,
|
||||
highlight,
|
||||
isEmojiOnly,
|
||||
isFailed,
|
||||
isPending,
|
||||
isPostAddChannelMember,
|
||||
|
|
@ -290,6 +294,7 @@ export default class PostBody extends PureComponent {
|
|||
postProps,
|
||||
postType,
|
||||
replyBarStyle,
|
||||
shouldRenderJumboEmoji,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {isLongPost, maxHeight} = this.state;
|
||||
|
|
@ -331,6 +336,17 @@ export default class PostBody extends PureComponent {
|
|||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (isEmojiOnly) {
|
||||
messageComponent = (
|
||||
<View style={style.row}>
|
||||
<MarkdownEmoji
|
||||
baseTextStyle={messageStyle}
|
||||
isEdited={hasBeenEdited}
|
||||
shouldRenderJumboEmoji={shouldRenderJumboEmoji}
|
||||
value={message}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else if (message.length) {
|
||||
messageComponent = (
|
||||
<View style={style.row}>
|
||||
|
|
|
|||
93
app/utils/emoji_utils.js
Normal file
93
app/utils/emoji_utils.js
Normal file
File diff suppressed because one or more lines are too long
220
app/utils/emoji_utils.test.js
Normal file
220
app/utils/emoji_utils.test.js
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {hasEmojisOnly} from './emoji_utils';
|
||||
|
||||
describe('hasEmojisOnly with named emojis', () => {
|
||||
const testCases = [{
|
||||
name: 'Named emoji',
|
||||
message: ':smile:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Valid custom emoji',
|
||||
message: ':valid_custom:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Invalid custom emoji',
|
||||
message: ':invalid_custom:',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Named emojis',
|
||||
message: ':smile: :heart:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Named emojis with white spaces',
|
||||
message: ' :smile: :heart: ',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Named emojis with potential custom emojis',
|
||||
message: ':smile: :heart: :valid_custom: :one:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Named emojis of 4 plus invalid :exceed: named emoji',
|
||||
message: ':smile: :heart: :valid_custom: :one: :exceed:',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Named emojis greater than max of 4',
|
||||
message: ':smile: :heart: :valid_custom: :one: :heart:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: 'smile',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: 'smile:',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: ':smile',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: ':smile::',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: '::smile:',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Mixed valid and invalid named emojis',
|
||||
message: ' :smile: invalid :heart: ',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}];
|
||||
|
||||
const customEmojis = new Map([['valid_custom', 0]]);
|
||||
for (const testCase of testCases) {
|
||||
it(`${testCase.name} - ${testCase.message}`, () => {
|
||||
expect(hasEmojisOnly(testCase.message, customEmojis)).toEqual(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasEmojisOnly with unicode emojis', () => {
|
||||
const testCases = [{
|
||||
name: 'Unicode emoji',
|
||||
message: '👍',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Unicode emoji',
|
||||
message: '🙌',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Unicode emojis',
|
||||
message: '🙌 👏',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Unicode emojis without whitespace in between',
|
||||
message: '🙌👏',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Unicode emojis with white spaces',
|
||||
message: ' 😣 😖 ',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: '4 unicode emojis',
|
||||
message: '😣 😖 🙌 👏',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Unicode emojis greater than max of 4',
|
||||
message: '😣 😖 🙌 👏 💩',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Unicode emoji',
|
||||
message: '\ud83d\udd5f',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Not valid unicode emoji',
|
||||
message: '\ud83d\udd5fnotvalid',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Mixed valid and invalid unicode emojis',
|
||||
message: '😣 invalid 😖',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}];
|
||||
|
||||
const customEmojis = new Map();
|
||||
for (const testCase of testCases) {
|
||||
it(`${testCase.name} - ${testCase.message}`, () => {
|
||||
expect(hasEmojisOnly(testCase.message, customEmojis)).toEqual(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasEmojisOnly with emoticons', () => {
|
||||
const testCases = [{
|
||||
name: 'Emoticon',
|
||||
message: ':)',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Emoticon',
|
||||
message: ':+1:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Emoticons',
|
||||
message: ':) :-o',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Emoticons with white spaces',
|
||||
message: ' :) :-o ',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: '4 emoticons',
|
||||
message: ':) :-o :+1: :|',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'Emoticons greater than max of 4',
|
||||
message: ':) :-o :+1: :| :p',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid emoticon',
|
||||
message: ':|:p',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Not valid named emoji',
|
||||
message: ':) :-o :+1::|',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'Mixed valid and invalid named emojis',
|
||||
message: ':) :-o invalid :|',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}];
|
||||
|
||||
const customEmojis = new Map();
|
||||
for (const testCase of testCases) {
|
||||
it(`${testCase.name} - ${testCase.message}`, () => {
|
||||
expect(hasEmojisOnly(testCase.message, customEmojis)).toEqual(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasEmojisOnly with empty and mixed emojis', () => {
|
||||
const testCases = [{
|
||||
name: 'not with empty message',
|
||||
message: '',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'not with empty message',
|
||||
message: ' ',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'not with no emoji pattern',
|
||||
message: 'smile',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'with invalid custom emoji',
|
||||
message: ':smile: :) :invalid_custom:',
|
||||
expected: {isEmojiOnly: false, shouldRenderJumboEmoji: false},
|
||||
}, {
|
||||
name: 'with named emoji and emoticon',
|
||||
message: ':smile: :) :valid_custom:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'with unicode emoji and emoticon',
|
||||
message: '👍 :)',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'with named and unicode emojis',
|
||||
message: ':smile: 👍',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'with named & unicode emojis and emoticon',
|
||||
message: ':smile: 👍 :)',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'with 4 named & unicode emojis and emoticon',
|
||||
message: ':smile: 👍 :) :heart:',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: true},
|
||||
}, {
|
||||
name: 'with named & unicode emojis and emoticon greater than max of 4',
|
||||
message: ':smile: 👍 :) :heart: 👏',
|
||||
expected: {isEmojiOnly: true, shouldRenderJumboEmoji: false},
|
||||
}];
|
||||
|
||||
const customEmojis = new Map([['valid_custom', 0]]);
|
||||
for (const testCase of testCases) {
|
||||
it(`${testCase.name} - ${testCase.message}`, () => {
|
||||
expect(hasEmojisOnly(testCase.message, customEmojis)).toEqual(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue