diff --git a/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts
index 7fadd6cc8e8..c397377d3e8 100644
--- a/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts
@@ -53,7 +53,7 @@ describe('Recent Emoji', () => {
// # Submit post
const message = 'hi';
- cy.uiGetPostTextBox().and('have.value', `:${firstEmoji}: `).type(`${message} {enter}`);
+ cy.uiGetPostTextBox().and('have.value', '๐ ').type(`${message} {enter}`);
cy.uiWaitUntilMessagePostedIncludes(message);
// # Post reaction to post
@@ -68,11 +68,16 @@ describe('Recent Emoji', () => {
// * Verify recently used category is present in emoji picker
cy.findByText(/Recently Used/i).should('exist').and('be.visible');
- // * Assert first emoji should equal with second recent emoji
- cy.findAllByTestId('emojiItem').eq(0).should('have.attr', 'aria-label', 'grin emoji');
+ // * Assert both emojis appear in the recently used section (grin most recent, joy before it)
+ cy.findAllByTestId('emojiItem').then((items) => {
+ const labels = [...items].map((el) => el.getAttribute('aria-label'));
+ const grinIdx = labels.indexOf('grin emoji');
+ const joyIdx = labels.indexOf('joy emoji');
- // * Assert second emoji should equal with first recent emoji
- cy.findAllByTestId('emojiItem').eq(1).should('have.attr', 'aria-label', 'joy emoji');
+ expect(grinIdx, 'grin should be in recently used').to.be.greaterThan(-1);
+ expect(joyIdx, 'joy should be in recently used').to.be.greaterThan(-1);
+ expect(grinIdx, 'grin should appear before joy (more recent)').to.be.lessThan(joyIdx);
+ });
});
it('MM-T4463 Recently used custom emoji, when is deleted should be removed from recent emoji category and quick reactions', () => {
diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_insert_position_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_insert_position_spec.js
index 4dc78ab79b8..58c9ff1aeba 100644
--- a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_insert_position_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_insert_position_spec.js
@@ -30,11 +30,11 @@ describe('Messaging', () => {
// # Select the grinning emoji from the emoji picker.
cy.clickEmojiInEmojiPicker('grinning');
- // * The emoji should be inserted where the cursor is at the time of selection.
- cy.uiGetPostTextBox().should('have.value', 'Hello :grinning: World!');
+ // * The emoji should be inserted as a Unicode character where the cursor is at the time of selection.
+ cy.uiGetPostTextBox().should('have.value', 'Hello\uD83D\uDE00World!');
cy.uiGetPostTextBox().type('{enter}');
// * The emoji should be displayed in the post at the position inserted.
- cy.getLastPost().find('p').should('have.html', `Hello :grinning: World!`);
+ cy.getLastPost().find('p').should('contain', 'Hello').and('contain', 'World!');
});
});
diff --git a/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts b/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
index 8cf07c9ea25..6852c62d161 100644
--- a/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/emoji_picker/emoji_picker.spec.ts
@@ -38,15 +38,15 @@ test(
// * Verify emoji picker popup disappears
await emojiGifPickerPopup.notToBeVisible();
- // * Verify that the emoji was correctly added to the post textbox, followed by a space
- await expectPostCreateState(postCreate.input, ':slightly_smiling_face: ', '');
+ // * Verify that the emoji was correctly added to the post textbox (as unicode), followed by a space
+ await expectPostCreateState(postCreate.input, '๐ ', '');
// # Repeat those steps with another emoji
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('upside down face');
- // * Verify that the second emoji was correctly added to the post textbox, also followed by a space
- await expectPostCreateState(postCreate.input, ':slightly_smiling_face: :upside_down_face: ', '');
+ // * Verify that the second emoji was correctly added to the post textbox (as unicode), also followed by a space
+ await expectPostCreateState(postCreate.input, '๐ ๐ ', '');
// # Clear the textbox and replace it with some text
await postCreate.writeMessage('ab');
@@ -61,8 +61,8 @@ test(
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('face with raised eyebrow');
- // * Verify that the emoji was added with surrounding whitespace and that the caret is placed after that
- await expectPostCreateState(postCreate.input, 'a :face_with_raised_eyebrow: ', 'b');
+ // * Verify that the emoji was added with surrounding whitespace (as unicode) and that the caret is placed after that
+ await expectPostCreateState(postCreate.input, 'a ๐คจ ', 'b');
// # Clear the textbox and replace it with some words
await postCreate.writeMessage('this is a test');
@@ -80,8 +80,8 @@ test(
await postCreate.openEmojiPicker();
await emojiGifPickerPopup.clickEmoji('neutral face');
- // * Verify that the emoji was added without an extra space before it
- await expectPostCreateState(postCreate.input, 'this is a :neutral_face: ', 'test');
+ // * Verify that the emoji was added without an extra space before it (as unicode)
+ await expectPostCreateState(postCreate.input, 'this is a ๐ ', 'test');
},
);
diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
index 0833e3607d6..7ba9bcb4e82 100644
--- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
@@ -522,18 +522,18 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
expect(textbox).toHaveFocus();
- expect(textbox).toHaveValue(':blush: ');
- expect(textbox.selectionStart).toEqual(8);
- expect(textbox.selectionEnd).toEqual(8);
+ expect(textbox).toHaveValue('\uD83D\uDE0A ');
+ expect(textbox.selectionStart).toEqual(3);
+ expect(textbox.selectionEnd).toEqual(3);
// Do it again
await userEvent.click(screen.getByRole('button', {name: 'select an emoji'}));
await userEvent.click(screen.getByRole('button', {name: 'relaxed emoji'}));
expect(textbox).toHaveFocus();
- expect(textbox).toHaveValue(':blush: :relaxed: ');
- expect(textbox.selectionStart).toEqual(18);
- expect(textbox.selectionEnd).toEqual(18);
+ expect(textbox).toHaveValue('\uD83D\uDE0A \u263A\uFE0F ');
+ expect(textbox.selectionStart).toEqual(6);
+ expect(textbox.selectionEnd).toEqual(6);
});
it('should add a space after the existing text if needed', async () => {
@@ -553,9 +553,9 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
expect(textbox).toHaveFocus();
- expect(textbox).toHaveValue('This is some text :blush: ');
- expect(textbox.selectionStart).toEqual(26);
- expect(textbox.selectionEnd).toEqual(26);
+ expect(textbox).toHaveValue('This is some text \uD83D\uDE0A ');
+ expect(textbox.selectionStart).toEqual(21);
+ expect(textbox.selectionEnd).toEqual(21);
});
it('should be able to add an emoji in the middle of the text', async () => {
@@ -579,10 +579,8 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
expect(textbox).toHaveFocus();
- expect(textbox).toHaveValue('aaa :blush: bbb');
-
- // The caret should now be after the emoji
- expect(textbox.selectionStart).toEqual(12);
+ expect(textbox).toHaveValue('aaa \uD83D\uDE0A bbb');
+ expect(textbox.selectionStart).toEqual(7);
expect(textbox.selectionEnd).toEqual(textbox.selectionEnd);
});
@@ -607,10 +605,8 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
await userEvent.click(screen.getByRole('button', {name: 'blush emoji'}));
expect(textbox).toHaveFocus();
- expect(textbox).toHaveValue('aaa :blush: bbb');
-
- // The caret should now be after the emoji
- expect(textbox.selectionStart).toEqual(12);
+ expect(textbox).toHaveValue('aaa \uD83D\uDE0A bbb');
+ expect(textbox.selectionStart).toEqual(7);
expect(textbox.selectionEnd).toEqual(textbox.selectionEnd);
});
});
diff --git a/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx b/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
index a54169fddc3..ae4a6914855 100644
--- a/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/use_editor_emoji_picker.tsx
@@ -8,15 +8,16 @@ import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
-import type {Emoji} from '@mattermost/types/emojis';
+import type {Emoji, SystemEmoji} from '@mattermost/types/emojis';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
-import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
+import {getEmojiName, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
import useEmojiPicker, {useEmojiPickerOffset} from 'components/emoji_picker/use_emoji_picker';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import WithTooltip from 'components/with_tooltip';
+import {unifiedToUnicode} from 'utils/emoji_utils';
import {focusAndInsertText} from 'utils/exec_commands';
import {horizontallyWithin} from 'utils/floating';
@@ -56,17 +57,18 @@ const useEditorEmojiPicker = (
}, [textboxId]);
const handleEmojiClick = useCallback((emoji: Emoji) => {
- const emojiAlias = getEmojiName(emoji);
-
- if (!emojiAlias) {
- //Oops.. There went something wrong
- return;
+ if (isSystemEmoji(emoji)) {
+ insertTextAtCaret(unifiedToUnicode((emoji as SystemEmoji).unified));
+ } else {
+ const emojiAlias = getEmojiName(emoji);
+ if (!emojiAlias) {
+ return;
+ }
+ insertTextAtCaret(`:${emojiAlias}:`);
}
- insertTextAtCaret(`:${emojiAlias}:`);
-
setShowEmojiPicker(false);
- }, [insertTextAtCaret]);
+ }, [insertTextAtCaret, textboxId]);
const handleGifClick = useCallback((gif: string) => {
insertTextAtCaret(gif);
diff --git a/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx b/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
index aa290d23fc6..f2f1cb1822b 100644
--- a/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
+++ b/webapp/channels/src/components/edit_scheduled_post/edit_post.tsx
@@ -7,14 +7,14 @@ import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {EmoticonPlusOutlineIcon, InformationOutlineIcon} from '@mattermost/compass-icons/components';
-import type {Emoji} from '@mattermost/types/emojis';
+import type {Emoji, SystemEmoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts';
import type {ScheduledPost} from '@mattermost/types/schedule_post';
import {scheduledPostToPost} from '@mattermost/types/schedule_post';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import type {ActionResult} from 'mattermost-redux/types/actions';
-import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
+import {getEmojiName, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
import {openModal} from 'actions/views/modals';
import {getConnectionId} from 'selectors/general';
@@ -26,6 +26,7 @@ import Textbox from 'components/textbox';
import type {TextboxClass, TextboxElement} from 'components/textbox';
import {AppEvents, Constants, ModalIdentifiers, StoragePrefixes} from 'utils/constants';
+import {unifiedToUnicode} from 'utils/emoji_utils';
import * as Keyboard from 'utils/keyboard';
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
import {applyMarkdown} from 'utils/markdown/apply_markdown';
@@ -502,13 +503,19 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
return;
}
- const emojiAlias = getEmojiName(emoji);
- if (!emojiAlias) {
- //Oops.. There went something wrong
- return;
+ let emojiText: string;
+ if (isSystemEmoji(emoji)) {
+ emojiText = unifiedToUnicode((emoji as SystemEmoji).unified);
+ } else {
+ const emojiAlias = getEmojiName(emoji);
+ if (!emojiAlias) {
+ return;
+ }
+ emojiText = `:${emojiAlias}:`;
}
- let newMessage = `:${emojiAlias}: `;
+ const isUnicode = isSystemEmoji(emoji);
+ let newMessage = isUnicode ? emojiText : `${emojiText} `;
let newCaretPosition = newMessage.length;
if (editText.length > 0) {
@@ -517,10 +524,13 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
editText,
);
- // check whether the first piece of the message is empty when cursor
- // is placed at beginning of message and avoid adding an empty string at the beginning of the message
- newMessage = firstPiece === '' ? `:${emojiAlias}: ${lastPiece}` : `${firstPiece} :${emojiAlias}: ${lastPiece}`;
- newCaretPosition = firstPiece === '' ? `:${emojiAlias}: `.length : `${firstPiece} :${emojiAlias}: `.length;
+ if (isUnicode) {
+ newMessage = firstPiece + emojiText + lastPiece;
+ newCaretPosition = firstPiece.length + emojiText.length;
+ } else {
+ newMessage = firstPiece === '' ? `${emojiText} ${lastPiece}` : `${firstPiece} ${emojiText} ${lastPiece}`;
+ newCaretPosition = firstPiece === '' ? `${emojiText} `.length : `${firstPiece} ${emojiText} `.length;
+ }
}
draftRef.current = {
diff --git a/webapp/channels/src/components/suggestion/emoticon_provider.tsx b/webapp/channels/src/components/suggestion/emoticon_provider.tsx
index 0982613f4a3..74abe2adcc4 100644
--- a/webapp/channels/src/components/suggestion/emoticon_provider.tsx
+++ b/webapp/channels/src/components/suggestion/emoticon_provider.tsx
@@ -4,7 +4,7 @@
import React from 'react';
import {defineMessage} from 'react-intl';
-import type {Emoji} from '@mattermost/types/emojis';
+import type {Emoji, SystemEmoji} from '@mattermost/types/emojis';
import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis';
import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
@@ -12,7 +12,7 @@ import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_util
import {getEmojiMap, getRecentEmojisNames} from 'selectors/emojis';
import store from 'stores/redux_store';
-import {compareEmojis, emojiMatchesSkin} from 'utils/emoji_utils';
+import {compareEmojis, emojiMatchesSkin, unifiedToUnicode} from 'utils/emoji_utils';
import * as Emoticons from 'utils/emoticons';
import Provider from './provider';
@@ -29,7 +29,7 @@ type EmojiItem = {
}
const EmoticonSuggestion = React.forwardRef>((props, ref) => {
- const text = props.term;
+ const displayName = ':' + props.item.name + ':';
const emoji = props.item.emoji;
return (
@@ -45,7 +45,7 @@ const EmoticonSuggestion = React.forwardRef
- {text}
+ {displayName}
);
@@ -94,7 +94,12 @@ export default class EmoticonProvider extends Provider {
}
formatEmojis(emojis: EmojiItem[]) {
- return emojis.map((item) => ':' + item.name + ':');
+ return emojis.map((item) => {
+ if (isSystemEmoji(item.emoji)) {
+ return unifiedToUnicode((item.emoji as SystemEmoji).unified);
+ }
+ return ':' + item.name + ':';
+ });
}
// findAndSuggestEmojis uses the provided partialName to match anywhere inside an emoji name.
diff --git a/webapp/channels/src/utils/emoji_utils.test.tsx b/webapp/channels/src/utils/emoji_utils.test.tsx
index e85bd630c96..0658a0cb9fc 100644
--- a/webapp/channels/src/utils/emoji_utils.test.tsx
+++ b/webapp/channels/src/utils/emoji_utils.test.tsx
@@ -6,7 +6,7 @@ import React from 'react';
import {EmojiIndicesByAlias, Emojis} from 'utils/emoji';
import {TestHelper as TH} from 'utils/test_helper';
-import {compareEmojis, convertEmojiSkinTone, wrapEmojis} from './emoji_utils';
+import {compareEmojis, convertEmojiSkinTone, unifiedToUnicode, wrapEmojis} from './emoji_utils';
describe('compareEmojis', () => {
test('should sort an array of emojis alphabetically', () => {
@@ -393,6 +393,24 @@ describe('convertEmojiSkinTone', () => {
});
});
+describe('unifiedToUnicode', () => {
+ test('should convert a single codepoint', () => {
+ expect(unifiedToUnicode('1F600')).toBe('\uD83D\uDE00'); // ๐
+ });
+
+ test('should convert multi-codepoint emoji', () => {
+ expect(unifiedToUnicode('1F468-200D-1F469-200D-1F467')).toBe('\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67');
+ });
+
+ test('should convert skin tone variant', () => {
+ expect(unifiedToUnicode('1F64C-1F3FD')).toBe('\uD83D\uDE4C\uD83C\uDFFD');
+ });
+
+ test('should handle basic ASCII-range codepoints', () => {
+ expect(unifiedToUnicode('23-FE0F-20E3')).toBe('#\uFE0F\u20E3'); // #๏ธโฃ
+ });
+});
+
function getEmoji(name: string) {
return Emojis[EmojiIndicesByAlias.get(name)!];
}
diff --git a/webapp/channels/src/utils/emoji_utils.tsx b/webapp/channels/src/utils/emoji_utils.tsx
index 52a43210495..23dc137e8ec 100644
--- a/webapp/channels/src/utils/emoji_utils.tsx
+++ b/webapp/channels/src/utils/emoji_utils.tsx
@@ -179,6 +179,10 @@ export function getSkin(emoji: Emoji) {
return null;
}
+export function unifiedToUnicode(unified: string): string {
+ return unified.split('-').map((cp) => String.fromCodePoint(parseInt(cp, 16))).join('');
+}
+
export function trimmedEmojiName(emojiName: string) {
return emojiName.startsWith(':') && emojiName.endsWith(':') ? emojiName.slice(1, -1) : emojiName;
}