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(); + }); +});