[MM-14191] Render emojis in Interactive Message Buttons (#2640)

This commit is contained in:
kosgrz 2019-04-09 17:41:39 +02:00 committed by Harrison Healey
parent 9ae71cf393
commit 6b5d0a49e5
4 changed files with 245 additions and 3 deletions

View file

@ -2,12 +2,12 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text} from 'react-native';
import PropTypes from 'prop-types';
import Button from 'react-native-button';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ActionButtonText from './action_button_text';
export default class ActionButton extends PureComponent {
static propTypes = {
@ -34,7 +34,10 @@ export default class ActionButton extends PureComponent {
containerStyle={style.button}
onPress={this.handleActionPress}
>
<Text style={style.text}>{name}</Text>
<ActionButtonText
message={name}
style={style.text}
/>
</Button>
);
}
@ -56,6 +59,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.buttonColor,
fontSize: 12,
fontWeight: '600',
lineHeight: 13,
},
};
});

View file

@ -0,0 +1,91 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {Text, View, StyleSheet} from 'react-native';
import Emoji from 'app/components/emoji';
import {getEmoticonName} from 'app/utils/emoji_utils';
// reEmoji matches an emoji (eg. :taco:) at the start of a string.
const reEmoji = /^:([a-z0-9_\-+]+):\B/i;
// reEmoticon matches an emoticon (eg. :D) at the start of a string.
const reEmoticon = /^(?:(:-?\))|(;-?\))|(:o)|(:-o)|(:-?])|(:-?d)|(x-d)|(:-?p)|(:-?[[@])|(:-?\()|(:[']-?\()|(:-?\/)|(:-?s)|(:-?\|)|(:-?\$)|(:-x)|(<3|&lt;3)|(<\/3|&lt;\/3)|(:[`']-?\(|:&#x27;\(|:&#39;\())(?=$|\s|[*_~?])/i;
// reMain matches some amount of plain text, starting at the beginning of the string and hopefully stopping right
// before the next emoji by looking for any character that could start an emoji (:, ;, x, or <)
const reMain = /^[\s\S]+?(?=[:;x<]|$)/i;
export default function ActionButtonText({message, style}) {
const components = [];
let text = message;
while (text) {
let match;
// See if the text starts with an emoji
if ((match = text.match(reEmoji))) {
components.push(
<Emoji
key={components.length}
literal={match[0]}
emojiName={match[1]}
textStyle={style}
/>
);
text = text.substring(match[0].length);
continue;
}
// Or an emoticon
if ((match = text.match(reEmoticon))) {
const emoticonName = getEmoticonName(match[0]);
if (emoticonName) {
components.push(
<Emoji
key={components.length}
literal={match[0]}
emojiName={emoticonName}
textStyle={style}
/>
);
text = text.substring(match[0].length);
continue;
}
}
// This is plain text, so capture as much text as possible until we hit the next possible emoji. Note that
// reMain always captures at least one character, so text will always be getting shorter
match = text.match(reMain);
components.push(
<Text
key={components.length}
style={style}
>
{match[0]}
</Text>
);
text = text.substring(match[0].length);
}
return (
<View style={styles.container}>
{components}
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
});
ActionButtonText.propTypes = {
message: PropTypes.string.isRequired,
style: PropTypes.object.isRequired,
};

View file

@ -0,0 +1,143 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import ActionButtonText from './action_button_text';
describe('ActionButtonText emojis', () => {
const emojis = [
{
name: 'smile',
literal: ':smile:',
},
{
name: 'custom_emoji',
literal: ':custom_emoji:',
},
{
name: 'heart',
literal: ':heart:',
},
{
name: 'one',
literal: ':one:',
},
{
name: 'slightly_smiling_face',
literal: ':)',
},
{
name: 'wink',
literal: ';)',
},
{
name: 'open_mouth',
literal: ':o',
},
{
name: 'scream',
literal: ':-o',
},
{
name: 'smirk',
literal: ':]',
},
{
name: 'smile',
literal: ':D',
},
{
name: 'stuck_out_tongue_closed_eyes',
literal: 'x-d',
},
{
name: 'stuck_out_tongue',
literal: ':p',
},
{
name: 'rage',
literal: ':@',
},
{
name: 'slightly_frowning_face',
literal: ':(',
},
{
name: 'cry',
literal: ':`(',
},
{
name: 'confused',
literal: ':/',
},
{
name: 'confounded',
literal: ':s',
},
{
name: 'neutral_face',
literal: ':|',
},
{
name: 'flushed',
literal: ':$',
},
{
name: 'mask',
literal: ':-x',
},
{
name: 'heart',
literal: '<3',
},
{
name: 'broken_heart',
literal: '</3',
},
{
name: '+1',
literal: ':+1:',
},
{
name: '-1',
literal: ':-1:',
},
];
emojis.forEach(({name, literal}) => {
test('only emoji ' + name, () => {
const baseProps = {message: literal, style: {fontSize: 12}};
const wrapper = shallow(<ActionButtonText {...baseProps}/>);
expect(wrapper.getElement().props.children.length).toBe(1);
const child = wrapper.getElement().props.children[0];
expect(child.type.displayName).toBe('Connect(Emoji)');
expect(child.props.emojiName).toBe(name);
expect(child.props.literal).toBe(literal);
expect(child.props.textStyle.fontSize).toBe(12);
});
test('emoji ' + name + ' with additional text', () => {
const baseProps = {message: 'emoji test with literal equals to ' + literal, style: {fontSize: 12}};
const wrapper = shallow(<ActionButtonText {...baseProps}/>);
expect(wrapper.getElement().props.children.length).toBe(2);
const textChild = wrapper.getElement().props.children[0];
const emoticonChild = wrapper.getElement().props.children[1];
expect(textChild.type.displayName).toBe('Text');
expect(textChild.props.children).toBe('emoji test with literal equals to ');
expect(textChild.props.style.fontSize).toBe(12);
expect(emoticonChild.props.emojiName).toBe(name);
expect(emoticonChild.props.literal).toBe(literal);
expect(emoticonChild.type.displayName).toBe('Connect(Emoji)');
expect(emoticonChild.props.textStyle.fontSize).toBe(12);
});
});
});

View file

@ -20,7 +20,7 @@ const RE_EMOTICON = {
stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p
rage: /(^|\s)(:-?[[@])(?=$|\s)/g, // :@
slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :(
cry: /(^|\s)(:[']-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
cry: /(^|\s)(:[`']-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s
neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
@ -46,6 +46,10 @@ function isEmoticon(text) {
return false;
}
export function getEmoticonName(value) {
return Object.keys(RE_EMOTICON).find((key) => value.match(RE_EMOTICON[key]) !== null);
}
export function hasEmojisOnly(message, customEmojis) {
if (!message || message.length === 0 || (/^\s{4}/).test(message)) {
return {isEmojiOnly: false, shouldRenderJumboEmoji: false};