[MM-14191] Render emojis in Interactive Message Buttons (#2640)
This commit is contained in:
parent
9ae71cf393
commit
6b5d0a49e5
4 changed files with 245 additions and 3 deletions
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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|<3)|(<\/3|<\/3)|(:[`'’]-?\(|:'\(|:'\())(?=$|\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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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)(:['’]-?\(|:'\(|:'\()(?=$|\s)/g, // :`(
|
||||
cry: /(^|\s)(:[`'’]-?\(|:'\(|:'\()(?=$|\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};
|
||||
|
|
|
|||
Loading…
Reference in a new issue