diff --git a/app/actions/views/emoji.js b/app/actions/views/emoji.js
index 23c51e1d0..5afc6aa27 100644
--- a/app/actions/views/emoji.js
+++ b/app/actions/views/emoji.js
@@ -9,6 +9,8 @@ import {Client4} from '@mm-redux/client';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';
+import {EmojiIndicesByAlias, EmojiIndicesByUnicode, Emojis} from '@utils/emojis';
+import emojiRegex from 'emoji-regex';
const getPostIdsForThread = makeGetPostIdsForThread();
@@ -98,3 +100,41 @@ async function getCustomEmojiByName(name) {
return null;
}
+
+export function addRecentUsedEmojisInMessage(message) {
+ return (dispatch) => {
+ const RE_UNICODE_EMOJI = emojiRegex();
+ const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_-]+):)/g;
+ const emojis = message.match(RE_UNICODE_EMOJI);
+ const namedEmojis = message.match(RE_NAMED_EMOJI);
+ function emojiUnicode(input) {
+ const emoji = [];
+ for (const i of input) {
+ emoji.push(i.codePointAt(0).toString(16));
+ }
+ return emoji.join('-');
+ }
+ const emojisAvailableWithMattermost = [];
+ if (emojis) {
+ for (const emoji of emojis) {
+ const unicode = emojiUnicode(emoji);
+ const index = EmojiIndicesByUnicode.get(unicode || '');
+ if (index) {
+ emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
+ }
+ }
+ }
+ if (namedEmojis) {
+ for (const emoji of namedEmojis) {
+ const index = EmojiIndicesByAlias.get(emoji.slice(1, -1));
+ if (index) {
+ emojisAvailableWithMattermost.push(Emojis[index].aliases[0]);
+ }
+ }
+ }
+ dispatch({
+ type: ViewTypes.ADD_RECENT_EMOJI_ARRAY,
+ emojis: emojisAvailableWithMattermost,
+ });
+ };
+}
diff --git a/app/components/emoji_picker/emoji_picker_base.js b/app/components/emoji_picker/emoji_picker_base.js
index ca8cceae0..70a1d65ce 100644
--- a/app/components/emoji_picker/emoji_picker_base.js
+++ b/app/components/emoji_picker/emoji_picker_base.js
@@ -232,15 +232,17 @@ export default class EmojiPicker extends PureComponent {
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / ((EMOJI_SIZE + 7) + (EMOJI_GUTTER * shorten)))));
};
- renderItem = ({item}) => {
+ renderItem = ({item, section}) => {
return (
-
+
+
+
);
};
diff --git a/app/components/post_draft/draft_input/draft_input.js b/app/components/post_draft/draft_input/draft_input.js
index f56b7591c..f4e75ad9c 100644
--- a/app/components/post_draft/draft_input/draft_input.js
+++ b/app/components/post_draft/draft_input/draft_input.js
@@ -59,6 +59,7 @@ export default class DraftInput extends PureComponent {
useGroupMentions: PropTypes.bool.isRequired,
channelMemberCountsByGroup: PropTypes.object,
groupsWithAllowReference: PropTypes.object,
+ addRecentUsedEmojisInMessage: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -162,7 +163,11 @@ export default class DraftInput extends PureComponent {
message: value,
};
- createPost(post, postFiles);
+ createPost(post, postFiles).then(({data}) => {
+ if (data) {
+ this.props.addRecentUsedEmojisInMessage(message);
+ }
+ });
if (postFiles.length) {
handleClearFiles(channelId, rootId);
diff --git a/app/components/post_draft/draft_input/draft_input.test.js b/app/components/post_draft/draft_input/draft_input.test.js
index 73af574e2..298eafb0a 100644
--- a/app/components/post_draft/draft_input/draft_input.test.js
+++ b/app/components/post_draft/draft_input/draft_input.test.js
@@ -10,11 +10,15 @@ import DraftInput from './draft_input';
jest.useFakeTimers();
+async function createPost() {
+ return {data: true, failed: false};
+}
+
describe('DraftInput', () => {
const baseProps = {
registerTypingAnimation: jest.fn(),
addReactionToLatestPost: jest.fn(),
- createPost: jest.fn(),
+ createPost: jest.fn(createPost),
executeCommand: jest.fn(),
handleCommentDraftChanged: jest.fn(),
handlePostDraftChanged: jest.fn(),
@@ -75,6 +79,7 @@ describe('DraftInput', () => {
},
},
membersCount: 10,
+ addRecentUsedEmojisInMessage: jest.fn(),
};
const ref = React.createRef();
diff --git a/app/components/post_draft/draft_input/index.js b/app/components/post_draft/draft_input/index.js
index 253ddaa9a..6fc35bfd9 100644
--- a/app/components/post_draft/draft_input/index.js
+++ b/app/components/post_draft/draft_input/index.js
@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {executeCommand} from '@actions/views/command';
-import {addReactionToLatestPost} from '@actions/views/emoji';
+import {addReactionToLatestPost, addRecentUsedEmojisInMessage} from '@actions/views/emoji';
import {handleClearFiles, handleClearFailedFiles} from '@actions/views/file_upload';
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
import {getChannelTimezones, getChannelMemberCountsByGroup} from '@mm-redux/actions/channels';
@@ -101,6 +101,7 @@ const mapDispatchToProps = {
handleClearFailedFiles,
setStatus,
getChannelMemberCountsByGroup,
+ addRecentUsedEmojisInMessage,
};
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(PostDraft);
diff --git a/app/components/reaction_picker/reaction_picker.js b/app/components/reaction_picker/reaction_picker.js
index 14a27caa6..2aff0abcf 100644
--- a/app/components/reaction_picker/reaction_picker.js
+++ b/app/components/reaction_picker/reaction_picker.js
@@ -18,6 +18,7 @@ import {
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import ReactionButton from './reaction_button';
+import {EmojiIndicesByAlias} from '@utils/emojis';
const ReactionPicker = (props) => {
const {theme} = props;
@@ -37,7 +38,11 @@ const ReactionPicker = (props) => {
iconSize = SMALL_ICON_SIZE;
}
- const emojis = Array.from(new Set(props.recentEmojis.concat(DEFAULT_EMOJIS))).splice(0, 6);
+ const recentEmojisIndices = props.recentEmojis.map((recentEmoji) => EmojiIndicesByAlias.get(recentEmoji));
+ const emojis = props.recentEmojis.
+ concat(DEFAULT_EMOJIS.filter((defaultEmoji) => !recentEmojisIndices.includes(EmojiIndicesByAlias.get(defaultEmoji)))).
+ splice(0, 6);
+
const list = emojis.map((emoji) => {
return (
MAXIMUM_RECENT_EMOJI) {
+ nextState.splice(MAXIMUM_RECENT_EMOJI);
+ }
return nextState;
}
+ case ViewTypes.ADD_RECENT_EMOJI_ARRAY: {
+ const nextRecentEmojis = action.emojis.reduce((currentState, emoji) => {
+ return [emoji, ...currentState.filter((currentEmoji) => currentEmoji !== emoji)];
+ }, state);
+ if (nextRecentEmojis.length > MAXIMUM_RECENT_EMOJI) {
+ nextRecentEmojis.splice(MAXIMUM_RECENT_EMOJI);
+ }
+ return nextRecentEmojis;
+ }
+
default:
return state;
}
diff --git a/detox/e2e/test/messaging/recent_emojis.e2e.js b/detox/e2e/test/messaging/recent_emojis.e2e.js
new file mode 100644
index 000000000..870742a8a
--- /dev/null
+++ b/detox/e2e/test/messaging/recent_emojis.e2e.js
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// *******************************************************************
+// - [#] indicates a test step (e.g. # Go to a screen)
+// - [*] indicates an assertion (e.g. * Check the title)
+// - Use element testID when selecting an element. Create one if none.
+// *******************************************************************
+
+import {ChannelScreen, AddReactionScreen} from '@support/ui/screen';
+import {Setup} from '@support/server_api';
+import {PostOptions} from '@support/ui/component';
+
+describe('Messaging', () => {
+ beforeAll(async () => {
+ const {user} = await Setup.apiInit();
+
+ // # Open channel screen
+ await ChannelScreen.open(user);
+ });
+
+ afterAll(async () => {
+ await ChannelScreen.logout();
+ });
+
+ it('MM-T3495 should include post message emojis in Recent Emojis section and Recently Used section', async () => {
+ // * Verify channel screen is visible
+ await ChannelScreen.toBeVisible();
+
+ const {
+ postMessage,
+ } = ChannelScreen;
+
+ // # Type message on post input
+ const message = 'The quick brown fox :fox_face: jumps over the lazy dog :dog:';
+ await postMessage(message);
+
+ // * Verify message is posted
+ await expect(element(by.text('The quick brown fox 🦊 jumps over the lazy dog 🐶'))).toBeVisible();
+
+ // # Open PostOptions
+ await element(by.text('The quick brown fox 🦊 jumps over the lazy dog 🐶')).longPress();
+ await PostOptions.toBeVisible();
+
+ // * Verify emojis exist
+ await expect(element(by.text('🦊'))).toExist();
+ await expect(element(by.text('🐶'))).toExist();
+
+ // # Open AddReaction screen
+ await AddReactionScreen.open();
+
+ // * Verify Emojis exist in recently used section
+ await expect(element(by.text('🦊').withAncestor(by.id('RECENTLY USED')))).toExist();
+ await expect(element(by.text('🐶').withAncestor(by.id('RECENTLY USED')))).toExist();
+
+ // # Close AddReaction Screen
+ await AddReactionScreen.close();
+ });
+});