diff --git a/app/components/message_attachments/action_button/action_button.js b/app/components/message_attachments/action_button/action_button.js
index 4209dc68a..86374fd04 100644
--- a/app/components/message_attachments/action_button/action_button.js
+++ b/app/components/message_attachments/action_button/action_button.js
@@ -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}
>
- {name}
+
);
}
@@ -56,6 +59,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.buttonColor,
fontSize: 12,
fontWeight: '600',
+ lineHeight: 13,
},
};
});
diff --git a/app/components/message_attachments/action_button/action_button_text.js b/app/components/message_attachments/action_button/action_button_text.js
new file mode 100644
index 000000000..6c07409a5
--- /dev/null
+++ b/app/components/message_attachments/action_button/action_button_text.js
@@ -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(
+
+ );
+ text = text.substring(match[0].length);
+ continue;
+ }
+
+ // Or an emoticon
+ if ((match = text.match(reEmoticon))) {
+ const emoticonName = getEmoticonName(match[0]);
+ if (emoticonName) {
+ components.push(
+
+ );
+ 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(
+
+ {match[0]}
+
+ );
+ text = text.substring(match[0].length);
+ }
+
+ return (
+
+ {components}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+});
+
+ActionButtonText.propTypes = {
+ message: PropTypes.string.isRequired,
+ style: PropTypes.object.isRequired,
+};
\ No newline at end of file
diff --git a/app/components/message_attachments/action_button/action_button_text.test.js b/app/components/message_attachments/action_button/action_button_text.test.js
new file mode 100644
index 000000000..1e55197c7
--- /dev/null
+++ b/app/components/message_attachments/action_button/action_button_text.test.js
@@ -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();
+
+ 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();
+
+ 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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/utils/emoji_utils.js b/app/utils/emoji_utils.js
index c95f41cb3..0dfc88f81 100644
--- a/app/utils/emoji_utils.js
+++ b/app/utils/emoji_utils.js
@@ -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};