From 165ad0d4f791f8ae2109472d8a626d911fa368e0 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 30 Nov 2016 13:55:49 -0500 Subject: PLT-1378 Initial version of emoji reactions (#4520) * Refactored emoji.json to support multiple aliases and emoji categories * Added custom category to emoji.jsx and stabilized all fields * Removed conflicting aliases for :mattermost: and :ca: * fixup after store changes * Added emoji reactions * Removed reactions for an emoji when that emoji is deleted * Fixed incorrect test case * Renamed ReactionList to ReactionListView * Fixed :+1: and :-1: not showing up as possible reactions * Removed text emoticons from emoji reaction autocomplete * Changed emoji reactions to be sorted by the order that they were first created * Set a maximum number of listeners for the ReactionStore * Removed unused code from Textbox component * Fixed reaction permissions * Changed error code when trying to modify reactions for another user * Fixed merge conflicts * Properly applied theme colours to reactions * Fixed ESLint and gofmt errors * Fixed ReactionListContainer to properly update when its post prop changes * Removed unnecessary escape characters from reaction regexes * Shared reaction message pattern between CreatePost and CreateComment * Removed an unnecessary select query when saving a reaction * Changed reactions route to be under /reactions * Fixed copyright dates on newly added files * Removed debug code that prevented all unit tests from being ran * Cleaned up unnecessary code for reactions * Renamed ReactionStore.List to ReactionStore.GetForPost --- webapp/stores/emoji_store.jsx | 101 +++++++++++++++++++++++++-------------- webapp/stores/post_store.jsx | 21 ++++++-- webapp/stores/reaction_store.jsx | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 webapp/stores/reaction_store.jsx (limited to 'webapp/stores') diff --git a/webapp/stores/emoji_store.jsx b/webapp/stores/emoji_store.jsx index fd72b4636..3d9bf7875 100644 --- a/webapp/stores/emoji_store.jsx +++ b/webapp/stores/emoji_store.jsx @@ -5,12 +5,64 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; import EventEmitter from 'events'; -import EmojiJson from 'utils/emoji.json'; +import * as Emoji from 'utils/emoji.jsx'; const ActionTypes = Constants.ActionTypes; const CHANGE_EVENT = 'changed'; +// Wrap the contents of the store so that we don't need to construct an ES6 map where most of the content +// (the system emojis) will never change. It provides the get/has functions of a map and an iterator so +// that it can be used in for..of loops +class EmojiMap { + constructor(customEmojis) { + this.customEmojis = customEmojis; + + // Store customEmojis to an array so we can iterate it more easily + this.customEmojisArray = [...customEmojis]; + } + + has(name) { + return Emoji.EmojiIndicesByAlias.has(name) || this.customEmojis.has(name); + } + + get(name) { + if (Emoji.EmojiIndicesByAlias.has(name)) { + return Emoji.Emojis[Emoji.EmojiIndicesByAlias.get(name)]; + } + + return this.customEmojis.get(name); + } + + [Symbol.iterator]() { + const customEmojisArray = this.customEmojisArray; + + return { + systemIndex: 0, + customIndex: 0, + next() { + if (this.systemIndex < Emoji.Emojis.length) { + const emoji = Emoji.Emojis[this.systemIndex]; + + this.systemIndex += 1; + + return {value: [emoji.aliases[0], emoji]}; + } + + if (this.customIndex < customEmojisArray.length) { + const emoji = customEmojisArray[this.customIndex][1]; + + this.customIndex += 1; + + return {value: [emoji.name, emoji]}; + } + + return {done: true}; + } + }; + } +} + class EmojiStore extends EventEmitter { constructor() { super(); @@ -19,18 +71,10 @@ class EmojiStore extends EventEmitter { this.setMaxListeners(600); - this.emojis = new Map(EmojiJson); - this.systemEmojis = new Map(EmojiJson); - - this.unicodeEmojis = new Map(); - for (const [, emoji] of this.systemEmojis) { - if (emoji.unicode) { - this.unicodeEmojis.set(emoji.unicode, emoji); - } - } - this.receivedCustomEmojis = false; this.customEmojis = new Map(); + + this.map = new EmojiMap(this.customEmojis); } addChangeListener(callback) { @@ -50,20 +94,19 @@ class EmojiStore extends EventEmitter { } setCustomEmojis(customEmojis) { + customEmojis.sort((a, b) => a.name[0].localeCompare(b.name[0])); + this.customEmojis = new Map(); for (const emoji of customEmojis) { this.addCustomEmoji(emoji); } - this.sortCustomEmojis(); - this.updateEmojiMap(); + this.map = new EmojiMap(this.customEmojis); } addCustomEmoji(emoji) { this.customEmojis.set(emoji.name, emoji); - - // this doesn't update this.emojis, but it's only called by setCustomEmojis which does that afterwards } removeCustomEmoji(id) { @@ -73,21 +116,10 @@ class EmojiStore extends EventEmitter { break; } } - - this.updateEmojiMap(); - } - - sortCustomEmojis() { - this.customEmojis = new Map([...this.customEmojis.entries()].sort((a, b) => a[0].localeCompare(b[0]))); - } - - updateEmojiMap() { - // add custom emojis to the map first so that they can't override system ones - this.emojis = new Map([...this.customEmojis, ...this.systemEmojis]); } - getSystemEmojis() { - return this.systemEmojis; + hasSystemEmoji(name) { + return Emoji.EmojiIndicesByAlias.has(name); } getCustomEmojiMap() { @@ -95,24 +127,23 @@ class EmojiStore extends EventEmitter { } getEmojis() { - return this.emojis; + return this.map; } has(name) { - return this.emojis.has(name); + return this.map.has(name); } get(name) { - // prioritize system emojis so that custom ones can't override them - return this.emojis.get(name); + return this.map.get(name); } hasUnicode(codepoint) { - return this.unicodeEmojis.has(codepoint); + return Emoji.EmojiIndicesByUnicode.has(codepoint); } getUnicode(codepoint) { - return this.unicodeEmojis.get(codepoint); + return Emoji.Emojis[Emoji.EmojiIndicesByUnicode.get(codepoint)]; } getEmojiImageUrl(emoji) { @@ -121,7 +152,7 @@ class EmojiStore extends EventEmitter { return `/api/v3/emoji/${emoji.id}`; } - const filename = emoji.unicode || emoji.filename || emoji.name; + const filename = emoji.filename || emoji.aliases[0]; return Constants.EMOJI_PATH + '/' + filename + '.png'; } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index fbe5cd457..5e8155c40 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -118,7 +118,15 @@ class PostStoreClass extends EventEmitter { getEarliestPost(id) { if (this.postsInfo.hasOwnProperty(id)) { - return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[this.postsInfo[id].postList.order.length - 1]]; + const postList = this.postsInfo[id].postList; + + for (let i = postList.order.length - 1; i >= 0; i--) { + const postId = postList.order[i]; + + if (postList.posts[postId].state !== Constants.POST_DELETED) { + return postList.posts[postId]; + } + } } return null; @@ -126,7 +134,13 @@ class PostStoreClass extends EventEmitter { getLatestPost(id) { if (this.postsInfo.hasOwnProperty(id)) { - return this.postsInfo[id].postList.posts[this.postsInfo[id].postList.order[0]]; + const postList = this.postsInfo[id].postList; + + for (const postId of postList.order) { + if (postList.posts[postId].state !== Constants.POST_DELETED) { + return postList.posts[postId]; + } + } } return null; @@ -318,7 +332,8 @@ class PostStoreClass extends EventEmitter { // make sure to copy the post so that component state changes work properly postList.posts[post.id] = Object.assign({}, post, { state: Constants.POST_DELETED, - file_ids: [] + file_ids: [], + has_reactions: false }); } } diff --git a/webapp/stores/reaction_store.jsx b/webapp/stores/reaction_store.jsx new file mode 100644 index 000000000..166569e3d --- /dev/null +++ b/webapp/stores/reaction_store.jsx @@ -0,0 +1,92 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; +import EventEmitter from 'events'; + +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'changed'; + +class ReactionStore extends EventEmitter { + constructor() { + super(); + + this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this)); + + this.reactions = new Map(); + + this.setMaxListeners(600); + } + + addChangeListener(postId, callback) { + this.on(CHANGE_EVENT + postId, callback); + } + + removeChangeListener(postId, callback) { + this.removeListener(CHANGE_EVENT + postId, callback); + } + + emitChange(postId) { + this.emit(CHANGE_EVENT + postId, postId); + } + + setReactions(postId, reactions) { + this.reactions.set(postId, reactions); + } + + addReaction(postId, reaction) { + const reactions = []; + + for (const existing of this.getReactions(postId)) { + // make sure not to add duplicates + if (existing.user_id !== reaction.user_id || existing.post_id !== reaction.post_id || + existing.emoji_name !== reaction.emoji_name) { + reactions.push(existing); + } + } + + reactions.push(reaction); + + this.setReactions(postId, reactions); + } + + removeReaction(postId, reaction) { + const reactions = []; + + for (const existing of this.getReactions(postId)) { + if (existing.user_id !== reaction.user_id || existing.post_id !== reaction.post_id || + existing.emoji_name !== reaction.emoji_name) { + reactions.push(existing); + } + } + + this.setReactions(postId, reactions); + } + + getReactions(postId) { + return this.reactions.get(postId) || []; + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_REACTIONS: + this.setReactions(action.postId, action.reactions); + this.emitChange(action.postId); + break; + case ActionTypes.ADDED_REACTION: + this.addReaction(action.postId, action.reaction); + this.emitChange(action.postId); + break; + case ActionTypes.REMOVED_REACTION: + this.removeReaction(action.postId, action.reaction); + this.emitChange(action.postId); + break; + } + } +} + +export default new ReactionStore(); \ No newline at end of file -- cgit v1.2.3-1-g7c22