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 👍 and 👎 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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
2bf0342d13
Коммит
165ad0d4f7
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
92
webapp/stores/reaction_store.jsx
Обычный файл
92
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();
|
||||
Ссылка в новой задаче
Block a user