summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/api.go1
-rw-r--r--api/emoji.go13
-rw-r--r--api/reaction.go203
-rw-r--r--api/reaction_test.go314
-rw-r--r--i18n/en.json76
-rw-r--r--model/client.go37
-rw-r--r--model/post.go1
-rw-r--r--model/reaction.go78
-rw-r--r--model/reaction_test.go64
-rw-r--r--model/websocket_message.go2
-rw-r--r--store/sql_reaction_store.go230
-rw-r--r--store/sql_reaction_store_test.go270
-rw-r--r--store/sql_store.go7
-rw-r--r--store/sql_upgrade.go16
-rw-r--r--store/store.go8
-rw-r--r--tests/test-emoticons1.md2
-rw-r--r--tests/test-emoticons3.md2
-rw-r--r--webapp/actions/post_actions.jsx20
-rw-r--r--webapp/actions/websocket_actions.jsx31
-rw-r--r--webapp/client/client.jsx37
-rw-r--r--webapp/components/create_comment.jsx74
-rw-r--r--webapp/components/create_post.jsx29
-rw-r--r--webapp/components/emoji/components/add_emoji.jsx2
-rw-r--r--webapp/components/post_view/components/post.jsx1
-rw-r--r--webapp/components/post_view/components/post_body.jsx6
-rw-r--r--webapp/components/post_view/components/reaction.jsx136
-rw-r--r--webapp/components/post_view/components/reaction_list_container.jsx82
-rw-r--r--webapp/components/post_view/components/reaction_list_view.jsx48
-rw-r--r--webapp/components/rhs_comment.jsx5
-rw-r--r--webapp/components/rhs_root_post.jsx5
-rw-r--r--webapp/components/rhs_thread.jsx1
-rw-r--r--webapp/components/suggestion/emoticon_provider.jsx32
-rw-r--r--webapp/components/textbox.jsx11
-rw-r--r--webapp/i18n/en.json5
-rw-r--r--webapp/images/emoji/basecamp.pngbin898 -> 0 bytes
-rw-r--r--webapp/images/emoji/basecampy.pngbin2951 -> 0 bytes
-rw-r--r--webapp/images/emoji/mattermost.png (renamed from webapp/images/emoji/mm.png)bin6576 -> 6576 bytes
-rw-r--r--webapp/sass/layout/_post.scss28
-rw-r--r--webapp/stores/emoji_store.jsx101
-rw-r--r--webapp/stores/post_store.jsx21
-rw-r--r--webapp/stores/reaction_store.jsx92
-rw-r--r--webapp/tests/client_reaction.test.jsx81
-rw-r--r--webapp/utils/async_client.jsx50
-rw-r--r--webapp/utils/constants.jsx8
-rw-r--r--webapp/utils/emoji.json1
-rw-r--r--webapp/utils/emoji.jsx18
-rw-r--r--webapp/utils/utils.jsx3
47 files changed, 2154 insertions, 98 deletions
diff --git a/api/api.go b/api/api.go
index 3af23b9e0..10dfaa7d5 100644
--- a/api/api.go
+++ b/api/api.go
@@ -103,6 +103,7 @@ func InitApi() {
InitEmoji()
InitStatus()
InitWebrtc()
+ InitReaction()
InitDeprecated()
// 404 on any api route before web.go has a chance to serve it
diff --git a/api/emoji.go b/api/emoji.go
index 9108db2ad..37adace49 100644
--- a/api/emoji.go
+++ b/api/emoji.go
@@ -209,11 +209,14 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
+ var emoji *model.Emoji
if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
- if c.Session.UserId != result.Data.(*model.Emoji).CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ emoji = result.Data.(*model.Emoji)
+
+ if c.Session.UserId != emoji.CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized
return
@@ -226,6 +229,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
}
go deleteEmojiImage(id)
+ go deleteReactionsForEmoji(emoji.Name)
ReturnStatusOK(w)
}
@@ -236,6 +240,13 @@ func deleteEmojiImage(id string) {
}
}
+func deleteReactionsForEmoji(emojiName string) {
+ if result := <-Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil {
+ l4g.Warn(utils.T("api.emoji.delete.delete_reactions.app_error"), emojiName)
+ l4g.Warn(result.Err)
+ }
+}
+
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "")
diff --git a/api/reaction.go b/api/reaction.go
new file mode 100644
index 000000000..5acf09f9e
--- /dev/null
+++ b/api/reaction.go
@@ -0,0 +1,203 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ l4g "github.com/alecthomas/log4go"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "net/http"
+)
+
+func InitReaction() {
+ l4g.Debug(utils.T("api.reaction.init.debug"))
+
+ BaseRoutes.NeedPost.Handle("/reactions/save", ApiUserRequired(saveReaction)).Methods("POST")
+ BaseRoutes.NeedPost.Handle("/reactions/delete", ApiUserRequired(deleteReaction)).Methods("POST")
+ BaseRoutes.NeedPost.Handle("/reactions", ApiUserRequired(listReactions)).Methods("GET")
+}
+
+func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
+ reaction := model.ReactionFromJson(r.Body)
+ if reaction == nil {
+ c.SetInvalidParam("saveReaction", "reaction")
+ return
+ }
+
+ if reaction.UserId != c.Session.UserId {
+ c.Err = model.NewLocAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ params := mux.Vars(r)
+
+ channelId := params["channel_id"]
+ if len(channelId) != 26 {
+ c.SetInvalidParam("saveReaction", "channelId")
+ return
+ }
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 || postId != reaction.PostId {
+ c.SetInvalidParam("saveReaction", "postId")
+ return
+ }
+
+ pchan := Srv.Store.Post().Get(reaction.PostId)
+
+ var postHadReactions bool
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId {
+ c.Err = model.NewLocAppError("saveReaction", "api.reaction.save_reaction.mismatched_channel_id.app_error",
+ nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ } else {
+ postHadReactions = post.HasReactions
+ }
+
+ if result := <-Srv.Store.Reaction().Save(reaction); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_ADDED, channelId, reaction, postHadReactions)
+
+ reaction := result.Data.(*model.Reaction)
+
+ w.Write([]byte(reaction.ToJson()))
+ }
+}
+
+func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
+ reaction := model.ReactionFromJson(r.Body)
+ if reaction == nil {
+ c.SetInvalidParam("deleteReaction", "reaction")
+ return
+ }
+
+ if reaction.UserId != c.Session.UserId {
+ c.Err = model.NewLocAppError("deleteReaction", "api.reaction.delete_reaction.user_id.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ params := mux.Vars(r)
+
+ channelId := params["channel_id"]
+ if len(channelId) != 26 {
+ c.SetInvalidParam("deleteReaction", "channelId")
+ return
+ }
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 || postId != reaction.PostId {
+ c.SetInvalidParam("deleteReaction", "postId")
+ return
+ }
+
+ pchan := Srv.Store.Post().Get(reaction.PostId)
+
+ var postHadReactions bool
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId {
+ c.Err = model.NewLocAppError("deleteReaction", "api.reaction.delete_reaction.mismatched_channel_id.app_error",
+ nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ } else {
+ postHadReactions = post.HasReactions
+ }
+
+ if result := <-Srv.Store.Reaction().Delete(reaction); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_REMOVED, channelId, reaction, postHadReactions)
+
+ ReturnStatusOK(w)
+ }
+}
+
+func sendReactionEvent(event string, channelId string, reaction *model.Reaction, postHadReactions bool) {
+ // send out that a reaction has been added/removed
+ go func() {
+ message := model.NewWebSocketEvent(event, "", channelId, "", nil)
+ message.Add("reaction", reaction.ToJson())
+
+ Publish(message)
+ }()
+
+ // send out that a post was updated if post.HasReactions has changed
+ go func() {
+ var post *model.Post
+ if result := <-Srv.Store.Post().Get(reaction.PostId); result.Err != nil {
+ l4g.Warn(utils.T("api.reaction.send_reaction_event.post.app_error"))
+ return
+ } else {
+ post = result.Data.(*model.PostList).Posts[reaction.PostId]
+ }
+
+ if post.HasReactions != postHadReactions {
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", channelId, "", nil)
+ message.Add("post", post.ToJson())
+
+ Publish(message)
+ }
+ }()
+}
+
+func listReactions(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ channelId := params["channel_id"]
+ if len(channelId) != 26 {
+ c.SetInvalidParam("deletePost", "channelId")
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 {
+ c.SetInvalidParam("listReactions", "postId")
+ return
+ }
+
+ pchan := Srv.Store.Post().Get(postId)
+
+ if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ return
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId {
+ c.Err = model.NewLocAppError("listReactions", "api.reaction.list_reactions.mismatched_channel_id.app_error",
+ nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if result := <-Srv.Store.Reaction().GetForPost(postId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ reactions := result.Data.([]*model.Reaction)
+
+ w.Write([]byte(model.ReactionsToJson(reactions)))
+ }
+}
diff --git a/api/reaction_test.go b/api/reaction_test.go
new file mode 100644
index 000000000..dad5a6a0c
--- /dev/null
+++ b/api/reaction_test.go
@@ -0,0 +1,314 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestSaveReaction(t *testing.T) {
+ th := Setup().InitBasic()
+
+ Client := th.BasicClient
+
+ user := th.BasicUser
+ user2 := th.BasicUser2
+
+ channel := th.BasicChannel
+ post := th.BasicPost
+
+ // saving a reaction
+ reaction := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ }
+ if returned, err := Client.SaveReaction(channel.Id, reaction); err != nil {
+ t.Fatal(err)
+ } else {
+ reaction = returned
+ }
+
+ if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction {
+ t.Fatal("didn't save reaction correctly")
+ }
+
+ // saving a duplicate reaction
+ if _, err := Client.SaveReaction(channel.Id, reaction); err != nil {
+ t.Fatal(err)
+ }
+
+ // saving a second reaction on a post
+ reaction2 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "sad",
+ }
+ if returned, err := Client.SaveReaction(channel.Id, reaction2); err != nil {
+ t.Fatal(err)
+ } else {
+ reaction2 = returned
+ }
+
+ if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 2 ||
+ (*reactions[0] != *reaction && *reactions[1] != *reaction) || (*reactions[0] != *reaction2 && *reactions[1] != *reaction2) {
+ t.Fatal("didn't save multiple reactions correctly")
+ }
+
+ // saving a reaction without a user id
+ reaction3 := &model.Reaction{
+ PostId: post.Id,
+ EmojiName: "smile",
+ }
+ if _, err := Client.SaveReaction(channel.Id, reaction3); err == nil {
+ t.Fatal("should've failed to save reaction without user id")
+ }
+
+ // saving a reaction without a post id
+ reaction4 := &model.Reaction{
+ UserId: user.Id,
+ EmojiName: "smile",
+ }
+ if _, err := Client.SaveReaction(channel.Id, reaction4); err == nil {
+ t.Fatal("should've failed to save reaction without post id")
+ }
+
+ // saving a reaction without a emoji name
+ reaction5 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ }
+ if _, err := Client.SaveReaction(channel.Id, reaction5); err == nil {
+ t.Fatal("should've failed to save reaction without emoji name")
+ }
+
+ // saving a reaction for another user
+ reaction6 := &model.Reaction{
+ UserId: user2.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ }
+ if _, err := Client.SaveReaction(channel.Id, reaction6); err == nil {
+ t.Fatal("should've failed to save reaction for another user")
+ }
+
+ // saving a reaction to a channel we're not a member of
+ th.LoginBasic2()
+ channel2 := th.CreateChannel(th.BasicClient, th.BasicTeam)
+ post2 := th.CreatePost(th.BasicClient, channel2)
+ th.LoginBasic()
+
+ reaction7 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post2.Id,
+ EmojiName: "smile",
+ }
+ if _, err := Client.SaveReaction(channel2.Id, reaction7); err == nil {
+ t.Fatal("should've failed to save reaction to a channel we're not a member of")
+ }
+
+ // saving a reaction to a direct channel
+ directChannel := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel)
+ directPost := th.CreatePost(th.BasicClient, directChannel)
+
+ reaction8 := &model.Reaction{
+ UserId: user.Id,
+ PostId: directPost.Id,
+ EmojiName: "smile",
+ }
+ if returned, err := Client.SaveReaction(directChannel.Id, reaction8); err != nil {
+ t.Fatal(err)
+ } else {
+ reaction8 = returned
+ }
+
+ if reactions := Client.MustGeneric(Client.ListReactions(directChannel.Id, directPost.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction8 {
+ t.Fatal("didn't save reaction correctly")
+ }
+
+ // saving a reaction for a post in the wrong channel
+ reaction9 := &model.Reaction{
+ UserId: user.Id,
+ PostId: directPost.Id,
+ EmojiName: "sad",
+ }
+ if _, err := Client.SaveReaction(channel.Id, reaction9); err == nil {
+ t.Fatal("should've failed to save reaction to a post that isn't in the given channel")
+ }
+}
+
+func TestDeleteReaction(t *testing.T) {
+ th := Setup().InitBasic()
+
+ Client := th.BasicClient
+
+ user := th.BasicUser
+ user2 := th.BasicUser2
+
+ channel := th.BasicChannel
+ post := th.BasicPost
+
+ reaction1 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ }
+
+ // deleting a reaction that does exist
+ Client.MustGeneric(Client.SaveReaction(channel.Id, reaction1))
+ if err := Client.DeleteReaction(channel.Id, reaction1); err != nil {
+ t.Fatal(err)
+ }
+
+ if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 0 {
+ t.Fatal("should've deleted reaction")
+ }
+
+ // deleting one reaction when a post has multiple
+ reaction2 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "sad",
+ }
+ reaction1 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction1)).(*model.Reaction)
+ reaction2 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction2)).(*model.Reaction)
+ if err := Client.DeleteReaction(channel.Id, reaction2); err != nil {
+ t.Fatal(err)
+ }
+
+ if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction1 {
+ t.Fatal("should've deleted only one reaction")
+ }
+
+ // deleting a reaction made by another user
+ reaction3 := &model.Reaction{
+ UserId: user2.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ }
+
+ th.LoginBasic2()
+ Client.Must(Client.JoinChannel(channel.Id))
+ reaction3 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction3)).(*model.Reaction)
+
+ th.LoginBasic()
+ if err := Client.DeleteReaction(channel.Id, reaction3); err == nil {
+ t.Fatal("should've failed to delete another user's reaction")
+ }
+
+ // deleting a reaction for a post we can't see
+ channel2 := th.CreateChannel(th.BasicClient, th.BasicTeam)
+ post2 := th.CreatePost(th.BasicClient, channel2)
+
+ reaction4 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post2.Id,
+ EmojiName: "smile",
+ }
+
+ reaction4 = Client.MustGeneric(Client.SaveReaction(channel2.Id, reaction4)).(*model.Reaction)
+ Client.Must(Client.LeaveChannel(channel2.Id))
+
+ if err := Client.DeleteReaction(channel2.Id, reaction4); err == nil {
+ t.Fatal("should've failed to delete a reaction from a channel we're not in")
+ }
+
+ // deleting a reaction for a post with the wrong channel
+ channel3 := th.CreateChannel(th.BasicClient, th.BasicTeam)
+
+ reaction5 := &model.Reaction{
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "happy",
+ }
+ if _, err := Client.SaveReaction(channel3.Id, reaction5); err == nil {
+ t.Fatal("should've failed to save reaction to a post that isn't in the given channel")
+ }
+}
+
+func TestListReactions(t *testing.T) {
+ th := Setup().InitBasic()
+
+ Client := th.BasicClient
+
+ user := th.BasicUser
+ user2 := th.BasicUser2
+
+ channel := th.BasicChannel
+
+ post := th.BasicPost
+
+ userReactions := []*model.Reaction{
+ {
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ },
+ {
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "happy",
+ },
+ {
+ UserId: user.Id,
+ PostId: post.Id,
+ EmojiName: "sad",
+ },
+ }
+
+ for i, reaction := range userReactions {
+ userReactions[i] = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction)).(*model.Reaction)
+ }
+
+ th.LoginBasic2()
+ Client.Must(Client.JoinChannel(channel.Id))
+
+ userReactions2 := []*model.Reaction{
+ {
+ UserId: user2.Id,
+ PostId: post.Id,
+ EmojiName: "smile",
+ },
+ {
+ UserId: user2.Id,
+ PostId: post.Id,
+ EmojiName: "sad",
+ },
+ }
+
+ for i, reaction := range userReactions2 {
+ userReactions2[i] = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction)).(*model.Reaction)
+ }
+
+ if reactions, err := Client.ListReactions(channel.Id, post.Id); err != nil {
+ t.Fatal(err)
+ } else if len(reactions) != 5 {
+ t.Fatal("should've returned 5 reactions")
+ } else {
+ checkForReaction := func(expected *model.Reaction) {
+ found := false
+
+ for _, reaction := range reactions {
+ if *reaction == *expected {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Fatalf("didn't return expected reaction %v", *expected)
+ }
+ }
+
+ for _, reaction := range userReactions {
+ checkForReaction(reaction)
+ }
+
+ for _, reaction := range userReactions2 {
+ checkForReaction(reaction)
+ }
+ }
+}
diff --git a/i18n/en.json b/i18n/en.json
index 1cf41377e..97bae3b1a 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -806,6 +806,10 @@
"translation": "Unable to create emoji. Image must be less than 1 MB in size."
},
{
+ "id": "api.emoji.delete.delete_reactions.app_error",
+ "translation": "Unable to delete reactions when deleting emoji with emoji name %v"
+ },
+ {
"id": "api.emoji.delete.permissions.app_error",
"translation": "Inappropriate permissions to delete emoji."
},
@@ -1504,6 +1508,22 @@
"translation": "session.user_id={{.SessionUserId}}, preference.user_id={{.PreferenceUserId}}"
},
{
+ "id": "api.reaction.delete_reaction.mismatched_channel_id.app_error",
+ "translation": "Failed to save reaction when channel id in URL doesn't match post id in URL"
+ },
+ {
+ "id": "api.reaction.list_reactions.mismatched_channel_id.app_error",
+ "translation": "Failed to get reactions when channel id in URL doesn't match post id in URL"
+ },
+ {
+ "id": "api.reaction.save_reaction.mismatched_channel_id.app_error",
+ "translation": "Failed to save reaction when channel id in URL doesn't match post id in URL"
+ },
+ {
+ "id": "api.reaction.send_reaction_event.post.app_error",
+ "translation": "Failed to get post when sending websocket event for reaction"
+ },
+ {
"id": "api.saml.save_certificate.app_error",
"translation": "Certificate did not save properly."
},
@@ -3708,6 +3728,22 @@
"translation": "Value is too long"
},
{
+ "id": "model.reaction.is_valid.create_at.app_error",
+ "translation": "Create at must be a valid time"
+ },
+ {
+ "id": "model.reaction.is_valid.emoji_name.app_error",
+ "translation": "Invalid emoji name"
+ },
+ {
+ "id": "model.reaction.is_valid.post_id.app_error",
+ "translation": "Invalid post id"
+ },
+ {
+ "id": "model.reaction.is_valid.user_id.app_error",
+ "translation": "Invalid user id"
+ },
+ {
"id": "model.team.is_valid.characters.app_error",
"translation": "Name must be 2 or more lowercase alphanumeric characters"
},
@@ -4576,6 +4612,46 @@
"translation": "We couldn't update the preference"
},
{
+ "id": "store.sql_reaction.delete.begin.app_error",
+ "translation": "Unable to open transaction while deleting reaction"
+ },
+ {
+ "id": "store.sql_reaction.delete.commit.app_error",
+ "translation": "Unable to commit transaction while deleting reaction"
+ },
+ {
+ "id": "store.sql_reaction.delete.save.app_error",
+ "translation": "Unable to delete reaction"
+ },
+ {
+ "id": "store.sql_reaction.delete_all_with_emoj_name.delete_reactions.app_error",
+ "translation": "Unable to delete reactions with the given emoji name"
+ },
+ {
+ "id": "store.sql_reaction.delete_all_with_emoj_name.get_reactions.app_error",
+ "translation": "Unable to get reactions with the given emoji name"
+ },
+ {
+ "id": "store.sql_reaction.delete_all_with_emoji_name.update_post.warn",
+ "translation": "Unable to update Post.HasReactions while removing reactions post_id=%v, error=%v"
+ },
+ {
+ "id": "store.sql_reaction.get_for_post.app_error",
+ "translation": "Unable to get reactions for post"
+ },
+ {
+ "id": "store.sql_reaction.save.begin.app_error",
+ "translation": "Unable to open transaction while saving reaction"
+ },
+ {
+ "id": "store.sql_reaction.save.commit.app_error",
+ "translation": "Unable to commit transaction while saving reaction"
+ },
+ {
+ "id": "store.sql_reaction.save.save.app_error",
+ "translation": "Unable to save reaction"
+ },
+ {
"id": "store.sql_session.analytics_session_count.app_error",
"translation": "We couldn't count the sessions"
},
diff --git a/model/client.go b/model/client.go
index 631de9c56..f782940d8 100644
--- a/model/client.go
+++ b/model/client.go
@@ -2102,6 +2102,7 @@ func (c *Client) DeleteEmoji(id string) (bool, *AppError) {
if r, err := c.DoApiPost(c.GetEmojiRoute()+"/delete", MapToJson(data)); err != nil {
return false, err
} else {
+ defer closeBody(r)
c.fillInExtraProperties(r)
return c.CheckStatusOK(r), nil
}
@@ -2132,6 +2133,7 @@ func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppErro
return AppErrorFromJson(rp.Body)
} else {
defer closeBody(rp)
+ c.fillInExtraProperties(rp)
return nil
}
}
@@ -2143,6 +2145,7 @@ func (c *Client) RemoveCertificateFile(filename string) *AppError {
return err
} else {
defer closeBody(r)
+ c.fillInExtraProperties(r)
return nil
}
}
@@ -2154,6 +2157,7 @@ func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{},
return nil, err
} else {
defer closeBody(r)
+ c.fillInExtraProperties(r)
return StringInterfaceFromJson(r.Body), nil
}
}
@@ -2182,3 +2186,36 @@ func (c *Client) GetFileInfosForPost(channelId string, postId string, etag strin
return FileInfosFromJson(r.Body), nil
}
}
+
+// Saves an emoji reaction for a post in the given channel. Returns the saved reaction if successful, otherwise returns an AppError.
+func (c *Client) SaveReaction(channelId string, reaction *Reaction) (*Reaction, *AppError) {
+ if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/save", reaction.PostId), reaction.ToJson()); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ c.fillInExtraProperties(r)
+ return ReactionFromJson(r.Body), nil
+ }
+}
+
+// Removes an emoji reaction for a post in the given channel. Returns nil if successful, otherwise returns an AppError.
+func (c *Client) DeleteReaction(channelId string, reaction *Reaction) *AppError {
+ if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/delete", reaction.PostId), reaction.ToJson()); err != nil {
+ return err
+ } else {
+ defer closeBody(r)
+ c.fillInExtraProperties(r)
+ return nil
+ }
+}
+
+// Lists all emoji reactions made for the given post in the given channel. Returns a list of Reactions if successful, otherwise returns an AppError.
+func (c *Client) ListReactions(channelId string, postId string) ([]*Reaction, *AppError) {
+ if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions", postId), "", ""); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ c.fillInExtraProperties(r)
+ return ReactionsFromJson(r.Body), nil
+ }
+}
diff --git a/model/post.go b/model/post.go
index da14b650f..b5dcc4539 100644
--- a/model/post.go
+++ b/model/post.go
@@ -38,6 +38,7 @@ type Post struct {
Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more
FileIds StringArray `json:"file_ids,omitempty"`
PendingPostId string `json:"pending_post_id" db:"-"`
+ HasReactions bool `json:"has_reactions,omitempty"`
}
func (o *Post) ToJson() string {
diff --git a/model/reaction.go b/model/reaction.go
new file mode 100644
index 000000000..afbdd1e88
--- /dev/null
+++ b/model/reaction.go
@@ -0,0 +1,78 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type Reaction struct {
+ UserId string `json:"user_id"`
+ PostId string `json:"post_id"`
+ EmojiName string `json:"emoji_name"`
+ CreateAt int64 `json:"create_at"`
+}
+
+func (o *Reaction) ToJson() string {
+ if b, err := json.Marshal(o); err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ReactionFromJson(data io.Reader) *Reaction {
+ var o Reaction
+
+ if err := json.NewDecoder(data).Decode(&o); err != nil {
+ return nil
+ } else {
+ return &o
+ }
+}
+
+func ReactionsToJson(o []*Reaction) string {
+ if b, err := json.Marshal(o); err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ReactionsFromJson(data io.Reader) []*Reaction {
+ var o []*Reaction
+
+ if err := json.NewDecoder(data).Decode(&o); err != nil {
+ return nil
+ } else {
+ return o
+ }
+}
+
+func (o *Reaction) IsValid() *AppError {
+ if len(o.UserId) != 26 {
+ return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.user_id.app_error", nil, "user_id="+o.UserId)
+ }
+
+ if len(o.PostId) != 26 {
+ return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.post_id.app_error", nil, "post_id="+o.PostId)
+ }
+
+ if len(o.EmojiName) == 0 || len(o.EmojiName) > 64 {
+ return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName)
+ }
+
+ if o.CreateAt == 0 {
+ return NewLocAppError("Reaction.IsValid", "model.reaction.is_valid.create_at.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func (o *Reaction) PreSave() {
+ if o.CreateAt == 0 {
+ o.CreateAt = GetMillis()
+ }
+}
diff --git a/model/reaction_test.go b/model/reaction_test.go
new file mode 100644
index 000000000..da73f477a
--- /dev/null
+++ b/model/reaction_test.go
@@ -0,0 +1,64 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestReactionIsValid(t *testing.T) {
+ reaction := Reaction{
+ UserId: NewId(),
+ PostId: NewId(),
+ EmojiName: "emoji",
+ CreateAt: GetMillis(),
+ }
+
+ if err := reaction.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ reaction.UserId = ""
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("user id should be invalid")
+ }
+
+ reaction.UserId = "1234garbage"
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("user id should be invalid")
+ }
+
+ reaction.UserId = NewId()
+ reaction.PostId = ""
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("post id should be invalid")
+ }
+
+ reaction.PostId = "1234garbage"
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("post id should be invalid")
+ }
+
+ reaction.PostId = NewId()
+ reaction.EmojiName = ""
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("emoji name should be invalid")
+ }
+
+ reaction.EmojiName = strings.Repeat("a", 65)
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("emoji name should be invalid")
+ }
+
+ reaction.EmojiName = strings.Repeat("a", 64)
+ if err := reaction.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+
+ reaction.CreateAt = 0
+ if err := reaction.IsValid(); err == nil {
+ t.Fatal("create at should be invalid")
+ }
+}
diff --git a/model/websocket_message.go b/model/websocket_message.go
index 3fa58aeb3..c3530c038 100644
--- a/model/websocket_message.go
+++ b/model/websocket_message.go
@@ -27,6 +27,8 @@ const (
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
+ WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added"
+ WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed"
)
type WebSocketMessage interface {
diff --git a/store/sql_reaction_store.go b/store/sql_reaction_store.go
new file mode 100644
index 000000000..7bd063a15
--- /dev/null
+++ b/store/sql_reaction_store.go
@@ -0,0 +1,230 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/go-gorp/gorp"
+)
+
+type SqlReactionStore struct {
+ *SqlStore
+}
+
+func NewSqlReactionStore(sqlStore *SqlStore) ReactionStore {
+ s := &SqlReactionStore{sqlStore}
+
+ for _, db := range sqlStore.GetAllConns() {
+ table := db.AddTableWithName(model.Reaction{}, "Reactions").SetKeys(false, "UserId", "PostId", "EmojiName")
+ table.ColMap("UserId").SetMaxSize(26)
+ table.ColMap("PostId").SetMaxSize(26)
+ table.ColMap("EmojiName").SetMaxSize(64)
+ }
+
+ return s
+}
+
+func (s SqlReactionStore) CreateIndexesIfNotExists() {
+ s.CreateIndexIfNotExists("idx_reactions_post_id", "Reactions", "PostId")
+}
+
+func (s SqlReactionStore) Save(reaction *model.Reaction) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ reaction.PreSave()
+ if result.Err = reaction.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if transaction, err := s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewLocAppError("SqlReactionStore.Save", "store.sql_reaction.save.begin.app_error", nil, err.Error())
+ } else {
+ err := saveReactionAndUpdatePost(transaction, reaction)
+
+ if err != nil {
+ transaction.Rollback()
+
+ // We don't consider duplicated save calls as an error
+ if !IsUniqueConstraintError(err.Error(), []string{"reactions_pkey", "PRIMARY"}) {
+ result.Err = model.NewLocAppError("SqlPreferenceStore.Save", "store.sql_reaction.save.save.app_error", nil, err.Error())
+ }
+ } else {
+ if err := transaction.Commit(); err != nil {
+ // don't need to rollback here since the transaction is already closed
+ result.Err = model.NewLocAppError("SqlPreferenceStore.Save", "store.sql_preference.save.commit.app_error", nil, err.Error())
+ }
+ }
+
+ if result.Err == nil {
+ result.Data = reaction
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlReactionStore) Delete(reaction *model.Reaction) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if transaction, err := s.GetMaster().Begin(); err != nil {
+ result.Err = model.NewLocAppError("SqlReactionStore.Delete", "store.sql_reaction.delete.begin.app_error", nil, err.Error())
+ } else {
+ err := deleteReactionAndUpdatePost(transaction, reaction)
+
+ if err != nil {
+ transaction.Rollback()
+
+ result.Err = model.NewLocAppError("SqlPreferenceStore.Delete", "store.sql_reaction.delete.app_error", nil, err.Error())
+ } else if err := transaction.Commit(); err != nil {
+ // don't need to rollback here since the transaction is already closed
+ result.Err = model.NewLocAppError("SqlPreferenceStore.Delete", "store.sql_preference.delete.commit.app_error", nil, err.Error())
+ } else {
+ result.Data = reaction
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func saveReactionAndUpdatePost(transaction *gorp.Transaction, reaction *model.Reaction) error {
+ if err := transaction.Insert(reaction); err != nil {
+ return err
+ }
+
+ return updatePostForReactions(transaction, reaction.PostId)
+}
+
+func deleteReactionAndUpdatePost(transaction *gorp.Transaction, reaction *model.Reaction) error {
+ if _, err := transaction.Exec(
+ `DELETE FROM
+ Reactions
+ WHERE
+ PostId = :PostId AND
+ UserId = :UserId AND
+ EmojiName = :EmojiName`,
+ map[string]interface{}{"PostId": reaction.PostId, "UserId": reaction.UserId, "EmojiName": reaction.EmojiName}); err != nil {
+ return err
+ }
+
+ return updatePostForReactions(transaction, reaction.PostId)
+}
+
+const (
+ // Set HasReactions = true if and only if the post has reactions, update UpdateAt only if HasReactions changes
+ UPDATE_POST_HAS_REACTIONS_QUERY = `UPDATE
+ Posts
+ SET
+ UpdateAt = (CASE
+ WHEN HasReactions != (SELECT count(0) > 0 FROM Reactions WHERE PostId = :PostId) THEN :UpdateAt
+ ELSE UpdateAt
+ END),
+ HasReactions = (SELECT count(0) > 0 FROM Reactions WHERE PostId = :PostId)
+ WHERE
+ Id = :PostId`
+)
+
+func updatePostForReactions(transaction *gorp.Transaction, postId string) error {
+ _, err := transaction.Exec(UPDATE_POST_HAS_REACTIONS_QUERY, map[string]interface{}{"PostId": postId, "UpdateAt": model.GetMillis()})
+
+ return err
+}
+
+func (s SqlReactionStore) GetForPost(postId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var reactions []*model.Reaction
+
+ if _, err := s.GetReplica().Select(&reactions,
+ `SELECT
+ *
+ FROM
+ Reactions
+ WHERE
+ PostId = :PostId
+ ORDER BY
+ CreateAt`, map[string]interface{}{"PostId": postId}); err != nil {
+ result.Err = model.NewLocAppError("SqlReactionStore.GetForPost", "store.sql_reaction.get_for_post.app_error", nil, "")
+ } else {
+ result.Data = reactions
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlReactionStore) DeleteAllWithEmojiName(emojiName string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ // doesn't use a transaction since it's better for this to half-finish than to not commit anything
+ var reactions []*model.Reaction
+
+ if _, err := s.GetReplica().Select(&reactions,
+ `SELECT
+ *
+ FROM
+ Reactions
+ WHERE
+ EmojiName = :EmojiName`, map[string]interface{}{"EmojiName": emojiName}); err != nil {
+ result.Err = model.NewLocAppError("SqlReactionStore.DeleteAllWithEmojiName",
+ "store.sql_reaction.delete_all_with_emoji_name.get_reactions.app_error", nil,
+ "emoji_name="+emojiName+", error="+err.Error())
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if _, err := s.GetMaster().Exec(
+ `DELETE FROM
+ Reactions
+ WHERE
+ EmojiName = :EmojiName`, map[string]interface{}{"EmojiName": emojiName}); err != nil {
+ result.Err = model.NewLocAppError("SqlReactionStore.DeleteAllWithEmojiName",
+ "store.sql_reaction.delete_all_with_emoji_name.delete_reactions.app_error", nil,
+ "emoji_name="+emojiName+", error="+err.Error())
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ for _, reaction := range reactions {
+ if _, err := s.GetMaster().Exec(UPDATE_POST_HAS_REACTIONS_QUERY,
+ map[string]interface{}{"PostId": reaction.PostId, "UpdateAt": model.GetMillis()}); err != nil {
+ l4g.Warn(utils.T("store.sql_reaction.delete_all_with_emoji_name.update_post.warn"), reaction.PostId, err.Error())
+ }
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_reaction_store_test.go b/store/sql_reaction_store_test.go
new file mode 100644
index 000000000..5a1cb2d67
--- /dev/null
+++ b/store/sql_reaction_store_test.go
@@ -0,0 +1,270 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package store
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestReactionSave(t *testing.T) {
+ Setup()
+
+ post := Must(store.Post().Save(&model.Post{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ })).(*model.Post)
+ firstUpdateAt := post.UpdateAt
+
+ reaction1 := &model.Reaction{
+ UserId: model.NewId(),
+ PostId: post.Id,
+ EmojiName: model.NewId(),
+ }
+ if result := <-store.Reaction().Save(reaction1); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if saved := result.Data.(*model.Reaction); saved.UserId != reaction1.UserId ||
+ saved.PostId != reaction1.PostId || saved.EmojiName != reaction1.EmojiName {
+ t.Fatal("should've saved reaction and returned it")
+ }
+
+ var secondUpdateAt int64
+ if postList := Must(store.Post().Get(reaction1.PostId)).(*model.PostList); !postList.Posts[post.Id].HasReactions {
+ t.Fatal("should've set HasReactions = true on post")
+ } else if postList.Posts[post.Id].UpdateAt == firstUpdateAt {
+ t.Fatal("should've marked post as updated when HasReactions changed")
+ } else {
+ secondUpdateAt = postList.Posts[post.Id].UpdateAt
+ }
+
+ if result := <-store.Reaction().Save(reaction1); result.Err != nil {
+ t.Log(result.Err)
+ t.Fatal("should've allowed saving a duplicate reaction")
+ }
+
+ // different user
+ reaction2 := &model.Reaction{
+ UserId: model.NewId(),
+ PostId: reaction1.PostId,
+ EmojiName: reaction1.EmojiName,
+ }
+ if result := <-store.Reaction().Save(reaction2); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ if postList := Must(store.Post().Get(reaction2.PostId)).(*model.PostList); postList.Posts[post.Id].UpdateAt != secondUpdateAt {
+ t.Fatal("shouldn't mark as updated when HasReactions hasn't changed")
+ }
+
+ // different post
+ reaction3 := &model.Reaction{
+ UserId: reaction1.UserId,
+ PostId: model.NewId(),
+ EmojiName: reaction1.EmojiName,
+ }
+ if result := <-store.Reaction().Save(reaction3); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ // different emoji
+ reaction4 := &model.Reaction{
+ UserId: reaction1.UserId,
+ PostId: reaction1.PostId,
+ EmojiName: model.NewId(),
+ }
+ if result := <-store.Reaction().Save(reaction4); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ // invalid reaction
+ reaction5 := &model.Reaction{
+ UserId: reaction1.UserId,
+ PostId: reaction1.PostId,
+ }
+ if result := <-store.Reaction().Save(reaction5); result.Err == nil {
+ t.Fatal("should've failed for invalid reaction")
+ }
+}
+
+func TestReactionDelete(t *testing.T) {
+ Setup()
+
+ post := Must(store.Post().Save(&model.Post{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ })).(*model.Post)
+
+ reaction := &model.Reaction{
+ UserId: model.NewId(),
+ PostId: post.Id,
+ EmojiName: model.NewId(),
+ }
+
+ Must(store.Reaction().Save(reaction))
+ firstUpdateAt := Must(store.Post().Get(reaction.PostId)).(*model.PostList).Posts[post.Id].UpdateAt
+
+ if result := <-store.Reaction().Delete(reaction); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ if result := <-store.Reaction().GetForPost(post.Id); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if len(result.Data.([]*model.Reaction)) != 0 {
+ t.Fatal("should've deleted reaction")
+ }
+
+ if postList := Must(store.Post().Get(post.Id)).(*model.PostList); postList.Posts[post.Id].HasReactions {
+ t.Fatal("should've set HasReactions = false on post")
+ } else if postList.Posts[post.Id].UpdateAt == firstUpdateAt {
+ t.Fatal("shouldn't mark as updated when HasReactions has changed after deleting reactions")
+ }
+}
+
+func TestReactionGetForPost(t *testing.T) {
+ Setup()
+
+ postId := model.NewId()
+
+ userId := model.NewId()
+
+ reactions := []*model.Reaction{
+ {
+ UserId: userId,
+ PostId: postId,
+ EmojiName: "smile",
+ },
+ {
+ UserId: model.NewId(),
+ PostId: postId,
+ EmojiName: "smile",
+ },
+ {
+ UserId: userId,
+ PostId: postId,
+ EmojiName: "sad",
+ },
+ {
+ UserId: userId,
+ PostId: model.NewId(),
+ EmojiName: "angry",
+ },
+ }
+
+ for _, reaction := range reactions {
+ Must(store.Reaction().Save(reaction))
+ }
+
+ if result := <-store.Reaction().GetForPost(postId); result.Err != nil {
+ t.Fatal(result.Err)
+ } else if returned := result.Data.([]*model.Reaction); len(returned) != 3 {
+ t.Fatal("should've returned 3 reactions")
+ } else {
+ for _, reaction := range reactions {
+ found := false
+
+ for _, returnedReaction := range returned {
+ if returnedReaction.UserId == reaction.UserId && returnedReaction.PostId == reaction.PostId &&
+ returnedReaction.EmojiName == reaction.EmojiName {
+ found = true
+ break
+ }
+ }
+
+ if !found && reaction.PostId == postId {
+ t.Fatalf("should've returned reaction for post %v", reaction)
+ } else if found && reaction.PostId != postId {
+ t.Fatal("shouldn't have returned reaction for another post")
+ }
+ }
+ }
+}
+
+func TestReactionDeleteAllWithEmojiName(t *testing.T) {
+ Setup()
+
+ emojiToDelete := model.NewId()
+
+ post := Must(store.Post().Save(&model.Post{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ })).(*model.Post)
+ post2 := Must(store.Post().Save(&model.Post{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ })).(*model.Post)
+ post3 := Must(store.Post().Save(&model.Post{
+ ChannelId: model.NewId(),
+ UserId: model.NewId(),
+ })).(*model.Post)
+
+ userId := model.NewId()
+
+ reactions := []*model.Reaction{
+ {
+ UserId: userId,
+ PostId: post.Id,
+ EmojiName: emojiToDelete,
+ },
+ {
+ UserId: model.NewId(),
+ PostId: post.Id,
+ EmojiName: emojiToDelete,
+ },
+ {
+ UserId: userId,
+ PostId: post.Id,
+ EmojiName: "sad",
+ },
+ {
+ UserId: userId,
+ PostId: post2.Id,
+ EmojiName: "angry",
+ },
+ {
+ UserId: userId,
+ PostId: post3.Id,
+ EmojiName: emojiToDelete,
+ },
+ }
+
+ for _, reaction := range reactions {
+ Must(store.Reaction().Save(reaction))
+ }
+
+ if result := <-store.Reaction().DeleteAllWithEmojiName(emojiToDelete); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ // check that the reactions were deleted
+ if returned := Must(store.Reaction().GetForPost(post.Id)).([]*model.Reaction); len(returned) != 1 {
+ t.Fatal("should've only removed reactions with emoji name")
+ } else {
+ for _, reaction := range returned {
+ if reaction.EmojiName == "smile" {
+ t.Fatal("should've removed reaction with emoji name")
+ }
+ }
+ }
+
+ if returned := Must(store.Reaction().GetForPost(post2.Id)).([]*model.Reaction); len(returned) != 1 {
+ t.Fatal("should've only removed reactions with emoji name")
+ }
+
+ if returned := Must(store.Reaction().GetForPost(post3.Id)).([]*model.Reaction); len(returned) != 0 {
+ t.Fatal("should've only removed reactions with emoji name")
+ }
+
+ // check that the posts are updated
+ if postList := Must(store.Post().Get(post.Id)).(*model.PostList); !postList.Posts[post.Id].HasReactions {
+ t.Fatal("post should still have reactions")
+ }
+
+ if postList := Must(store.Post().Get(post2.Id)).(*model.PostList); !postList.Posts[post2.Id].HasReactions {
+ t.Fatal("post should still have reactions")
+ }
+
+ if postList := Must(store.Post().Get(post3.Id)).(*model.PostList); postList.Posts[post3.Id].HasReactions {
+ t.Fatal("post shouldn't have reactions any more")
+ }
+}
diff --git a/store/sql_store.go b/store/sql_store.go
index 215e0f894..6a852430c 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -84,6 +84,7 @@ type SqlStore struct {
emoji EmojiStore
status StatusStore
fileInfo FileInfoStore
+ reaction ReactionStore
SchemaVersion string
rrCounter int64
}
@@ -134,6 +135,7 @@ func NewSqlStore() Store {
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
sqlStore.status = NewSqlStatusStore(sqlStore)
sqlStore.fileInfo = NewSqlFileInfoStore(sqlStore)
+ sqlStore.reaction = NewSqlReactionStore(sqlStore)
err := sqlStore.master.CreateTablesIfNotExists()
if err != nil {
@@ -161,6 +163,7 @@ func NewSqlStore() Store {
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
sqlStore.status.(*SqlStatusStore).CreateIndexesIfNotExists()
sqlStore.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists()
+ sqlStore.reaction.(*SqlReactionStore).CreateIndexesIfNotExists()
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
@@ -676,6 +679,10 @@ func (ss *SqlStore) FileInfo() FileInfoStore {
return ss.fileInfo
}
+func (ss *SqlStore) Reaction() ReactionStore {
+ return ss.reaction
+}
+
func (ss *SqlStore) DropAllTables() {
ss.master.TruncateTables()
}
diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go
index 992fac189..38aac4299 100644
--- a/store/sql_upgrade.go
+++ b/store/sql_upgrade.go
@@ -15,6 +15,7 @@ import (
)
const (
+ VERSION_3_6_0 = "3.6.0"
VERSION_3_5_0 = "3.5.0"
VERSION_3_4_0 = "3.4.0"
VERSION_3_3_0 = "3.3.0"
@@ -37,6 +38,7 @@ func UpgradeDatabase(sqlStore *SqlStore) {
UpgradeDatabaseToVersion33(sqlStore)
UpgradeDatabaseToVersion34(sqlStore)
UpgradeDatabaseToVersion35(sqlStore)
+ UpgradeDatabaseToVersion36(sqlStore)
// If the SchemaVersion is empty this this is the first time it has ran
// so lets set it to the current version.
@@ -210,3 +212,17 @@ func UpgradeDatabaseToVersion35(sqlStore *SqlStore) {
saveSchemaVersion(sqlStore, VERSION_3_5_0)
}
}
+
+func UpgradeDatabaseToVersion36(sqlStore *SqlStore) {
+ //if shouldPerformUpgrade(sqlStore, VERSION_3_5_0, VERSION_3_6_0) {
+
+ sqlStore.CreateColumnIfNotExists("Posts", "HasReactions", "tinyint", "boolean", "0")
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // TODO FIXME UNCOMMENT WHEN WE DO RELEASE
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //sqlStore.Session().RemoveAllSessions()
+
+ //saveSchemaVersion(sqlStore, VERSION_3_6_0)
+ //}
+}
diff --git a/store/store.go b/store/store.go
index ae938a797..7602be8f4 100644
--- a/store/store.go
+++ b/store/store.go
@@ -46,6 +46,7 @@ type Store interface {
Emoji() EmojiStore
Status() StatusStore
FileInfo() FileInfoStore
+ Reaction() ReactionStore
MarkSystemRanUnitTests()
Close()
DropAllTables()
@@ -310,3 +311,10 @@ type FileInfoStore interface {
AttachToPost(fileId string, postId string) StoreChannel
DeleteForPost(postId string) StoreChannel
}
+
+type ReactionStore interface {
+ Save(reaction *model.Reaction) StoreChannel
+ Delete(reaction *model.Reaction) StoreChannel
+ GetForPost(postId string) StoreChannel
+ DeleteAllWithEmojiName(emojiName string) StoreChannel
+}
diff --git a/tests/test-emoticons1.md b/tests/test-emoticons1.md
index 25328a4ab..675e16df8 100644
--- a/tests/test-emoticons1.md
+++ b/tests/test-emoticons1.md
@@ -1,7 +1,7 @@
# Emoticon Testing
Verify that all emoticons render.
-:mm: :mattermost:
+:mattermost:
### Emoticon - Punctuation
diff --git a/tests/test-emoticons3.md b/tests/test-emoticons3.md
index 17fd3f7a2..2b7e65b0c 100644
--- a/tests/test-emoticons3.md
+++ b/tests/test-emoticons3.md
@@ -1,3 +1,3 @@
### Emoticons - Places
-:house: :house_with_garden: :school: :office: :post_office: :hospital: :bank: :convenience_store: :love_hotel: :hotel: :wedding: :church: :department_store: :european_post_office: :city_sunrise: :city_sunset: :japanese_castle: :european_castle: :tent: :factory: :tokyo_tower: :japan: :mount_fuji: :sunrise_over_mountains: :sunrise: :stars: :statue_of_liberty: :bridge_at_night: :carousel_horse: :rainbow: :ferris_wheel: :fountain: :roller_coaster: :ship: :speedboat: :boat: :sailboat: :rowboat: :anchor: :rocket: :airplane: :helicopter: :steam_locomotive: :tram: :mountain_railway: :bike: :aerial_tramway: :suspension_railway: :mountain_cableway: :tractor: :blue_car: :oncoming_automobile: :car: :red_car: :taxi: :oncoming_taxi: :articulated_lorry: :bus: :oncoming_bus: :rotating_light: :police_car: :oncoming_police_car: :fire_engine: :ambulance: :minibus: :truck: :train: :station: :train2: :bullettrain_front: :bullettrain_side: :light_rail: :monorail: :railway_car: :trolleybus: :ticket: :fuelpump: :vertical_traffic_light: :traffic_light: :warning: :construction: :beginner: :atm: :slot_machine: :busstop: :barber: :hotsprings: :checkered_flag: :crossed_flags: :izakaya_lantern: :moyai: :circus_tent: :performing_arts: :round_pushpin: :triangular_flag_on_post: :jp: :kr: :cn: :us: :fr: :es: :it: :ru: :gb: :uk: :de: :ca: :eh: :pk: :za:
+:house: :house_with_garden: :school: :office: :post_office: :hospital: :bank: :convenience_store: :love_hotel: :hotel: :wedding: :church: :department_store: :european_post_office: :city_sunrise: :city_sunset: :japanese_castle: :european_castle: :tent: :factory: :tokyo_tower: :japan: :mount_fuji: :sunrise_over_mountains: :sunrise: :stars: :statue_of_liberty: :bridge_at_night: :carousel_horse: :rainbow: :ferris_wheel: :fountain: :roller_coaster: :ship: :speedboat: :boat: :sailboat: :rowboat: :anchor: :rocket: :airplane: :helicopter: :steam_locomotive: :tram: :mountain_railway: :bike: :aerial_tramway: :suspension_railway: :mountain_cableway: :tractor: :blue_car: :oncoming_automobile: :car: :red_car: :taxi: :oncoming_taxi: :articulated_lorry: :bus: :oncoming_bus: :rotating_light: :police_car: :oncoming_police_car: :fire_engine: :ambulance: :minibus: :truck: :train: :station: :train2: :bullettrain_front: :bullettrain_side: :light_rail: :monorail: :railway_car: :trolleybus: :ticket: :fuelpump: :vertical_traffic_light: :traffic_light: :warning: :construction: :beginner: :atm: :slot_machine: :busstop: :barber: :hotsprings: :checkered_flag: :crossed_flags: :izakaya_lantern: :moyai: :circus_tent: :performing_arts: :round_pushpin: :triangular_flag_on_post: :jp: :kr: :cn: :us: :fr: :es: :it: :ru: :gb: :uk: :de: :ca: :pk: :za:
diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx
index 9599a9a77..4f861c909 100644
--- a/webapp/actions/post_actions.jsx
+++ b/webapp/actions/post_actions.jsx
@@ -252,3 +252,23 @@ export function loadProfilesForPosts(posts) {
AsyncClient.getProfilesByIds(list);
}
+
+export function addReaction(channelId, postId, emojiName) {
+ const reaction = {
+ post_id: postId,
+ user_id: UserStore.getCurrentId(),
+ emoji_name: emojiName
+ };
+
+ AsyncClient.saveReaction(channelId, reaction);
+}
+
+export function removeReaction(channelId, postId, emojiName) {
+ const reaction = {
+ post_id: postId,
+ user_id: UserStore.getCurrentId(),
+ emoji_name: emojiName
+ };
+
+ AsyncClient.deleteReaction(channelId, reaction);
+}
diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx
index d6adb5105..ec433aab5 100644
--- a/webapp/actions/websocket_actions.jsx
+++ b/webapp/actions/websocket_actions.jsx
@@ -11,6 +11,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-line no-unused-vars
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import Client from 'client/web_client.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import * as WebrtcActions from './webrtc_actions.jsx';
@@ -23,7 +24,7 @@ import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
import * as StatusActions from 'actions/status_actions.jsx';
-import {Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx';
+import {ActionTypes, Constants, SocketEvents, UserStatuses} from 'utils/constants.jsx';
import {browserHistory} from 'react-router/es6';
@@ -165,6 +166,14 @@ function handleEvent(msg) {
handleWebrtc(msg);
break;
+ case SocketEvents.REACTION_ADDED:
+ handleReactionAddedEvent(msg);
+ break;
+
+ case SocketEvents.REACTION_REMOVED:
+ handleReactionRemovedEvent(msg);
+ break;
+
default:
}
}
@@ -320,3 +329,23 @@ function handleWebrtc(msg) {
const data = msg.data;
return WebrtcActions.handle(data);
}
+
+function handleReactionAddedEvent(msg) {
+ const reaction = JSON.parse(msg.data.reaction);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.ADDED_REACTION,
+ postId: reaction.post_id,
+ reaction
+ });
+}
+
+function handleReactionRemovedEvent(msg) {
+ const reaction = JSON.parse(msg.data.reaction);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.REMOVED_REACTION,
+ postId: reaction.post_id,
+ reaction
+ });
+}
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index 75e47267c..dbc41f228 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -2005,11 +2005,11 @@ export default class Client {
removeCertificateFile(filename, success, error) {
request.
- post(`${this.getAdminRoute()}/remove_certificate`).
- set(this.defaultHeaders).
- accept('application/json').
- send({filename}).
- end(this.handleResponse.bind(this, 'removeCertificateFile', success, error));
+ post(`${this.getAdminRoute()}/remove_certificate`).
+ set(this.defaultHeaders).
+ accept('application/json').
+ send({filename}).
+ end(this.handleResponse.bind(this, 'removeCertificateFile', success, error));
}
samlCertificateStatus(success, error) {
@@ -2030,6 +2030,33 @@ export default class Client {
});
}
+ saveReaction(channelId, reaction, success, error) {
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/save`).
+ set(this.defaultHeaders).
+ accept('application/json').
+ send(reaction).
+ end(this.handleResponse.bind(this, 'saveReaction', success, error));
+ }
+
+ deleteReaction(channelId, reaction, success, error) {
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/posts/${reaction.post_id}/reactions/delete`).
+ set(this.defaultHeaders).
+ accept('application/json').
+ send(reaction).
+ end(this.handleResponse.bind(this, 'deleteReaction', success, error));
+ }
+
+ listReactions(channelId, postId, success, error) {
+ request.
+ get(`${this.getChannelNeededRoute(channelId)}/posts/${postId}/reactions`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'listReactions', success, error));
+ }
+
webrtcToken(success, error) {
request.post(`${this.getWebrtcRoute()}/token`).
set(this.defaultHeaders).
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 2aa85265a..a0a4ddcd2 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -5,6 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Client from 'client/web_client.jsx';
+import EmojiStore from 'stores/emoji_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
import PostStore from 'stores/post_store.jsx';
@@ -17,6 +18,7 @@ import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
+import * as PostActions from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -25,6 +27,8 @@ import {FormattedMessage} from 'react-intl';
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
+import {REACTION_PATTERN} from './create_post.jsx';
+
import React from 'react';
export default class CreateComment extends React.Component {
@@ -34,6 +38,8 @@ export default class CreateComment extends React.Component {
this.lastTime = 0;
this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleSubmitPost = this.handleSubmitPost.bind(this);
+ this.handleSubmitReaction = this.handleSubmitReaction.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -100,15 +106,9 @@ export default class CreateComment extends React.Component {
return;
}
- const post = {};
- post.file_ids = [];
- post.message = this.state.message;
-
- if (post.message.trim().length === 0 && this.state.fileInfos.length === 0) {
- return;
- }
+ const message = this.state.message;
- if (post.message.length > Constants.CHARACTER_LIMIT) {
+ if (message.length > Constants.CHARACTER_LIMIT) {
this.setState({
postError: (
<FormattedMessage
@@ -121,15 +121,43 @@ export default class CreateComment extends React.Component {
return;
}
- MessageHistoryStore.storeMessageInHistory(this.state.message);
+ MessageHistoryStore.storeMessageInHistory(message);
+
+ if (message.trim().length === 0 && this.state.previews.length === 0) {
+ return;
+ }
+
+ const isReaction = REACTION_PATTERN.exec(message);
+ if (isReaction && EmojiStore.has(isReaction[2])) {
+ this.handleSubmitReaction(isReaction);
+ } else {
+ this.handleSubmitPost(message);
+ }
+
+ this.setState({
+ message: '',
+ submitting: false,
+ postError: null,
+ fileInfos: [],
+ serverError: null
+ });
+ const fasterThanHumanWillClick = 150;
+ const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
+ this.focusTextbox(forceFocus);
+ }
+
+ handleSubmitPost(message) {
const userId = UserStore.getCurrentId();
+ const time = Utils.getTimestamp();
+ const post = {};
+ post.file_ids = [];
+ post.message = message;
post.channel_id = this.props.channelId;
post.root_id = this.props.rootId;
post.parent_id = this.props.rootId;
post.file_ids = this.state.fileInfos.map((info) => info.id);
- const time = Utils.getTimestamp();
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
@@ -160,18 +188,21 @@ export default class CreateComment extends React.Component {
});
}
);
+ }
- this.setState({
- message: '',
- submitting: false,
- postError: null,
- fileInfos: [],
- serverError: null
- });
+ handleSubmitReaction(isReaction) {
+ const action = isReaction[1];
- const fasterThanHumanWillClick = 150;
- const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
- this.focusTextbox(forceFocus);
+ const emojiName = isReaction[2];
+ const postId = this.props.latestPostId;
+
+ if (action === '+') {
+ PostActions.addReaction(this.props.channelId, postId, emojiName);
+ } else if (action === '-') {
+ PostActions.removeReaction(this.props.channelId, postId, emojiName);
+ }
+
+ PostStore.storeCommentDraft(this.props.rootId, null);
}
commentMsgKeyPress(e) {
@@ -455,5 +486,6 @@ export default class CreateComment extends React.Component {
CreateComment.propTypes = {
channelId: React.PropTypes.string.isRequired,
- rootId: React.PropTypes.string.isRequired
+ rootId: React.PropTypes.string.isRequired,
+ latestPostId: React.PropTypes.string.isRequired
};
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index db61aca41..2ba79af36 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -9,14 +9,16 @@ import FilePreview from './file_preview.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
+import * as PostActions from 'actions/post_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import EmojiStore from 'stores/emoji_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -34,6 +36,8 @@ const KeyCodes = Constants.KeyCodes;
import React from 'react';
+export const REACTION_PATTERN = /^(\+|-):([^:\s]+):\s*$/;
+
export default class CreatePost extends React.Component {
constructor(props) {
super(props);
@@ -101,6 +105,7 @@ export default class CreatePost extends React.Component {
this.setState({submitting: true, serverError: null});
+ const isReaction = REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) {
PostStore.storeDraft(this.state.channelId, null);
this.setState({message: '', postError: null, fileInfos: []});
@@ -123,14 +128,18 @@ export default class CreatePost extends React.Component {
const state = {};
state.serverError = err.message;
state.submitting = false;
- this.setState(state);
+ this.setState({state});
}
}
);
+ } else if (isReaction && EmojiStore.has(isReaction[2])) {
+ this.sendReaction(isReaction);
} else {
this.sendMessage(post);
}
+ this.setState({message: '', submitting: false, postError: null, fileInfos: [], serverError: null});
+
const fasterThanHumanWillClick = 150;
const forceFocus = (Date.now() - this.state.lastBlurAt < fasterThanHumanWillClick);
this.focusTextbox(forceFocus);
@@ -148,7 +157,6 @@ export default class CreatePost extends React.Component {
post.parent_id = this.state.parentId;
GlobalActions.emitUserPostedEvent(post);
- this.setState({message: '', submitting: false, postError: null, fileInfos: [], serverError: null});
Client.createPost(post,
(data) => {
@@ -177,6 +185,21 @@ export default class CreatePost extends React.Component {
);
}
+ sendReaction(isReaction) {
+ const action = isReaction[1];
+
+ const emojiName = isReaction[2];
+ const postId = PostStore.getLatestPost(this.state.channelId).id;
+
+ if (action === '+') {
+ PostActions.addReaction(this.state.channelId, postId, emojiName);
+ } else if (action === '-') {
+ PostActions.removeReaction(this.state.channelId, postId, emojiName);
+ }
+
+ PostStore.storeCurrentDraft(null);
+ }
+
focusTextbox(keepFocus = false) {
if (keepFocus || !Utils.isMobile()) {
this.refs.textbox.focus();
diff --git a/webapp/components/emoji/components/add_emoji.jsx b/webapp/components/emoji/components/add_emoji.jsx
index 9e4babc19..d859da0df 100644
--- a/webapp/components/emoji/components/add_emoji.jsx
+++ b/webapp/components/emoji/components/add_emoji.jsx
@@ -85,7 +85,7 @@ export default class AddEmoji extends React.Component {
});
return;
- } else if (EmojiStore.getSystemEmojis().has(emoji.name)) {
+ } else if (EmojiStore.hasSystemEmoji(emoji.name)) {
this.setState({
saving: false,
error: (
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index 823cb8ce7..58ea947b2 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -255,6 +255,7 @@ export default class Post extends React.Component {
/>
<PostBody
post={post}
+ currentUser={this.props.currentUser}
sameRoot={this.props.sameRoot}
parentPost={parentPost}
handleCommentClick={this.handleCommentClick}
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx
index cfcbe8930..60e682e8d 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/components/post_body.jsx
@@ -10,6 +10,7 @@ import FileAttachmentListContainer from 'components/file_attachment_list_contain
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
import PostMessageContainer from './post_message_container.jsx';
import PendingPostOptions from './pending_post_options.jsx';
+import ReactionListContainer from './reaction_list_container.jsx';
import {FormattedMessage} from 'react-intl';
@@ -202,6 +203,10 @@ export default class PostBody extends React.Component {
<div className={'post__body ' + mentionHighlightClass}>
{messageWithAdditionalContent}
{fileAttachmentHolder}
+ <ReactionListContainer
+ post={post}
+ currentUserId={this.props.currentUser.id}
+ />
</div>
</div>
);
@@ -210,6 +215,7 @@ export default class PostBody extends React.Component {
PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func,
handleCommentClick: React.PropTypes.func.isRequired,
diff --git a/webapp/components/post_view/components/reaction.jsx b/webapp/components/post_view/components/reaction.jsx
new file mode 100644
index 000000000..5bb62d859
--- /dev/null
+++ b/webapp/components/post_view/components/reaction.jsx
@@ -0,0 +1,136 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import EmojiStore from 'stores/emoji_store.jsx';
+import * as PostActions from 'actions/post_actions.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+
+export default class Reaction extends React.Component {
+ static propTypes = {
+ post: React.PropTypes.object.isRequired,
+ currentUserId: React.PropTypes.string.isRequired,
+ emojiName: React.PropTypes.string.isRequired,
+ reactions: React.PropTypes.arrayOf(React.PropTypes.object)
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.addReaction = this.addReaction.bind(this);
+ this.removeReaction = this.removeReaction.bind(this);
+ }
+
+ addReaction(e) {
+ e.preventDefault();
+ PostActions.addReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
+ }
+
+ removeReaction(e) {
+ e.preventDefault();
+ PostActions.removeReaction(this.props.post.channel_id, this.props.post.id, this.props.emojiName);
+ }
+
+ render() {
+ if (!EmojiStore.has(this.props.emojiName)) {
+ return null;
+ }
+
+ let currentUserReacted = false;
+ const users = [];
+ for (const reaction of this.props.reactions) {
+ if (reaction.user_id === this.props.currentUserId) {
+ currentUserReacted = true;
+ } else {
+ users.push(Utils.displayUsername(reaction.user_id));
+ }
+ }
+
+ // sort users in alphabetical order with "you" being first if the current user reacted
+ users.sort();
+ if (currentUserReacted) {
+ users.unshift(Utils.localizeMessage('reaction.you', 'You'));
+ }
+
+ let tooltip;
+ if (users.length > 1) {
+ tooltip = (
+ <FormattedHTMLMessage
+ id='reaction.multipleReacted'
+ defaultMessage='<b>{users} and {lastUser}</b> reacted with <b>:{emojiName}:</b>'
+ values={{
+ users: users.slice(0, -1).join(', '),
+ lastUser: users[users.length - 1],
+ emojiName: this.props.emojiName
+ }}
+ />
+ );
+ } else {
+ tooltip = (
+ <FormattedHTMLMessage
+ id='reaction.oneReacted'
+ defaultMessage='<b>{user}</b> reacted with <b>:{emojiName}:</b>'
+ values={{
+ user: users[0],
+ emojiName: this.props.emojiName
+ }}
+ />
+ );
+ }
+
+ let handleClick;
+ let clickTooltip;
+ let className = 'post-reaction';
+ if (currentUserReacted) {
+ handleClick = this.removeReaction;
+ clickTooltip = (
+ <FormattedMessage
+ id='reaction.clickToRemove'
+ defaultMessage='(click to remove)'
+ />
+ );
+
+ className += ' post-reaction--current-user';
+ } else {
+ handleClick = this.addReaction;
+ clickTooltip = (
+ <FormattedMessage
+ id='reaction.clickToAdd'
+ defaultMessage='(click to add)'
+ />
+ );
+ }
+
+ return (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ shouldUpdatePosition={true}
+ overlay={
+ <Tooltip>
+ {tooltip}
+ <br/>
+ {clickTooltip}
+ </Tooltip>
+ }
+ >
+ <div
+ className={className}
+ onClick={handleClick}
+ >
+ <img
+ className='post-reaction__emoji'
+ src={EmojiStore.getEmojiImageUrl(EmojiStore.get(this.props.emojiName))}
+ />
+ <span className='post-reaction__count'>
+ {this.props.reactions.length}
+ </span>
+ </div>
+ </OverlayTrigger>
+ );
+ }
+}
diff --git a/webapp/components/post_view/components/reaction_list_container.jsx b/webapp/components/post_view/components/reaction_list_container.jsx
new file mode 100644
index 000000000..0ac4fa35a
--- /dev/null
+++ b/webapp/components/post_view/components/reaction_list_container.jsx
@@ -0,0 +1,82 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import ReactionStore from 'stores/reaction_store.jsx';
+
+import ReactionListView from './reaction_list_view.jsx';
+
+export default class ReactionListContainer extends React.Component {
+ static propTypes = {
+ post: React.PropTypes.object.isRequired,
+ currentUserId: React.PropTypes.string.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleReactionsChanged = this.handleReactionsChanged.bind(this);
+
+ this.state = {
+ reactions: ReactionStore.getReactions(this.props.post.id)
+ };
+ }
+
+ componentDidMount() {
+ ReactionStore.addChangeListener(this.props.post.id, this.handleReactionsChanged);
+
+ if (this.props.post.has_reactions) {
+ AsyncClient.listReactions(this.props.post.channel_id, this.props.post.id);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.post.id !== this.props.post.id) {
+ ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
+ ReactionStore.addChangeListener(nextProps.post.id, this.handleReactionsChanged);
+
+ this.setState({
+ reactions: ReactionStore.getReactions(nextProps.post.id)
+ });
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (nextProps.post.has_reactions !== this.props.post.has_reactions) {
+ return true;
+ }
+
+ if (nextState.reactions !== this.state.reactions) {
+ // this will only work so long as the entries in the ReactionStore are never mutated
+ return true;
+ }
+
+ return false;
+ }
+
+ componentWillUnmount() {
+ ReactionStore.removeChangeListener(this.props.post.id, this.handleReactionsChanged);
+ }
+
+ handleReactionsChanged() {
+ this.setState({
+ reactions: ReactionStore.getReactions(this.props.post.id)
+ });
+ }
+
+ render() {
+ if (!this.props.post.has_reactions) {
+ return null;
+ }
+
+ return (
+ <ReactionListView
+ post={this.props.post}
+ currentUserId={this.props.currentUserId}
+ reactions={this.state.reactions}
+ />
+ );
+ }
+}
diff --git a/webapp/components/post_view/components/reaction_list_view.jsx b/webapp/components/post_view/components/reaction_list_view.jsx
new file mode 100644
index 000000000..345b7a24c
--- /dev/null
+++ b/webapp/components/post_view/components/reaction_list_view.jsx
@@ -0,0 +1,48 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Reaction from './reaction.jsx';
+
+export default class ReactionListView extends React.Component {
+ static propTypes = {
+ post: React.PropTypes.object.isRequired,
+ currentUserId: React.PropTypes.string.isRequired,
+ reactions: React.PropTypes.arrayOf(React.PropTypes.object)
+ }
+
+ render() {
+ const reactionsByName = new Map();
+ const emojiNames = [];
+
+ for (const reaction of this.props.reactions) {
+ const emojiName = reaction.emoji_name;
+
+ if (reactionsByName.has(emojiName)) {
+ reactionsByName.get(emojiName).push(reaction);
+ } else {
+ emojiNames.push(emojiName);
+ reactionsByName.set(emojiName, [reaction]);
+ }
+ }
+
+ const children = emojiNames.map((emojiName) => {
+ return (
+ <Reaction
+ key={emojiName}
+ post={this.props.post}
+ currentUserId={this.props.currentUserId}
+ emojiName={emojiName}
+ reactions={reactionsByName.get(emojiName)}
+ />
+ );
+ });
+
+ return (
+ <div className='post-reaction-list'>
+ {children}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 416c0fe4b..f4cc0d8e5 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -6,6 +6,7 @@ import FileAttachmentListContainer from './file_attachment_list_container.jsx';
import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import ProfilePicture from 'components/profile_picture.jsx';
+import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx';
import TeamStore from 'stores/team_store.jsx';
@@ -404,6 +405,10 @@ export default class RhsComment extends React.Component {
{message}
</div>
{fileAttachment}
+ <ReactionListContainer
+ post={post}
+ currentUserId={this.props.currentUser.id}
+ />
</div>
</div>
</div>
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 22795967a..4681f3fd3 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -6,6 +6,7 @@ import PostBodyAdditionalContent from 'components/post_view/components/post_body
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import FileAttachmentListContainer from './file_attachment_list_container.jsx';
import ProfilePicture from 'components/profile_picture.jsx';
+import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx';
import ChannelStore from 'stores/channel_store.jsx';
@@ -389,6 +390,10 @@ export default class RhsRootPost extends React.Component {
message={messageWrapper}
/>
{fileAttachment}
+ <ReactionListContainer
+ post={post}
+ currentUserId={this.props.currentUser.id}
+ />
</div>
</div>
</div>
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index 11c79d722..a3266e9ba 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -339,6 +339,7 @@ export default class RhsThread extends React.Component {
<CreateComment
channelId={selected.channel_id}
rootId={selected.id}
+ latestPostId={postsArray.length > 0 ? postsArray[postsArray.length - 1].id : selected.id}
/>
</div>
</div>
diff --git a/webapp/components/suggestion/emoticon_provider.jsx b/webapp/components/suggestion/emoticon_provider.jsx
index c2b6b9a50..d04750159 100644
--- a/webapp/components/suggestion/emoticon_provider.jsx
+++ b/webapp/components/suggestion/emoticon_provider.jsx
@@ -46,20 +46,23 @@ export default class EmoticonProvider {
handlePretextChanged(suggestionId, pretext) {
let hasSuggestions = false;
- // look for partial matches among the named emojis
- const captured = (/(?:^|\s)(:([^:\s]*))$/g).exec(pretext);
+ // look for the potential emoticons at the start of the text, after whitespace, and at the start of emoji reaction commands
+ const captured = (/(^|\s|^\+|^-)(:([^:\s]*))$/g).exec(pretext);
if (captured) {
- const text = captured[1];
- const partialName = captured[2];
+ const prefix = captured[1];
+ const text = captured[2];
+ const partialName = captured[3];
const matched = [];
- // check for text emoticons
- for (const emoticon of Object.keys(Emoticons.emoticonPatterns)) {
- if (Emoticons.emoticonPatterns[emoticon].test(text)) {
- SuggestionStore.addSuggestion(suggestionId, text, EmojiStore.get(emoticon), EmoticonSuggestion, text);
+ // check for text emoticons if this isn't for an emoji reaction
+ if (prefix !== '-' && prefix !== '+') {
+ for (const emoticon of Object.keys(Emoticons.emoticonPatterns)) {
+ if (Emoticons.emoticonPatterns[emoticon].test(text)) {
+ SuggestionStore.addSuggestion(suggestionId, text, EmojiStore.get(emoticon), EmoticonSuggestion, text);
- hasSuggestions = true;
+ hasSuggestions = true;
+ }
}
}
@@ -76,11 +79,14 @@ export default class EmoticonProvider {
// sort the emoticons so that emoticons starting with the entered text come first
matched.sort((a, b) => {
- const aPrefix = a.name.startsWith(partialName);
- const bPrefix = b.name.startsWith(partialName);
+ const aName = a.name || a.aliases[0];
+ const bName = b.name || b.aliases[0];
+
+ const aPrefix = aName.startsWith(partialName);
+ const bPrefix = bName.startsWith(partialName);
if (aPrefix === bPrefix) {
- return a.name.localeCompare(b.name);
+ return aName.localeCompare(bName);
} else if (aPrefix) {
return -1;
}
@@ -88,7 +94,7 @@ export default class EmoticonProvider {
return 1;
});
- const terms = matched.map((emoticon) => ':' + emoticon.name + ':');
+ const terms = matched.map((emoticon) => ':' + (emoticon.name || emoticon.aliases[0]) + ':');
SuggestionStore.clearSuggestions(suggestionId);
if (terms.length > 0) {
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 9192cd4f9..6ba925ed7 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -26,7 +26,6 @@ export default class Textbox extends React.Component {
this.focus = this.focus.bind(this);
this.recalculateSize = this.recalculateSize.bind(this);
- this.getStateFromStores = this.getStateFromStores.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
@@ -48,16 +47,6 @@ export default class Textbox extends React.Component {
}
}
- getStateFromStores() {
- const error = ErrorStore.getLastError();
-
- if (error) {
- return {message: error.message};
- }
-
- return {message: null};
- }
-
componentDidMount() {
ErrorStore.addChangeListener(this.onRecievedError);
}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 192c92f10..4283816f0 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1620,6 +1620,11 @@
"post_info.reply": "Reply",
"posts_view.loadMore": "Load more messages",
"posts_view.newMsg": "New Messages",
+ "reaction.clickToAdd": "(click to add)",
+ "reaction.clickToRemove": "(click to remove)",
+ "reaction.multipleReacted": "<b>{users} and {lastUser}</b> reacted with <b>:{emojiName}:</b>",
+ "reaction.oneReacted": "<b>{user}</b> reacted with <b>:{emojiName}:</b>",
+ "reaction.you": "You",
"removed_channel.channelName": "the channel",
"removed_channel.from": "Removed from ",
"removed_channel.okay": "Okay",
diff --git a/webapp/images/emoji/basecamp.png b/webapp/images/emoji/basecamp.png
deleted file mode 100644
index d0267fb85..000000000
--- a/webapp/images/emoji/basecamp.png
+++ /dev/null
Binary files differ
diff --git a/webapp/images/emoji/basecampy.png b/webapp/images/emoji/basecampy.png
deleted file mode 100644
index 806d013fc..000000000
--- a/webapp/images/emoji/basecampy.png
+++ /dev/null
Binary files differ
diff --git a/webapp/images/emoji/mm.png b/webapp/images/emoji/mattermost.png
index 90930aabe..90930aabe 100644
--- a/webapp/images/emoji/mm.png
+++ b/webapp/images/emoji/mattermost.png
Binary files differ
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index 4a995a4d6..9a042f325 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -1206,3 +1206,31 @@
margin-left: 50px !important;
min-width: 320px;
}
+
+.post-reaction-list {
+ height: 24px;
+}
+
+.post-reaction {
+ border: 1px solid $primary-color;
+ border-radius: 3px;
+ cursor: pointer;
+ display: inline-block;
+ padding: 1px 2px;
+ @include user-select(none);
+
+ .post-reaction__emoji {
+ height: 14px;
+ margin-top: 3px;
+ width: 14px;
+ vertical-align: top;
+ }
+
+ & + & {
+ margin-left: 5px;
+ }
+
+ &--current-user {
+ // background-colour set by theme code
+ }
+}
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
diff --git a/webapp/tests/client_reaction.test.jsx b/webapp/tests/client_reaction.test.jsx
new file mode 100644
index 000000000..fba3fd43b
--- /dev/null
+++ b/webapp/tests/client_reaction.test.jsx
@@ -0,0 +1,81 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import TestHelper from './test_helper.jsx';
+
+describe('Client.Reaction', function() {
+ this.timeout(100000);
+
+ it('saveListReaction', function(done) {
+ TestHelper.initBasic(() => {
+ const channelId = TestHelper.basicChannel().id;
+ const postId = TestHelper.basicPost().id;
+
+ const reaction = {
+ post_id: postId,
+ user_id: TestHelper.basicUser().id,
+ emoji_name: 'upside_down_face'
+ };
+
+ TestHelper.basicClient().saveReaction(
+ channelId,
+ reaction,
+ function() {
+ TestHelper.basicClient().listReactions(
+ channelId,
+ postId,
+ function(reactions) {
+ if (reactions.length === 1 &&
+ reactions[0].post_id === reaction.post_id &&
+ reactions[0].user_id === reaction.user_id &&
+ reactions[0].emoji_name === reaction.emoji_name) {
+ done();
+ } else {
+ done(new Error('test reaction wasn\'t returned'));
+ }
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
+ it('deleteReaction', function(done) {
+ TestHelper.initBasic(() => {
+ const channelId = TestHelper.basicChannel().id;
+ const postId = TestHelper.basicPost().id;
+
+ const reaction = {
+ post_id: postId,
+ user_id: TestHelper.basicUser().id,
+ emoji_name: 'upside_down_face'
+ };
+
+ TestHelper.basicClient().saveReaction(
+ channelId,
+ reaction,
+ function() {
+ TestHelper.basicClient().deleteReaction(
+ channelId,
+ reaction,
+ function() {
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+});
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index d41b2ddf7..3bbb01d2f 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1527,3 +1527,53 @@ export function deleteEmoji(id) {
}
);
}
+
+export function saveReaction(channelId, reaction) {
+ Client.saveReaction(
+ channelId,
+ reaction,
+ null, // the added reaction will be sent over the websocket
+ (err) => {
+ dispatchError(err, 'saveReaction');
+ }
+ );
+}
+
+export function deleteReaction(channelId, reaction) {
+ Client.deleteReaction(
+ channelId,
+ reaction,
+ null, // the removed reaction will be sent over the websocket
+ (err) => {
+ dispatchError(err, 'deleteReaction');
+ }
+ );
+}
+
+export function listReactions(channelId, postId) {
+ const callName = 'deleteEmoji' + postId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ Client.listReactions(
+ channelId,
+ postId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_REACTIONS,
+ postId,
+ reactions: data
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+ dispatchError(err, 'listReactions');
+ }
+ );
+} \ No newline at end of file
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 21ec07db3..f94461ec7 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -122,6 +122,10 @@ export const ActionTypes = keyMirror({
UPDATED_CUSTOM_EMOJI: null,
REMOVED_CUSTOM_EMOJI: null,
+ RECEIVED_REACTIONS: null,
+ ADDED_REACTION: null,
+ REMOVED_REACTION: null,
+
RECEIVED_MSG: null,
RECEIVED_MY_TEAM: null,
@@ -206,7 +210,9 @@ export const SocketEvents = {
EPHEMERAL_MESSAGE: 'ephemeral_message',
STATUS_CHANGED: 'status_change',
HELLO: 'hello',
- WEBRTC: 'webrtc'
+ WEBRTC: 'webrtc',
+ REACTION_ADDED: 'reaction_added',
+ REACTION_REMOVED: 'reaction_removed'
};
export const TutorialSteps = {
diff --git a/webapp/utils/emoji.json b/webapp/utils/emoji.json
deleted file mode 100644
index 03b56a18a..000000000
--- a/webapp/utils/emoji.json
+++ /dev/null
@@ -1 +0,0 @@
-[["smile",{"name":"smile","unicode":"1f604"}],["smiley",{"name":"smiley","unicode":"1f603"}],["grinning",{"name":"grinning","unicode":"1f600"}],["blush",{"name":"blush","unicode":"1f60a"}],["relaxed",{"name":"relaxed","unicode":"263a"}],["wink",{"name":"wink","unicode":"1f609"}],["heart_eyes",{"name":"heart_eyes","unicode":"1f60d"}],["kissing_heart",{"name":"kissing_heart","unicode":"1f618"}],["kissing_closed_eyes",{"name":"kissing_closed_eyes","unicode":"1f61a"}],["kissing",{"name":"kissing","unicode":"1f617"}],["kissing_smiling_eyes",{"name":"kissing_smiling_eyes","unicode":"1f619"}],["stuck_out_tongue_winking_eye",{"name":"stuck_out_tongue_winking_eye","unicode":"1f61c"}],["stuck_out_tongue_closed_eyes",{"name":"stuck_out_tongue_closed_eyes","unicode":"1f61d"}],["stuck_out_tongue",{"name":"stuck_out_tongue","unicode":"1f61b"}],["flushed",{"name":"flushed","unicode":"1f633"}],["grin",{"name":"grin","unicode":"1f601"}],["pensive",{"name":"pensive","unicode":"1f614"}],["relieved",{"name":"relieved","unicode":"1f60c"}],["unamused",{"name":"unamused","unicode":"1f612"}],["disappointed",{"name":"disappointed","unicode":"1f61e"}],["persevere",{"name":"persevere","unicode":"1f623"}],["cry",{"name":"cry","unicode":"1f622"}],["joy",{"name":"joy","unicode":"1f602"}],["sob",{"name":"sob","unicode":"1f62d"}],["sleepy",{"name":"sleepy","unicode":"1f62a"}],["disappointed_relieved",{"name":"disappointed_relieved","unicode":"1f625"}],["cold_sweat",{"name":"cold_sweat","unicode":"1f630"}],["sweat_smile",{"name":"sweat_smile","unicode":"1f605"}],["sweat",{"name":"sweat","unicode":"1f613"}],["weary",{"name":"weary","unicode":"1f629"}],["tired_face",{"name":"tired_face","unicode":"1f62b"}],["fearful",{"name":"fearful","unicode":"1f628"}],["scream",{"name":"scream","unicode":"1f631"}],["angry",{"name":"angry","unicode":"1f620"}],["rage",{"name":"rage","unicode":"1f621"}],["pout",{"name":"pout","unicode":"1f621"}],["triumph",{"name":"triumph","unicode":"1f624"}],["confounded",{"name":"confounded","unicode":"1f616"}],["laughing",{"name":"laughing","unicode":"1f606"}],["satisfied",{"name":"satisfied","unicode":"1f606"}],["yum",{"name":"yum","unicode":"1f60b"}],["mask",{"name":"mask","unicode":"1f637"}],["sunglasses",{"name":"sunglasses","unicode":"1f60e"}],["sleeping",{"name":"sleeping","unicode":"1f634"}],["dizzy_face",{"name":"dizzy_face","unicode":"1f635"}],["astonished",{"name":"astonished","unicode":"1f632"}],["worried",{"name":"worried","unicode":"1f61f"}],["frowning",{"name":"frowning","unicode":"1f626"}],["anguished",{"name":"anguished","unicode":"1f627"}],["smiling_imp",{"name":"smiling_imp","unicode":"1f608"}],["imp",{"name":"imp","unicode":"1f47f"}],["open_mouth",{"name":"open_mouth","unicode":"1f62e"}],["grimacing",{"name":"grimacing","unicode":"1f62c"}],["neutral_face",{"name":"neutral_face","unicode":"1f610"}],["confused",{"name":"confused","unicode":"1f615"}],["hushed",{"name":"hushed","unicode":"1f62f"}],["no_mouth",{"name":"no_mouth","unicode":"1f636"}],["innocent",{"name":"innocent","unicode":"1f607"}],["smirk",{"name":"smirk","unicode":"1f60f"}],["expressionless",{"name":"expressionless","unicode":"1f611"}],["man_with_gua_pi_mao",{"name":"man_with_gua_pi_mao","unicode":"1f472"}],["man_with_turban",{"name":"man_with_turban","unicode":"1f473"}],["cop",{"name":"cop","unicode":"1f46e"}],["construction_worker",{"name":"construction_worker","unicode":"1f477"}],["guardsman",{"name":"guardsman","unicode":"1f482"}],["baby",{"name":"baby","unicode":"1f476"}],["boy",{"name":"boy","unicode":"1f466"}],["girl",{"name":"girl","unicode":"1f467"}],["man",{"name":"man","unicode":"1f468"}],["woman",{"name":"woman","unicode":"1f469"}],["older_man",{"name":"older_man","unicode":"1f474"}],["older_woman",{"name":"older_woman","unicode":"1f475"}],["person_with_blond_hair",{"name":"person_with_blond_hair","unicode":"1f471"}],["angel",{"name":"angel","unicode":"1f47c"}],["princess",{"name":"princess","unicode":"1f478"}],["smiley_cat",{"name":"smiley_cat","unicode":"1f63a"}],["smile_cat",{"name":"smile_cat","unicode":"1f638"}],["heart_eyes_cat",{"name":"heart_eyes_cat","unicode":"1f63b"}],["kissing_cat",{"name":"kissing_cat","unicode":"1f63d"}],["smirk_cat",{"name":"smirk_cat","unicode":"1f63c"}],["scream_cat",{"name":"scream_cat","unicode":"1f640"}],["crying_cat_face",{"name":"crying_cat_face","unicode":"1f63f"}],["joy_cat",{"name":"joy_cat","unicode":"1f639"}],["pouting_cat",{"name":"pouting_cat","unicode":"1f63e"}],["japanese_ogre",{"name":"japanese_ogre","unicode":"1f479"}],["japanese_goblin",{"name":"japanese_goblin","unicode":"1f47a"}],["see_no_evil",{"name":"see_no_evil","unicode":"1f648"}],["hear_no_evil",{"name":"hear_no_evil","unicode":"1f649"}],["speak_no_evil",{"name":"speak_no_evil","unicode":"1f64a"}],["skull",{"name":"skull","unicode":"1f480"}],["alien",{"name":"alien","unicode":"1f47d"}],["hankey",{"name":"hankey","unicode":"1f4a9"}],["poop",{"name":"poop","unicode":"1f4a9"}],["shit",{"name":"shit","unicode":"1f4a9"}],["fire",{"name":"fire","unicode":"1f525"}],["sparkles",{"name":"sparkles","unicode":"2728"}],["star2",{"name":"star2","unicode":"1f31f"}],["dizzy",{"name":"dizzy","unicode":"1f4ab"}],["boom",{"name":"boom","unicode":"1f4a5"}],["collision",{"name":"collision","unicode":"1f4a5"}],["anger",{"name":"anger","unicode":"1f4a2"}],["sweat_drops",{"name":"sweat_drops","unicode":"1f4a6"}],["droplet",{"name":"droplet","unicode":"1f4a7"}],["zzz",{"name":"zzz","unicode":"1f4a4"}],["dash",{"name":"dash","unicode":"1f4a8"}],["ear",{"name":"ear","unicode":"1f442"}],["eyes",{"name":"eyes","unicode":"1f440"}],["nose",{"name":"nose","unicode":"1f443"}],["tongue",{"name":"tongue","unicode":"1f445"}],["lips",{"name":"lips","unicode":"1f444"}],["+1",{"name":"+1","unicode":"1f44d"}],["thumbsup",{"name":"thumbsup","unicode":"1f44d"}],["-1",{"name":"-1","unicode":"1f44e"}],["thumbsdown",{"name":"thumbsdown","unicode":"1f44e"}],["ok_hand",{"name":"ok_hand","unicode":"1f44c"}],["facepunch",{"name":"facepunch","unicode":"1f44a"}],["punch",{"name":"punch","unicode":"1f44a"}],["fist",{"name":"fist","unicode":"270a"}],["v",{"name":"v","unicode":"270c"}],["wave",{"name":"wave","unicode":"1f44b"}],["hand",{"name":"hand","unicode":"270b"}],["raised_hand",{"name":"raised_hand","unicode":"270b"}],["open_hands",{"name":"open_hands","unicode":"1f450"}],["point_up_2",{"name":"point_up_2","unicode":"1f446"}],["point_down",{"name":"point_down","unicode":"1f447"}],["point_right",{"name":"point_right","unicode":"1f449"}],["point_left",{"name":"point_left","unicode":"1f448"}],["raised_hands",{"name":"raised_hands","unicode":"1f64c"}],["pray",{"name":"pray","unicode":"1f64f"}],["point_up",{"name":"point_up","unicode":"261d"}],["clap",{"name":"clap","unicode":"1f44f"}],["muscle",{"name":"muscle","unicode":"1f4aa"}],["walking",{"name":"walking","unicode":"1f6b6"}],["runner",{"name":"runner","unicode":"1f3c3"}],["running",{"name":"running","unicode":"1f3c3"}],["dancer",{"name":"dancer","unicode":"1f483"}],["couple",{"name":"couple","unicode":"1f46b"}],["family",{"name":"family","unicode":"1f46a"}],["two_men_holding_hands",{"name":"two_men_holding_hands","unicode":"1f46c"}],["two_women_holding_hands",{"name":"two_women_holding_hands","unicode":"1f46d"}],["couplekiss",{"name":"couplekiss","unicode":"1f48f"}],["couple_with_heart",{"name":"couple_with_heart","unicode":"1f491"}],["dancers",{"name":"dancers","unicode":"1f46f"}],["ok_woman",{"name":"ok_woman","unicode":"1f646"}],["no_good",{"name":"no_good","unicode":"1f645"}],["ng_woman",{"name":"ng_woman","unicode":"1f645"}],["information_desk_person",{"name":"information_desk_person","unicode":"1f481"}],["raising_hand",{"name":"raising_hand","unicode":"1f64b"}],["massage",{"name":"massage","unicode":"1f486"}],["haircut",{"name":"haircut","unicode":"1f487"}],["nail_care",{"name":"nail_care","unicode":"1f485"}],["bride_with_veil",{"name":"bride_with_veil","unicode":"1f470"}],["person_with_pouting_face",{"name":"person_with_pouting_face","unicode":"1f64e"}],["person_frowning",{"name":"person_frowning","unicode":"1f64d"}],["bow",{"name":"bow","unicode":"1f647"}],["tophat",{"name":"tophat","unicode":"1f3a9"}],["crown",{"name":"crown","unicode":"1f451"}],["womans_hat",{"name":"womans_hat","unicode":"1f452"}],["athletic_shoe",{"name":"athletic_shoe","unicode":"1f45f"}],["mans_shoe",{"name":"mans_shoe","unicode":"1f45e"}],["shoe",{"name":"shoe","unicode":"1f45e"}],["sandal",{"name":"sandal","unicode":"1f461"}],["high_heel",{"name":"high_heel","unicode":"1f460"}],["boot",{"name":"boot","unicode":"1f462"}],["shirt",{"name":"shirt","unicode":"1f455"}],["tshirt",{"name":"tshirt","unicode":"1f455"}],["necktie",{"name":"necktie","unicode":"1f454"}],["womans_clothes",{"name":"womans_clothes","unicode":"1f45a"}],["dress",{"name":"dress","unicode":"1f457"}],["running_shirt_with_sash",{"name":"running_shirt_with_sash","unicode":"1f3bd"}],["jeans",{"name":"jeans","unicode":"1f456"}],["kimono",{"name":"kimono","unicode":"1f458"}],["bikini",{"name":"bikini","unicode":"1f459"}],["briefcase",{"name":"briefcase","unicode":"1f4bc"}],["handbag",{"name":"handbag","unicode":"1f45c"}],["pouch",{"name":"pouch","unicode":"1f45d"}],["purse",{"name":"purse","unicode":"1f45b"}],["eyeglasses",{"name":"eyeglasses","unicode":"1f453"}],["ribbon",{"name":"ribbon","unicode":"1f380"}],["closed_umbrella",{"name":"closed_umbrella","unicode":"1f302"}],["lipstick",{"name":"lipstick","unicode":"1f484"}],["yellow_heart",{"name":"yellow_heart","unicode":"1f49b"}],["blue_heart",{"name":"blue_heart","unicode":"1f499"}],["purple_heart",{"name":"purple_heart","unicode":"1f49c"}],["green_heart",{"name":"green_heart","unicode":"1f49a"}],["heart",{"name":"heart","unicode":"2764"}],["broken_heart",{"name":"broken_heart","unicode":"1f494"}],["heartpulse",{"name":"heartpulse","unicode":"1f497"}],["heartbeat",{"name":"heartbeat","unicode":"1f493"}],["two_hearts",{"name":"two_hearts","unicode":"1f495"}],["sparkling_heart",{"name":"sparkling_heart","unicode":"1f496"}],["revolving_hearts",{"name":"revolving_hearts","unicode":"1f49e"}],["cupid",{"name":"cupid","unicode":"1f498"}],["love_letter",{"name":"love_letter","unicode":"1f48c"}],["kiss",{"name":"kiss","unicode":"1f48b"}],["ring",{"name":"ring","unicode":"1f48d"}],["gem",{"name":"gem","unicode":"1f48e"}],["bust_in_silhouette",{"name":"bust_in_silhouette","unicode":"1f464"}],["busts_in_silhouette",{"name":"busts_in_silhouette","unicode":"1f465"}],["speech_balloon",{"name":"speech_balloon","unicode":"1f4ac"}],["footprints",{"name":"footprints","unicode":"1f463"}],["thought_balloon",{"name":"thought_balloon","unicode":"1f4ad"}],["dog",{"name":"dog","unicode":"1f436"}],["wolf",{"name":"wolf","unicode":"1f43a"}],["cat",{"name":"cat","unicode":"1f431"}],["mouse",{"name":"mouse","unicode":"1f42d"}],["hamster",{"name":"hamster","unicode":"1f439"}],["rabbit",{"name":"rabbit","unicode":"1f430"}],["frog",{"name":"frog","unicode":"1f438"}],["tiger",{"name":"tiger","unicode":"1f42f"}],["koala",{"name":"koala","unicode":"1f428"}],["bear",{"name":"bear","unicode":"1f43b"}],["pig",{"name":"pig","unicode":"1f437"}],["pig_nose",{"name":"pig_nose","unicode":"1f43d"}],["cow",{"name":"cow","unicode":"1f42e"}],["boar",{"name":"boar","unicode":"1f417"}],["monkey_face",{"name":"monkey_face","unicode":"1f435"}],["monkey",{"name":"monkey","unicode":"1f412"}],["horse",{"name":"horse","unicode":"1f434"}],["sheep",{"name":"sheep","unicode":"1f411"}],["elephant",{"name":"elephant","unicode":"1f418"}],["panda_face",{"name":"panda_face","unicode":"1f43c"}],["penguin",{"name":"penguin","unicode":"1f427"}],["bird",{"name":"bird","unicode":"1f426"}],["baby_chick",{"name":"baby_chick","unicode":"1f424"}],["hatched_chick",{"name":"hatched_chick","unicode":"1f425"}],["hatching_chick",{"name":"hatching_chick","unicode":"1f423"}],["chicken",{"name":"chicken","unicode":"1f414"}],["snake",{"name":"snake","unicode":"1f40d"}],["turtle",{"name":"turtle","unicode":"1f422"}],["bug",{"name":"bug","unicode":"1f41b"}],["bee",{"name":"bee","unicode":"1f41d"}],["honeybee",{"name":"honeybee","unicode":"1f41d"}],["ant",{"name":"ant","unicode":"1f41c"}],["beetle",{"name":"beetle","unicode":"1f41e"}],["snail",{"name":"snail","unicode":"1f40c"}],["octopus",{"name":"octopus","unicode":"1f419"}],["shell",{"name":"shell","unicode":"1f41a"}],["tropical_fish",{"name":"tropical_fish","unicode":"1f420"}],["fish",{"name":"fish","unicode":"1f41f"}],["dolphin",{"name":"dolphin","unicode":"1f42c"}],["flipper",{"name":"flipper","unicode":"1f42c"}],["whale",{"name":"whale","unicode":"1f433"}],["whale2",{"name":"whale2","unicode":"1f40b"}],["cow2",{"name":"cow2","unicode":"1f404"}],["ram",{"name":"ram","unicode":"1f40f"}],["rat",{"name":"rat","unicode":"1f400"}],["water_buffalo",{"name":"water_buffalo","unicode":"1f403"}],["tiger2",{"name":"tiger2","unicode":"1f405"}],["rabbit2",{"name":"rabbit2","unicode":"1f407"}],["dragon",{"name":"dragon","unicode":"1f409"}],["racehorse",{"name":"racehorse","unicode":"1f40e"}],["goat",{"name":"goat","unicode":"1f410"}],["rooster",{"name":"rooster","unicode":"1f413"}],["dog2",{"name":"dog2","unicode":"1f415"}],["pig2",{"name":"pig2","unicode":"1f416"}],["mouse2",{"name":"mouse2","unicode":"1f401"}],["ox",{"name":"ox","unicode":"1f402"}],["dragon_face",{"name":"dragon_face","unicode":"1f432"}],["blowfish",{"name":"blowfish","unicode":"1f421"}],["crocodile",{"name":"crocodile","unicode":"1f40a"}],["camel",{"name":"camel","unicode":"1f42b"}],["dromedary_camel",{"name":"dromedary_camel","unicode":"1f42a"}],["leopard",{"name":"leopard","unicode":"1f406"}],["cat2",{"name":"cat2","unicode":"1f408"}],["poodle",{"name":"poodle","unicode":"1f429"}],["feet",{"name":"feet","unicode":"1f43e"}],["paw_prints",{"name":"paw_prints","unicode":"1f43e"}],["bouquet",{"name":"bouquet","unicode":"1f490"}],["cherry_blossom",{"name":"cherry_blossom","unicode":"1f338"}],["tulip",{"name":"tulip","unicode":"1f337"}],["four_leaf_clover",{"name":"four_leaf_clover","unicode":"1f340"}],["rose",{"name":"rose","unicode":"1f339"}],["sunflower",{"name":"sunflower","unicode":"1f33b"}],["hibiscus",{"name":"hibiscus","unicode":"1f33a"}],["maple_leaf",{"name":"maple_leaf","unicode":"1f341"}],["leaves",{"name":"leaves","unicode":"1f343"}],["fallen_leaf",{"name":"fallen_leaf","unicode":"1f342"}],["herb",{"name":"herb","unicode":"1f33f"}],["ear_of_rice",{"name":"ear_of_rice","unicode":"1f33e"}],["mushroom",{"name":"mushroom","unicode":"1f344"}],["cactus",{"name":"cactus","unicode":"1f335"}],["palm_tree",{"name":"palm_tree","unicode":"1f334"}],["evergreen_tree",{"name":"evergreen_tree","unicode":"1f332"}],["deciduous_tree",{"name":"deciduous_tree","unicode":"1f333"}],["chestnut",{"name":"chestnut","unicode":"1f330"}],["seedling",{"name":"seedling","unicode":"1f331"}],["blossom",{"name":"blossom","unicode":"1f33c"}],["globe_with_meridians",{"name":"globe_with_meridians","unicode":"1f310"}],["sun_with_face",{"name":"sun_with_face","unicode":"1f31e"}],["full_moon_with_face",{"name":"full_moon_with_face","unicode":"1f31d"}],["new_moon_with_face",{"name":"new_moon_with_face","unicode":"1f31a"}],["new_moon",{"name":"new_moon","unicode":"1f311"}],["waxing_crescent_moon",{"name":"waxing_crescent_moon","unicode":"1f312"}],["first_quarter_moon",{"name":"first_quarter_moon","unicode":"1f313"}],["moon",{"name":"moon","unicode":"1f314"}],["waxing_gibbous_moon",{"name":"waxing_gibbous_moon","unicode":"1f314"}],["full_moon",{"name":"full_moon","unicode":"1f315"}],["waning_gibbous_moon",{"name":"waning_gibbous_moon","unicode":"1f316"}],["last_quarter_moon",{"name":"last_quarter_moon","unicode":"1f317"}],["waning_crescent_moon",{"name":"waning_crescent_moon","unicode":"1f318"}],["last_quarter_moon_with_face",{"name":"last_quarter_moon_with_face","unicode":"1f31c"}],["first_quarter_moon_with_face",{"name":"first_quarter_moon_with_face","unicode":"1f31b"}],["crescent_moon",{"name":"crescent_moon","unicode":"1f319"}],["earth_africa",{"name":"earth_africa","unicode":"1f30d"}],["earth_americas",{"name":"earth_americas","unicode":"1f30e"}],["earth_asia",{"name":"earth_asia","unicode":"1f30f"}],["volcano",{"name":"volcano","unicode":"1f30b"}],["milky_way",{"name":"milky_way","unicode":"1f30c"}],["stars",{"name":"stars","unicode":"1f320"}],["star",{"name":"star","unicode":"2b50"}],["sunny",{"name":"sunny","unicode":"2600"}],["partly_sunny",{"name":"partly_sunny","unicode":"26c5"}],["cloud",{"name":"cloud","unicode":"2601"}],["zap",{"name":"zap","unicode":"26a1"}],["umbrella",{"name":"umbrella","unicode":"2614"}],["snowflake",{"name":"snowflake","unicode":"2744"}],["snowman",{"name":"snowman","unicode":"26c4"}],["cyclone",{"name":"cyclone","unicode":"1f300"}],["foggy",{"name":"foggy","unicode":"1f301"}],["rainbow",{"name":"rainbow","unicode":"1f308"}],["ocean",{"name":"ocean","unicode":"1f30a"}],["bamboo",{"name":"bamboo","unicode":"1f38d"}],["gift_heart",{"name":"gift_heart","unicode":"1f49d"}],["dolls",{"name":"dolls","unicode":"1f38e"}],["school_satchel",{"name":"school_satchel","unicode":"1f392"}],["mortar_board",{"name":"mortar_board","unicode":"1f393"}],["flags",{"name":"flags","unicode":"1f38f"}],["fireworks",{"name":"fireworks","unicode":"1f386"}],["sparkler",{"name":"sparkler","unicode":"1f387"}],["wind_chime",{"name":"wind_chime","unicode":"1f390"}],["rice_scene",{"name":"rice_scene","unicode":"1f391"}],["jack_o_lantern",{"name":"jack_o_lantern","unicode":"1f383"}],["ghost",{"name":"ghost","unicode":"1f47b"}],["santa",{"name":"santa","unicode":"1f385"}],["christmas_tree",{"name":"christmas_tree","unicode":"1f384"}],["gift",{"name":"gift","unicode":"1f381"}],["tanabata_tree",{"name":"tanabata_tree","unicode":"1f38b"}],["tada",{"name":"tada","unicode":"1f389"}],["confetti_ball",{"name":"confetti_ball","unicode":"1f38a"}],["balloon",{"name":"balloon","unicode":"1f388"}],["crossed_flags",{"name":"crossed_flags","unicode":"1f38c"}],["crystal_ball",{"name":"crystal_ball","unicode":"1f52e"}],["movie_camera",{"name":"movie_camera","unicode":"1f3a5"}],["camera",{"name":"camera","unicode":"1f4f7"}],["video_camera",{"name":"video_camera","unicode":"1f4f9"}],["vhs",{"name":"vhs","unicode":"1f4fc"}],["cd",{"name":"cd","unicode":"1f4bf"}],["dvd",{"name":"dvd","unicode":"1f4c0"}],["minidisc",{"name":"minidisc","unicode":"1f4bd"}],["floppy_disk",{"name":"floppy_disk","unicode":"1f4be"}],["computer",{"name":"computer","unicode":"1f4bb"}],["iphone",{"name":"iphone","unicode":"1f4f1"}],["phone",{"name":"phone","unicode":"260e"}],["telephone",{"name":"telephone","unicode":"260e"}],["telephone_receiver",{"name":"telephone_receiver","unicode":"1f4de"}],["pager",{"name":"pager","unicode":"1f4df"}],["fax",{"name":"fax","unicode":"1f4e0"}],["satellite",{"name":"satellite","unicode":"1f4e1"}],["tv",{"name":"tv","unicode":"1f4fa"}],["radio",{"name":"radio","unicode":"1f4fb"}],["loud_sound",{"name":"loud_sound","unicode":"1f50a"}],["sound",{"name":"sound","unicode":"1f509"}],["speaker",{"name":"speaker","unicode":"1f508"}],["mute",{"name":"mute","unicode":"1f507"}],["bell",{"name":"bell","unicode":"1f514"}],["no_bell",{"name":"no_bell","unicode":"1f515"}],["loudspeaker",{"name":"loudspeaker","unicode":"1f4e2"}],["mega",{"name":"mega","unicode":"1f4e3"}],["hourglass_flowing_sand",{"name":"hourglass_flowing_sand","unicode":"23f3"}],["hourglass",{"name":"hourglass","unicode":"231b"}],["alarm_clock",{"name":"alarm_clock","unicode":"23f0"}],["watch",{"name":"watch","unicode":"231a"}],["unlock",{"name":"unlock","unicode":"1f513"}],["lock",{"name":"lock","unicode":"1f512"}],["lock_with_ink_pen",{"name":"lock_with_ink_pen","unicode":"1f50f"}],["closed_lock_with_key",{"name":"closed_lock_with_key","unicode":"1f510"}],["key",{"name":"key","unicode":"1f511"}],["mag_right",{"name":"mag_right","unicode":"1f50e"}],["bulb",{"name":"bulb","unicode":"1f4a1"}],["flashlight",{"name":"flashlight","unicode":"1f526"}],["high_brightness",{"name":"high_brightness","unicode":"1f506"}],["low_brightness",{"name":"low_brightness","unicode":"1f505"}],["electric_plug",{"name":"electric_plug","unicode":"1f50c"}],["battery",{"name":"battery","unicode":"1f50b"}],["mag",{"name":"mag","unicode":"1f50d"}],["bathtub",{"name":"bathtub","unicode":"1f6c1"}],["bath",{"name":"bath","unicode":"1f6c0"}],["shower",{"name":"shower","unicode":"1f6bf"}],["toilet",{"name":"toilet","unicode":"1f6bd"}],["wrench",{"name":"wrench","unicode":"1f527"}],["nut_and_bolt",{"name":"nut_and_bolt","unicode":"1f529"}],["hammer",{"name":"hammer","unicode":"1f528"}],["door",{"name":"door","unicode":"1f6aa"}],["smoking",{"name":"smoking","unicode":"1f6ac"}],["bomb",{"name":"bomb","unicode":"1f4a3"}],["gun",{"name":"gun","unicode":"1f52b"}],["hocho",{"name":"hocho","unicode":"1f52a"}],["knife",{"name":"knife","unicode":"1f52a"}],["pill",{"name":"pill","unicode":"1f48a"}],["syringe",{"name":"syringe","unicode":"1f489"}],["moneybag",{"name":"moneybag","unicode":"1f4b0"}],["yen",{"name":"yen","unicode":"1f4b4"}],["dollar",{"name":"dollar","unicode":"1f4b5"}],["pound",{"name":"pound","unicode":"1f4b7"}],["euro",{"name":"euro","unicode":"1f4b6"}],["credit_card",{"name":"credit_card","unicode":"1f4b3"}],["money_with_wings",{"name":"money_with_wings","unicode":"1f4b8"}],["calling",{"name":"calling","unicode":"1f4f2"}],["e-mail",{"name":"e-mail","unicode":"1f4e7"}],["inbox_tray",{"name":"inbox_tray","unicode":"1f4e5"}],["outbox_tray",{"name":"outbox_tray","unicode":"1f4e4"}],["email",{"name":"email","unicode":"2709"}],["envelope",{"name":"envelope","unicode":"2709"}],["envelope_with_arrow",{"name":"envelope_with_arrow","unicode":"1f4e9"}],["incoming_envelope",{"name":"incoming_envelope","unicode":"1f4e8"}],["postal_horn",{"name":"postal_horn","unicode":"1f4ef"}],["mailbox",{"name":"mailbox","unicode":"1f4eb"}],["mailbox_closed",{"name":"mailbox_closed","unicode":"1f4ea"}],["mailbox_with_mail",{"name":"mailbox_with_mail","unicode":"1f4ec"}],["mailbox_with_no_mail",{"name":"mailbox_with_no_mail","unicode":"1f4ed"}],["postbox",{"name":"postbox","unicode":"1f4ee"}],["package",{"name":"package","unicode":"1f4e6"}],["memo",{"name":"memo","unicode":"1f4dd"}],["pencil",{"name":"pencil","unicode":"1f4dd"}],["page_facing_up",{"name":"page_facing_up","unicode":"1f4c4"}],["page_with_curl",{"name":"page_with_curl","unicode":"1f4c3"}],["bookmark_tabs",{"name":"bookmark_tabs","unicode":"1f4d1"}],["bar_chart",{"name":"bar_chart","unicode":"1f4ca"}],["chart_with_upwards_trend",{"name":"chart_with_upwards_trend","unicode":"1f4c8"}],["chart_with_downwards_trend",{"name":"chart_with_downwards_trend","unicode":"1f4c9"}],["scroll",{"name":"scroll","unicode":"1f4dc"}],["clipboard",{"name":"clipboard","unicode":"1f4cb"}],["date",{"name":"date","unicode":"1f4c5"}],["calendar",{"name":"calendar","unicode":"1f4c6"}],["card_index",{"name":"card_index","unicode":"1f4c7"}],["file_folder",{"name":"file_folder","unicode":"1f4c1"}],["open_file_folder",{"name":"open_file_folder","unicode":"1f4c2"}],["scissors",{"name":"scissors","unicode":"2702"}],["pushpin",{"name":"pushpin","unicode":"1f4cc"}],["paperclip",{"name":"paperclip","unicode":"1f4ce"}],["black_nib",{"name":"black_nib","unicode":"2712"}],["pencil2",{"name":"pencil2","unicode":"270f"}],["straight_ruler",{"name":"straight_ruler","unicode":"1f4cf"}],["triangular_ruler",{"name":"triangular_ruler","unicode":"1f4d0"}],["closed_book",{"name":"closed_book","unicode":"1f4d5"}],["green_book",{"name":"green_book","unicode":"1f4d7"}],["blue_book",{"name":"blue_book","unicode":"1f4d8"}],["orange_book",{"name":"orange_book","unicode":"1f4d9"}],["notebook",{"name":"notebook","unicode":"1f4d3"}],["notebook_with_decorative_cover",{"name":"notebook_with_decorative_cover","unicode":"1f4d4"}],["ledger",{"name":"ledger","unicode":"1f4d2"}],["books",{"name":"books","unicode":"1f4da"}],["book",{"name":"book","unicode":"1f4d6"}],["open_book",{"name":"open_book","unicode":"1f4d6"}],["bookmark",{"name":"bookmark","unicode":"1f516"}],["name_badge",{"name":"name_badge","unicode":"1f4db"}],["microscope",{"name":"microscope","unicode":"1f52c"}],["telescope",{"name":"telescope","unicode":"1f52d"}],["newspaper",{"name":"newspaper","unicode":"1f4f0"}],["art",{"name":"art","unicode":"1f3a8"}],["clapper",{"name":"clapper","unicode":"1f3ac"}],["microphone",{"name":"microphone","unicode":"1f3a4"}],["headphones",{"name":"headphones","unicode":"1f3a7"}],["musical_score",{"name":"musical_score","unicode":"1f3bc"}],["musical_note",{"name":"musical_note","unicode":"1f3b5"}],["notes",{"name":"notes","unicode":"1f3b6"}],["musical_keyboard",{"name":"musical_keyboard","unicode":"1f3b9"}],["violin",{"name":"violin","unicode":"1f3bb"}],["trumpet",{"name":"trumpet","unicode":"1f3ba"}],["saxophone",{"name":"saxophone","unicode":"1f3b7"}],["guitar",{"name":"guitar","unicode":"1f3b8"}],["space_invader",{"name":"space_invader","unicode":"1f47e"}],["video_game",{"name":"video_game","unicode":"1f3ae"}],["black_joker",{"name":"black_joker","unicode":"1f0cf"}],["flower_playing_cards",{"name":"flower_playing_cards","unicode":"1f3b4"}],["mahjong",{"name":"mahjong","unicode":"1f004"}],["game_die",{"name":"game_die","unicode":"1f3b2"}],["dart",{"name":"dart","unicode":"1f3af"}],["football",{"name":"football","unicode":"1f3c8"}],["basketball",{"name":"basketball","unicode":"1f3c0"}],["soccer",{"name":"soccer","unicode":"26bd"}],["baseball",{"name":"baseball","unicode":"26be"}],["tennis",{"name":"tennis","unicode":"1f3be"}],["8ball",{"name":"8ball","unicode":"1f3b1"}],["rugby_football",{"name":"rugby_football","unicode":"1f3c9"}],["bowling",{"name":"bowling","unicode":"1f3b3"}],["golf",{"name":"golf","unicode":"26f3"}],["mountain_bicyclist",{"name":"mountain_bicyclist","unicode":"1f6b5"}],["bicyclist",{"name":"bicyclist","unicode":"1f6b4"}],["checkered_flag",{"name":"checkered_flag","unicode":"1f3c1"}],["horse_racing",{"name":"horse_racing","unicode":"1f3c7"}],["trophy",{"name":"trophy","unicode":"1f3c6"}],["ski",{"name":"ski","unicode":"1f3bf"}],["snowboarder",{"name":"snowboarder","unicode":"1f3c2"}],["swimmer",{"name":"swimmer","unicode":"1f3ca"}],["surfer",{"name":"surfer","unicode":"1f3c4"}],["fishing_pole_and_fish",{"name":"fishing_pole_and_fish","unicode":"1f3a3"}],["coffee",{"name":"coffee","unicode":"2615"}],["tea",{"name":"tea","unicode":"1f375"}],["sake",{"name":"sake","unicode":"1f376"}],["baby_bottle",{"name":"baby_bottle","unicode":"1f37c"}],["beer",{"name":"beer","unicode":"1f37a"}],["beers",{"name":"beers","unicode":"1f37b"}],["cocktail",{"name":"cocktail","unicode":"1f378"}],["tropical_drink",{"name":"tropical_drink","unicode":"1f379"}],["wine_glass",{"name":"wine_glass","unicode":"1f377"}],["fork_and_knife",{"name":"fork_and_knife","unicode":"1f374"}],["pizza",{"name":"pizza","unicode":"1f355"}],["hamburger",{"name":"hamburger","unicode":"1f354"}],["fries",{"name":"fries","unicode":"1f35f"}],["poultry_leg",{"name":"poultry_leg","unicode":"1f357"}],["meat_on_bone",{"name":"meat_on_bone","unicode":"1f356"}],["spaghetti",{"name":"spaghetti","unicode":"1f35d"}],["curry",{"name":"curry","unicode":"1f35b"}],["fried_shrimp",{"name":"fried_shrimp","unicode":"1f364"}],["bento",{"name":"bento","unicode":"1f371"}],["sushi",{"name":"sushi","unicode":"1f363"}],["fish_cake",{"name":"fish_cake","unicode":"1f365"}],["rice_ball",{"name":"rice_ball","unicode":"1f359"}],["rice_cracker",{"name":"rice_cracker","unicode":"1f358"}],["rice",{"name":"rice","unicode":"1f35a"}],["ramen",{"name":"ramen","unicode":"1f35c"}],["stew",{"name":"stew","unicode":"1f372"}],["oden",{"name":"oden","unicode":"1f362"}],["dango",{"name":"dango","unicode":"1f361"}],["egg",{"name":"egg","unicode":"1f373"}],["bread",{"name":"bread","unicode":"1f35e"}],["doughnut",{"name":"doughnut","unicode":"1f369"}],["custard",{"name":"custard","unicode":"1f36e"}],["icecream",{"name":"icecream","unicode":"1f366"}],["ice_cream",{"name":"ice_cream","unicode":"1f368"}],["shaved_ice",{"name":"shaved_ice","unicode":"1f367"}],["birthday",{"name":"birthday","unicode":"1f382"}],["cake",{"name":"cake","unicode":"1f370"}],["cookie",{"name":"cookie","unicode":"1f36a"}],["chocolate_bar",{"name":"chocolate_bar","unicode":"1f36b"}],["candy",{"name":"candy","unicode":"1f36c"}],["lollipop",{"name":"lollipop","unicode":"1f36d"}],["honey_pot",{"name":"honey_pot","unicode":"1f36f"}],["apple",{"name":"apple","unicode":"1f34e"}],["green_apple",{"name":"green_apple","unicode":"1f34f"}],["tangerine",{"name":"tangerine","unicode":"1f34a"}],["orange",{"name":"orange","unicode":"1f34a"}],["mandarin",{"name":"mandarin","unicode":"1f34a"}],["lemon",{"name":"lemon","unicode":"1f34b"}],["cherries",{"name":"cherries","unicode":"1f352"}],["grapes",{"name":"grapes","unicode":"1f347"}],["watermelon",{"name":"watermelon","unicode":"1f349"}],["strawberry",{"name":"strawberry","unicode":"1f353"}],["peach",{"name":"peach","unicode":"1f351"}],["melon",{"name":"melon","unicode":"1f348"}],["banana",{"name":"banana","unicode":"1f34c"}],["pear",{"name":"pear","unicode":"1f350"}],["pineapple",{"name":"pineapple","unicode":"1f34d"}],["sweet_potato",{"name":"sweet_potato","unicode":"1f360"}],["eggplant",{"name":"eggplant","unicode":"1f346"}],["tomato",{"name":"tomato","unicode":"1f345"}],["corn",{"name":"corn","unicode":"1f33d"}],["house",{"name":"house","unicode":"1f3e0"}],["house_with_garden",{"name":"house_with_garden","unicode":"1f3e1"}],["school",{"name":"school","unicode":"1f3eb"}],["office",{"name":"office","unicode":"1f3e2"}],["post_office",{"name":"post_office","unicode":"1f3e3"}],["hospital",{"name":"hospital","unicode":"1f3e5"}],["bank",{"name":"bank","unicode":"1f3e6"}],["convenience_store",{"name":"convenience_store","unicode":"1f3ea"}],["love_hotel",{"name":"love_hotel","unicode":"1f3e9"}],["hotel",{"name":"hotel","unicode":"1f3e8"}],["wedding",{"name":"wedding","unicode":"1f492"}],["church",{"name":"church","unicode":"26ea"}],["department_store",{"name":"department_store","unicode":"1f3ec"}],["european_post_office",{"name":"european_post_office","unicode":"1f3e4"}],["city_sunrise",{"name":"city_sunrise","unicode":"1f307"}],["city_sunset",{"name":"city_sunset","unicode":"1f306"}],["japanese_castle",{"name":"japanese_castle","unicode":"1f3ef"}],["european_castle",{"name":"european_castle","unicode":"1f3f0"}],["tent",{"name":"tent","unicode":"26fa"}],["factory",{"name":"factory","unicode":"1f3ed"}],["tokyo_tower",{"name":"tokyo_tower","unicode":"1f5fc"}],["japan",{"name":"japan","unicode":"1f5fe"}],["mount_fuji",{"name":"mount_fuji","unicode":"1f5fb"}],["sunrise_over_mountains",{"name":"sunrise_over_mountains","unicode":"1f304"}],["sunrise",{"name":"sunrise","unicode":"1f305"}],["night_with_stars",{"name":"night_with_stars","unicode":"1f303"}],["statue_of_liberty",{"name":"statue_of_liberty","unicode":"1f5fd"}],["bridge_at_night",{"name":"bridge_at_night","unicode":"1f309"}],["carousel_horse",{"name":"carousel_horse","unicode":"1f3a0"}],["ferris_wheel",{"name":"ferris_wheel","unicode":"1f3a1"}],["fountain",{"name":"fountain","unicode":"26f2"}],["roller_coaster",{"name":"roller_coaster","unicode":"1f3a2"}],["ship",{"name":"ship","unicode":"1f6a2"}],["boat",{"name":"boat","unicode":"26f5"}],["sailboat",{"name":"sailboat","unicode":"26f5"}],["speedboat",{"name":"speedboat","unicode":"1f6a4"}],["rowboat",{"name":"rowboat","unicode":"1f6a3"}],["anchor",{"name":"anchor","unicode":"2693"}],["rocket",{"name":"rocket","unicode":"1f680"}],["airplane",{"name":"airplane","unicode":"2708"}],["seat",{"name":"seat","unicode":"1f4ba"}],["helicopter",{"name":"helicopter","unicode":"1f681"}],["steam_locomotive",{"name":"steam_locomotive","unicode":"1f682"}],["tram",{"name":"tram","unicode":"1f68a"}],["station",{"name":"station","unicode":"1f689"}],["mountain_railway",{"name":"mountain_railway","unicode":"1f69e"}],["train2",{"name":"train2","unicode":"1f686"}],["bullettrain_side",{"name":"bullettrain_side","unicode":"1f684"}],["bullettrain_front",{"name":"bullettrain_front","unicode":"1f685"}],["light_rail",{"name":"light_rail","unicode":"1f688"}],["metro",{"name":"metro","unicode":"1f687"}],["monorail",{"name":"monorail","unicode":"1f69d"}],["train",{"name":"train","unicode":"1f68b"}],["railway_car",{"name":"railway_car","unicode":"1f683"}],["trolleybus",{"name":"trolleybus","unicode":"1f68e"}],["bus",{"name":"bus","unicode":"1f68c"}],["oncoming_bus",{"name":"oncoming_bus","unicode":"1f68d"}],["blue_car",{"name":"blue_car","unicode":"1f699"}],["oncoming_automobile",{"name":"oncoming_automobile","unicode":"1f698"}],["car",{"name":"car","unicode":"1f697"}],["red_car",{"name":"red_car","unicode":"1f697"}],["taxi",{"name":"taxi","unicode":"1f695"}],["oncoming_taxi",{"name":"oncoming_taxi","unicode":"1f696"}],["articulated_lorry",{"name":"articulated_lorry","unicode":"1f69b"}],["truck",{"name":"truck","unicode":"1f69a"}],["rotating_light",{"name":"rotating_light","unicode":"1f6a8"}],["police_car",{"name":"police_car","unicode":"1f693"}],["oncoming_police_car",{"name":"oncoming_police_car","unicode":"1f694"}],["fire_engine",{"name":"fire_engine","unicode":"1f692"}],["ambulance",{"name":"ambulance","unicode":"1f691"}],["minibus",{"name":"minibus","unicode":"1f690"}],["bike",{"name":"bike","unicode":"1f6b2"}],["aerial_tramway",{"name":"aerial_tramway","unicode":"1f6a1"}],["suspension_railway",{"name":"suspension_railway","unicode":"1f69f"}],["mountain_cableway",{"name":"mountain_cableway","unicode":"1f6a0"}],["tractor",{"name":"tractor","unicode":"1f69c"}],["barber",{"name":"barber","unicode":"1f488"}],["busstop",{"name":"busstop","unicode":"1f68f"}],["ticket",{"name":"ticket","unicode":"1f3ab"}],["vertical_traffic_light",{"name":"vertical_traffic_light","unicode":"1f6a6"}],["traffic_light",{"name":"traffic_light","unicode":"1f6a5"}],["warning",{"name":"warning","unicode":"26a0"}],["construction",{"name":"construction","unicode":"1f6a7"}],["beginner",{"name":"beginner","unicode":"1f530"}],["fuelpump",{"name":"fuelpump","unicode":"26fd"}],["izakaya_lantern",{"name":"izakaya_lantern","unicode":"1f3ee"}],["lantern",{"name":"lantern","unicode":"1f3ee"}],["slot_machine",{"name":"slot_machine","unicode":"1f3b0"}],["hotsprings",{"name":"hotsprings","unicode":"2668"}],["moyai",{"name":"moyai","unicode":"1f5ff"}],["circus_tent",{"name":"circus_tent","unicode":"1f3aa"}],["performing_arts",{"name":"performing_arts","unicode":"1f3ad"}],["round_pushpin",{"name":"round_pushpin","unicode":"1f4cd"}],["triangular_flag_on_post",{"name":"triangular_flag_on_post","unicode":"1f6a9"}],["jp",{"name":"jp","unicode":"1f1ef-1f1f5"}],["kr",{"name":"kr","unicode":"1f1f0-1f1f7"}],["de",{"name":"de","unicode":"1f1e9-1f1ea"}],["cn",{"name":"cn","unicode":"1f1e8-1f1f3"}],["us",{"name":"us","unicode":"1f1fa-1f1f8"}],["fr",{"name":"fr","unicode":"1f1eb-1f1f7"}],["es",{"name":"es","unicode":"1f1ea-1f1f8"}],["it",{"name":"it","unicode":"1f1ee-1f1f9"}],["ru",{"name":"ru","unicode":"1f1f7-1f1fa"}],["gb",{"name":"gb","unicode":"1f1ec-1f1e7"}],["uk",{"name":"uk","unicode":"1f1ec-1f1e7"}],["one",{"name":"one","unicode":"0031-20e3"}],["two",{"name":"two","unicode":"0032-20e3"}],["three",{"name":"three","unicode":"0033-20e3"}],["four",{"name":"four","unicode":"0034-20e3"}],["five",{"name":"five","unicode":"0035-20e3"}],["six",{"name":"six","unicode":"0036-20e3"}],["seven",{"name":"seven","unicode":"0037-20e3"}],["eight",{"name":"eight","unicode":"0038-20e3"}],["nine",{"name":"nine","unicode":"0039-20e3"}],["zero",{"name":"zero","unicode":"0030-20e3"}],["keycap_ten",{"name":"keycap_ten","unicode":"1f51f"}],["1234",{"name":"1234","unicode":"1f522"}],["hash",{"name":"hash","unicode":"0023-20e3"}],["symbols",{"name":"symbols","unicode":"1f523"}],["arrow_up",{"name":"arrow_up","unicode":"2b06"}],["arrow_down",{"name":"arrow_down","unicode":"2b07"}],["arrow_left",{"name":"arrow_left","unicode":"2b05"}],["arrow_right",{"name":"arrow_right","unicode":"27a1"}],["capital_abcd",{"name":"capital_abcd","unicode":"1f520"}],["abcd",{"name":"abcd","unicode":"1f521"}],["abc",{"name":"abc","unicode":"1f524"}],["arrow_upper_right",{"name":"arrow_upper_right","unicode":"2197"}],["arrow_upper_left",{"name":"arrow_upper_left","unicode":"2196"}],["arrow_lower_right",{"name":"arrow_lower_right","unicode":"2198"}],["arrow_lower_left",{"name":"arrow_lower_left","unicode":"2199"}],["left_right_arrow",{"name":"left_right_arrow","unicode":"2194"}],["arrow_up_down",{"name":"arrow_up_down","unicode":"2195"}],["arrows_counterclockwise",{"name":"arrows_counterclockwise","unicode":"1f504"}],["arrow_backward",{"name":"arrow_backward","unicode":"25c0"}],["arrow_forward",{"name":"arrow_forward","unicode":"25b6"}],["arrow_up_small",{"name":"arrow_up_small","unicode":"1f53c"}],["arrow_down_small",{"name":"arrow_down_small","unicode":"1f53d"}],["leftwards_arrow_with_hook",{"name":"leftwards_arrow_with_hook","unicode":"21a9"}],["arrow_right_hook",{"name":"arrow_right_hook","unicode":"21aa"}],["information_source",{"name":"information_source","unicode":"2139"}],["rewind",{"name":"rewind","unicode":"23ea"}],["fast_forward",{"name":"fast_forward","unicode":"23e9"}],["arrow_double_up",{"name":"arrow_double_up","unicode":"23eb"}],["arrow_double_down",{"name":"arrow_double_down","unicode":"23ec"}],["arrow_heading_down",{"name":"arrow_heading_down","unicode":"2935"}],["arrow_heading_up",{"name":"arrow_heading_up","unicode":"2934"}],["ok",{"name":"ok","unicode":"1f197"}],["twisted_rightwards_arrows",{"name":"twisted_rightwards_arrows","unicode":"1f500"}],["repeat",{"name":"repeat","unicode":"1f501"}],["repeat_one",{"name":"repeat_one","unicode":"1f502"}],["new",{"name":"new","unicode":"1f195"}],["up",{"name":"up","unicode":"1f199"}],["cool",{"name":"cool","unicode":"1f192"}],["free",{"name":"free","unicode":"1f193"}],["ng",{"name":"ng","unicode":"1f196"}],["signal_strength",{"name":"signal_strength","unicode":"1f4f6"}],["cinema",{"name":"cinema","unicode":"1f3a6"}],["koko",{"name":"koko","unicode":"1f201"}],["u6307",{"name":"u6307","unicode":"1f22f"}],["u7a7a",{"name":"u7a7a","unicode":"1f233"}],["u6e80",{"name":"u6e80","unicode":"1f235"}],["u5408",{"name":"u5408","unicode":"1f234"}],["u7981",{"name":"u7981","unicode":"1f232"}],["ideograph_advantage",{"name":"ideograph_advantage","unicode":"1f250"}],["u5272",{"name":"u5272","unicode":"1f239"}],["u55b6",{"name":"u55b6","unicode":"1f23a"}],["u6709",{"name":"u6709","unicode":"1f236"}],["u7121",{"name":"u7121","unicode":"1f21a"}],["restroom",{"name":"restroom","unicode":"1f6bb"}],["mens",{"name":"mens","unicode":"1f6b9"}],["womens",{"name":"womens","unicode":"1f6ba"}],["baby_symbol",{"name":"baby_symbol","unicode":"1f6bc"}],["wc",{"name":"wc","unicode":"1f6be"}],["potable_water",{"name":"potable_water","unicode":"1f6b0"}],["put_litter_in_its_place",{"name":"put_litter_in_its_place","unicode":"1f6ae"}],["parking",{"name":"parking","unicode":"1f17f"}],["wheelchair",{"name":"wheelchair","unicode":"267f"}],["no_smoking",{"name":"no_smoking","unicode":"1f6ad"}],["u6708",{"name":"u6708","unicode":"1f237"}],["u7533",{"name":"u7533","unicode":"1f238"}],["sa",{"name":"sa","unicode":"1f202"}],["m",{"name":"m","unicode":"24c2"}],["passport_control",{"name":"passport_control","unicode":"1f6c2"}],["baggage_claim",{"name":"baggage_claim","unicode":"1f6c4"}],["left_luggage",{"name":"left_luggage","unicode":"1f6c5"}],["customs",{"name":"customs","unicode":"1f6c3"}],["accept",{"name":"accept","unicode":"1f251"}],["secret",{"name":"secret","unicode":"3299"}],["congratulations",{"name":"congratulations","unicode":"3297"}],["cl",{"name":"cl","unicode":"1f191"}],["sos",{"name":"sos","unicode":"1f198"}],["id",{"name":"id","unicode":"1f194"}],["no_entry_sign",{"name":"no_entry_sign","unicode":"1f6ab"}],["underage",{"name":"underage","unicode":"1f51e"}],["no_mobile_phones",{"name":"no_mobile_phones","unicode":"1f4f5"}],["do_not_litter",{"name":"do_not_litter","unicode":"1f6af"}],["non-potable_water",{"name":"non-potable_water","unicode":"1f6b1"}],["no_bicycles",{"name":"no_bicycles","unicode":"1f6b3"}],["no_pedestrians",{"name":"no_pedestrians","unicode":"1f6b7"}],["children_crossing",{"name":"children_crossing","unicode":"1f6b8"}],["no_entry",{"name":"no_entry","unicode":"26d4"}],["eight_spoked_asterisk",{"name":"eight_spoked_asterisk","unicode":"2733"}],["sparkle",{"name":"sparkle","unicode":"2747"}],["negative_squared_cross_mark",{"name":"negative_squared_cross_mark","unicode":"274e"}],["white_check_mark",{"name":"white_check_mark","unicode":"2705"}],["eight_pointed_black_star",{"name":"eight_pointed_black_star","unicode":"2734"}],["heart_decoration",{"name":"heart_decoration","unicode":"1f49f"}],["vs",{"name":"vs","unicode":"1f19a"}],["vibration_mode",{"name":"vibration_mode","unicode":"1f4f3"}],["mobile_phone_off",{"name":"mobile_phone_off","unicode":"1f4f4"}],["a",{"name":"a","unicode":"1f170"}],["b",{"name":"b","unicode":"1f171"}],["ab",{"name":"ab","unicode":"1f18e"}],["o2",{"name":"o2","unicode":"1f17e"}],["diamond_shape_with_a_dot_inside",{"name":"diamond_shape_with_a_dot_inside","unicode":"1f4a0"}],["loop",{"name":"loop","unicode":"27bf"}],["recycle",{"name":"recycle","unicode":"267b"}],["aries",{"name":"aries","unicode":"2648"}],["taurus",{"name":"taurus","unicode":"2649"}],["gemini",{"name":"gemini","unicode":"264a"}],["cancer",{"name":"cancer","unicode":"264b"}],["leo",{"name":"leo","unicode":"264c"}],["virgo",{"name":"virgo","unicode":"264d"}],["libra",{"name":"libra","unicode":"264e"}],["scorpius",{"name":"scorpius","unicode":"264f"}],["sagittarius",{"name":"sagittarius","unicode":"2650"}],["capricorn",{"name":"capricorn","unicode":"2651"}],["aquarius",{"name":"aquarius","unicode":"2652"}],["pisces",{"name":"pisces","unicode":"2653"}],["ophiuchus",{"name":"ophiuchus","unicode":"26ce"}],["six_pointed_star",{"name":"six_pointed_star","unicode":"1f52f"}],["atm",{"name":"atm","unicode":"1f3e7"}],["chart",{"name":"chart","unicode":"1f4b9"}],["heavy_dollar_sign",{"name":"heavy_dollar_sign","unicode":"1f4b2"}],["currency_exchange",{"name":"currency_exchange","unicode":"1f4b1"}],["copyright",{"name":"copyright","unicode":"00a9"}],["registered",{"name":"registered","unicode":"00ae"}],["tm",{"name":"tm","unicode":"2122"}],["x",{"name":"x","unicode":"274c"}],["bangbang",{"name":"bangbang","unicode":"203c"}],["interrobang",{"name":"interrobang","unicode":"2049"}],["exclamation",{"name":"exclamation","unicode":"2757"}],["heavy_exclamation_mark",{"name":"heavy_exclamation_mark","unicode":"2757"}],["question",{"name":"question","unicode":"2753"}],["grey_exclamation",{"name":"grey_exclamation","unicode":"2755"}],["grey_question",{"name":"grey_question","unicode":"2754"}],["o",{"name":"o","unicode":"2b55"}],["top",{"name":"top","unicode":"1f51d"}],["end",{"name":"end","unicode":"1f51a"}],["back",{"name":"back","unicode":"1f519"}],["on",{"name":"on","unicode":"1f51b"}],["soon",{"name":"soon","unicode":"1f51c"}],["arrows_clockwise",{"name":"arrows_clockwise","unicode":"1f503"}],["clock12",{"name":"clock12","unicode":"1f55b"}],["clock1230",{"name":"clock1230","unicode":"1f567"}],["clock1",{"name":"clock1","unicode":"1f550"}],["clock130",{"name":"clock130","unicode":"1f55c"}],["clock2",{"name":"clock2","unicode":"1f551"}],["clock230",{"name":"clock230","unicode":"1f55d"}],["clock3",{"name":"clock3","unicode":"1f552"}],["clock330",{"name":"clock330","unicode":"1f55e"}],["clock4",{"name":"clock4","unicode":"1f553"}],["clock430",{"name":"clock430","unicode":"1f55f"}],["clock5",{"name":"clock5","unicode":"1f554"}],["clock530",{"name":"clock530","unicode":"1f560"}],["clock6",{"name":"clock6","unicode":"1f555"}],["clock7",{"name":"clock7","unicode":"1f556"}],["clock8",{"name":"clock8","unicode":"1f557"}],["clock9",{"name":"clock9","unicode":"1f558"}],["clock10",{"name":"clock10","unicode":"1f559"}],["clock11",{"name":"clock11","unicode":"1f55a"}],["clock630",{"name":"clock630","unicode":"1f561"}],["clock730",{"name":"clock730","unicode":"1f562"}],["clock830",{"name":"clock830","unicode":"1f563"}],["clock930",{"name":"clock930","unicode":"1f564"}],["clock1030",{"name":"clock1030","unicode":"1f565"}],["clock1130",{"name":"clock1130","unicode":"1f566"}],["heavy_multiplication_x",{"name":"heavy_multiplication_x","unicode":"2716"}],["heavy_plus_sign",{"name":"heavy_plus_sign","unicode":"2795"}],["heavy_minus_sign",{"name":"heavy_minus_sign","unicode":"2796"}],["heavy_division_sign",{"name":"heavy_division_sign","unicode":"2797"}],["spades",{"name":"spades","unicode":"2660"}],["hearts",{"name":"hearts","unicode":"2665"}],["clubs",{"name":"clubs","unicode":"2663"}],["diamonds",{"name":"diamonds","unicode":"2666"}],["white_flower",{"name":"white_flower","unicode":"1f4ae"}],["100",{"name":"100","unicode":"1f4af"}],["heavy_check_mark",{"name":"heavy_check_mark","unicode":"2714"}],["ballot_box_with_check",{"name":"ballot_box_with_check","unicode":"2611"}],["radio_button",{"name":"radio_button","unicode":"1f518"}],["link",{"name":"link","unicode":"1f517"}],["curly_loop",{"name":"curly_loop","unicode":"27b0"}],["wavy_dash",{"name":"wavy_dash","unicode":"3030"}],["part_alternation_mark",{"name":"part_alternation_mark","unicode":"303d"}],["trident",{"name":"trident","unicode":"1f531"}],["black_medium_square",{"name":"black_medium_square","unicode":"25fc"}],["white_medium_square",{"name":"white_medium_square","unicode":"25fb"}],["black_medium_small_square",{"name":"black_medium_small_square","unicode":"25fe"}],["white_medium_small_square",{"name":"white_medium_small_square","unicode":"25fd"}],["black_small_square",{"name":"black_small_square","unicode":"25aa"}],["white_small_square",{"name":"white_small_square","unicode":"25ab"}],["small_red_triangle",{"name":"small_red_triangle","unicode":"1f53a"}],["black_square_button",{"name":"black_square_button","unicode":"1f532"}],["white_square_button",{"name":"white_square_button","unicode":"1f533"}],["black_circle",{"name":"black_circle","unicode":"26ab"}],["white_circle",{"name":"white_circle","unicode":"26aa"}],["red_circle",{"name":"red_circle","unicode":"1f534"}],["large_blue_circle",{"name":"large_blue_circle","unicode":"1f535"}],["small_red_triangle_down",{"name":"small_red_triangle_down","unicode":"1f53b"}],["white_large_square",{"name":"white_large_square","unicode":"2b1c"}],["black_large_square",{"name":"black_large_square","unicode":"2b1b"}],["large_orange_diamond",{"name":"large_orange_diamond","unicode":"1f536"}],["large_blue_diamond",{"name":"large_blue_diamond","unicode":"1f537"}],["small_orange_diamond",{"name":"small_orange_diamond","unicode":"1f538"}],["small_blue_diamond",{"name":"small_blue_diamond","unicode":"1f539"}],["ca",{"name":"ca","unicode":"1f1e8-1f1e6"}],["eh",{"name":"eh","unicode":"1f1e8-1f1e6"}],["pk",{"name":"pk","unicode":"1f1f5-1f1f0"}],["za",{"name":"za","unicode":"1f1ff-1f1e6"}],["slightly_smiling_face",{"name":"slightly_smiling_face","unicode":"1f642"}],["slightly_frowning_face",{"name":"slightly_frowning_face","unicode":"1f641"}],["upside_down_face",{"name":"upside_down_face","unicode":"1f643"}],["mm",{"name":"mm"}],["mattermost",{"name":"mattermost","filename":"mm"}],["basecamp",{"name":"basecamp"}],["basecampy",{"name":"basecampy"}],["bowtie",{"name":"bowtie"}],["feelsgood",{"name":"feelsgood"}],["finnadie",{"name":"finnadie"}],["fu",{"name":"fu"}],["goberserk",{"name":"goberserk"}],["godmode",{"name":"godmode"}],["hurtrealbad",{"name":"hurtrealbad"}],["metal",{"name":"metal"}],["neckbeard",{"name":"neckbeard"}],["octocat",{"name":"octocat"}],["rage1",{"name":"rage1"}],["rage2",{"name":"rage2"}],["rage3",{"name":"rage3"}],["rage4",{"name":"rage4"}],["shipit",{"name":"shipit"}],["squirrel",{"name":"squirrel","filename":"shipit"}],["suspect",{"name":"suspect"}],["taco",{"name":"taco"}],["trollface",{"name":"trollface"}]] \ No newline at end of file
diff --git a/webapp/utils/emoji.jsx b/webapp/utils/emoji.jsx
new file mode 100644
index 000000000..e129baae8
--- /dev/null
+++ b/webapp/utils/emoji.jsx
@@ -0,0 +1,18 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+// This file is automatically generated. Make changes to it at your own risk.
+
+/* eslint-disable */
+
+export const Emojis = [{"aliases":["smile"],"filename":"1f604"},{"aliases":["smiley"],"filename":"1f603"},{"aliases":["grinning"],"filename":"1f600"},{"aliases":["blush"],"filename":"1f60a"},{"aliases":["relaxed"],"filename":"263a"},{"aliases":["wink"],"filename":"1f609"},{"aliases":["heart_eyes"],"filename":"1f60d"},{"aliases":["kissing_heart"],"filename":"1f618"},{"aliases":["kissing_closed_eyes"],"filename":"1f61a"},{"aliases":["kissing"],"filename":"1f617"},{"aliases":["kissing_smiling_eyes"],"filename":"1f619"},{"aliases":["stuck_out_tongue_winking_eye"],"filename":"1f61c"},{"aliases":["stuck_out_tongue_closed_eyes"],"filename":"1f61d"},{"aliases":["stuck_out_tongue"],"filename":"1f61b"},{"aliases":["flushed"],"filename":"1f633"},{"aliases":["grin"],"filename":"1f601"},{"aliases":["pensive"],"filename":"1f614"},{"aliases":["relieved"],"filename":"1f60c"},{"aliases":["unamused"],"filename":"1f612"},{"aliases":["disappointed"],"filename":"1f61e"},{"aliases":["persevere"],"filename":"1f623"},{"aliases":["cry"],"filename":"1f622"},{"aliases":["joy"],"filename":"1f602"},{"aliases":["sob"],"filename":"1f62d"},{"aliases":["sleepy"],"filename":"1f62a"},{"aliases":["disappointed_relieved"],"filename":"1f625"},{"aliases":["cold_sweat"],"filename":"1f630"},{"aliases":["sweat_smile"],"filename":"1f605"},{"aliases":["sweat"],"filename":"1f613"},{"aliases":["weary"],"filename":"1f629"},{"aliases":["tired_face"],"filename":"1f62b"},{"aliases":["fearful"],"filename":"1f628"},{"aliases":["scream"],"filename":"1f631"},{"aliases":["angry"],"filename":"1f620"},{"aliases":["rage","pout"],"filename":"1f621"},{"aliases":["triumph"],"filename":"1f624"},{"aliases":["confounded"],"filename":"1f616"},{"aliases":["laughing","satisfied"],"filename":"1f606"},{"aliases":["yum"],"filename":"1f60b"},{"aliases":["mask"],"filename":"1f637"},{"aliases":["sunglasses"],"filename":"1f60e"},{"aliases":["sleeping"],"filename":"1f634"},{"aliases":["dizzy_face"],"filename":"1f635"},{"aliases":["astonished"],"filename":"1f632"},{"aliases":["worried"],"filename":"1f61f"},{"aliases":["frowning"],"filename":"1f626"},{"aliases":["anguished"],"filename":"1f627"},{"aliases":["smiling_imp"],"filename":"1f608"},{"aliases":["imp"],"filename":"1f47f"},{"aliases":["open_mouth"],"filename":"1f62e"},{"aliases":["grimacing"],"filename":"1f62c"},{"aliases":["neutral_face"],"filename":"1f610"},{"aliases":["confused"],"filename":"1f615"},{"aliases":["hushed"],"filename":"1f62f"},{"aliases":["no_mouth"],"filename":"1f636"},{"aliases":["innocent"],"filename":"1f607"},{"aliases":["smirk"],"filename":"1f60f"},{"aliases":["expressionless"],"filename":"1f611"},{"aliases":["man_with_gua_pi_mao"],"filename":"1f472"},{"aliases":["man_with_turban"],"filename":"1f473"},{"aliases":["cop"],"filename":"1f46e"},{"aliases":["construction_worker"],"filename":"1f477"},{"aliases":["guardsman"],"filename":"1f482"},{"aliases":["baby"],"filename":"1f476"},{"aliases":["boy"],"filename":"1f466"},{"aliases":["girl"],"filename":"1f467"},{"aliases":["man"],"filename":"1f468"},{"aliases":["woman"],"filename":"1f469"},{"aliases":["older_man"],"filename":"1f474"},{"aliases":["older_woman"],"filename":"1f475"},{"aliases":["person_with_blond_hair"],"filename":"1f471"},{"aliases":["angel"],"filename":"1f47c"},{"aliases":["princess"],"filename":"1f478"},{"aliases":["smiley_cat"],"filename":"1f63a"},{"aliases":["smile_cat"],"filename":"1f638"},{"aliases":["heart_eyes_cat"],"filename":"1f63b"},{"aliases":["kissing_cat"],"filename":"1f63d"},{"aliases":["smirk_cat"],"filename":"1f63c"},{"aliases":["scream_cat"],"filename":"1f640"},{"aliases":["crying_cat_face"],"filename":"1f63f"},{"aliases":["joy_cat"],"filename":"1f639"},{"aliases":["pouting_cat"],"filename":"1f63e"},{"aliases":["japanese_ogre"],"filename":"1f479"},{"aliases":["japanese_goblin"],"filename":"1f47a"},{"aliases":["see_no_evil"],"filename":"1f648"},{"aliases":["hear_no_evil"],"filename":"1f649"},{"aliases":["speak_no_evil"],"filename":"1f64a"},{"aliases":["skull"],"filename":"1f480"},{"aliases":["alien"],"filename":"1f47d"},{"aliases":["hankey","poop","shit"],"filename":"1f4a9"},{"aliases":["fire"],"filename":"1f525"},{"aliases":["sparkles"],"filename":"2728"},{"aliases":["star2"],"filename":"1f31f"},{"aliases":["dizzy"],"filename":"1f4ab"},{"aliases":["boom","collision"],"filename":"1f4a5"},{"aliases":["anger"],"filename":"1f4a2"},{"aliases":["sweat_drops"],"filename":"1f4a6"},{"aliases":["droplet"],"filename":"1f4a7"},{"aliases":["zzz"],"filename":"1f4a4"},{"aliases":["dash"],"filename":"1f4a8"},{"aliases":["ear"],"filename":"1f442"},{"aliases":["eyes"],"filename":"1f440"},{"aliases":["nose"],"filename":"1f443"},{"aliases":["tongue"],"filename":"1f445"},{"aliases":["lips"],"filename":"1f444"},{"aliases":["+1","thumbsup"],"filename":"1f44d"},{"aliases":["-1","thumbsdown"],"filename":"1f44e"},{"aliases":["ok_hand"],"filename":"1f44c"},{"aliases":["facepunch","punch"],"filename":"1f44a"},{"aliases":["fist"],"filename":"270a"},{"aliases":["v"],"filename":"270c"},{"aliases":["wave"],"filename":"1f44b"},{"aliases":["hand","raised_hand"],"filename":"270b"},{"aliases":["open_hands"],"filename":"1f450"},{"aliases":["point_up_2"],"filename":"1f446"},{"aliases":["point_down"],"filename":"1f447"},{"aliases":["point_right"],"filename":"1f449"},{"aliases":["point_left"],"filename":"1f448"},{"aliases":["raised_hands"],"filename":"1f64c"},{"aliases":["pray"],"filename":"1f64f"},{"aliases":["point_up"],"filename":"261d"},{"aliases":["clap"],"filename":"1f44f"},{"aliases":["muscle"],"filename":"1f4aa"},{"aliases":["walking"],"filename":"1f6b6"},{"aliases":["runner","running"],"filename":"1f3c3"},{"aliases":["dancer"],"filename":"1f483"},{"aliases":["couple"],"filename":"1f46b"},{"aliases":["family"],"filename":"1f46a"},{"aliases":["two_men_holding_hands"],"filename":"1f46c"},{"aliases":["two_women_holding_hands"],"filename":"1f46d"},{"aliases":["couplekiss"],"filename":"1f48f"},{"aliases":["couple_with_heart"],"filename":"1f491"},{"aliases":["dancers"],"filename":"1f46f"},{"aliases":["ok_woman"],"filename":"1f646"},{"aliases":["no_good","ng_woman"],"filename":"1f645"},{"aliases":["information_desk_person"],"filename":"1f481"},{"aliases":["raising_hand"],"filename":"1f64b"},{"aliases":["massage"],"filename":"1f486"},{"aliases":["haircut"],"filename":"1f487"},{"aliases":["nail_care"],"filename":"1f485"},{"aliases":["bride_with_veil"],"filename":"1f470"},{"aliases":["person_with_pouting_face"],"filename":"1f64e"},{"aliases":["person_frowning"],"filename":"1f64d"},{"aliases":["bow"],"filename":"1f647"},{"aliases":["tophat"],"filename":"1f3a9"},{"aliases":["crown"],"filename":"1f451"},{"aliases":["womans_hat"],"filename":"1f452"},{"aliases":["athletic_shoe"],"filename":"1f45f"},{"aliases":["mans_shoe","shoe"],"filename":"1f45e"},{"aliases":["sandal"],"filename":"1f461"},{"aliases":["high_heel"],"filename":"1f460"},{"aliases":["boot"],"filename":"1f462"},{"aliases":["shirt","tshirt"],"filename":"1f455"},{"aliases":["necktie"],"filename":"1f454"},{"aliases":["womans_clothes"],"filename":"1f45a"},{"aliases":["dress"],"filename":"1f457"},{"aliases":["running_shirt_with_sash"],"filename":"1f3bd"},{"aliases":["jeans"],"filename":"1f456"},{"aliases":["kimono"],"filename":"1f458"},{"aliases":["bikini"],"filename":"1f459"},{"aliases":["briefcase"],"filename":"1f4bc"},{"aliases":["handbag"],"filename":"1f45c"},{"aliases":["pouch"],"filename":"1f45d"},{"aliases":["purse"],"filename":"1f45b"},{"aliases":["eyeglasses"],"filename":"1f453"},{"aliases":["ribbon"],"filename":"1f380"},{"aliases":["closed_umbrella"],"filename":"1f302"},{"aliases":["lipstick"],"filename":"1f484"},{"aliases":["yellow_heart"],"filename":"1f49b"},{"aliases":["blue_heart"],"filename":"1f499"},{"aliases":["purple_heart"],"filename":"1f49c"},{"aliases":["green_heart"],"filename":"1f49a"},{"aliases":["heart"],"filename":"2764"},{"aliases":["broken_heart"],"filename":"1f494"},{"aliases":["heartpulse"],"filename":"1f497"},{"aliases":["heartbeat"],"filename":"1f493"},{"aliases":["two_hearts"],"filename":"1f495"},{"aliases":["sparkling_heart"],"filename":"1f496"},{"aliases":["revolving_hearts"],"filename":"1f49e"},{"aliases":["cupid"],"filename":"1f498"},{"aliases":["love_letter"],"filename":"1f48c"},{"aliases":["kiss"],"filename":"1f48b"},{"aliases":["ring"],"filename":"1f48d"},{"aliases":["gem"],"filename":"1f48e"},{"aliases":["bust_in_silhouette"],"filename":"1f464"},{"aliases":["busts_in_silhouette"],"filename":"1f465"},{"aliases":["speech_balloon"],"filename":"1f4ac"},{"aliases":["footprints"],"filename":"1f463"},{"aliases":["thought_balloon"],"filename":"1f4ad"},{"aliases":["dog"],"filename":"1f436"},{"aliases":["wolf"],"filename":"1f43a"},{"aliases":["cat"],"filename":"1f431"},{"aliases":["mouse"],"filename":"1f42d"},{"aliases":["hamster"],"filename":"1f439"},{"aliases":["rabbit"],"filename":"1f430"},{"aliases":["frog"],"filename":"1f438"},{"aliases":["tiger"],"filename":"1f42f"},{"aliases":["koala"],"filename":"1f428"},{"aliases":["bear"],"filename":"1f43b"},{"aliases":["pig"],"filename":"1f437"},{"aliases":["pig_nose"],"filename":"1f43d"},{"aliases":["cow"],"filename":"1f42e"},{"aliases":["boar"],"filename":"1f417"},{"aliases":["monkey_face"],"filename":"1f435"},{"aliases":["monkey"],"filename":"1f412"},{"aliases":["horse"],"filename":"1f434"},{"aliases":["sheep"],"filename":"1f411"},{"aliases":["elephant"],"filename":"1f418"},{"aliases":["panda_face"],"filename":"1f43c"},{"aliases":["penguin"],"filename":"1f427"},{"aliases":["bird"],"filename":"1f426"},{"aliases":["baby_chick"],"filename":"1f424"},{"aliases":["hatched_chick"],"filename":"1f425"},{"aliases":["hatching_chick"],"filename":"1f423"},{"aliases":["chicken"],"filename":"1f414"},{"aliases":["snake"],"filename":"1f40d"},{"aliases":["turtle"],"filename":"1f422"},{"aliases":["bug"],"filename":"1f41b"},{"aliases":["bee","honeybee"],"filename":"1f41d"},{"aliases":["ant"],"filename":"1f41c"},{"aliases":["beetle"],"filename":"1f41e"},{"aliases":["snail"],"filename":"1f40c"},{"aliases":["octopus"],"filename":"1f419"},{"aliases":["shell"],"filename":"1f41a"},{"aliases":["tropical_fish"],"filename":"1f420"},{"aliases":["fish"],"filename":"1f41f"},{"aliases":["dolphin","flipper"],"filename":"1f42c"},{"aliases":["whale"],"filename":"1f433"},{"aliases":["whale2"],"filename":"1f40b"},{"aliases":["cow2"],"filename":"1f404"},{"aliases":["ram"],"filename":"1f40f"},{"aliases":["rat"],"filename":"1f400"},{"aliases":["water_buffalo"],"filename":"1f403"},{"aliases":["tiger2"],"filename":"1f405"},{"aliases":["rabbit2"],"filename":"1f407"},{"aliases":["dragon"],"filename":"1f409"},{"aliases":["racehorse"],"filename":"1f40e"},{"aliases":["goat"],"filename":"1f410"},{"aliases":["rooster"],"filename":"1f413"},{"aliases":["dog2"],"filename":"1f415"},{"aliases":["pig2"],"filename":"1f416"},{"aliases":["mouse2"],"filename":"1f401"},{"aliases":["ox"],"filename":"1f402"},{"aliases":["dragon_face"],"filename":"1f432"},{"aliases":["blowfish"],"filename":"1f421"},{"aliases":["crocodile"],"filename":"1f40a"},{"aliases":["camel"],"filename":"1f42b"},{"aliases":["dromedary_camel"],"filename":"1f42a"},{"aliases":["leopard"],"filename":"1f406"},{"aliases":["cat2"],"filename":"1f408"},{"aliases":["poodle"],"filename":"1f429"},{"aliases":["feet","paw_prints"],"filename":"1f43e"},{"aliases":["bouquet"],"filename":"1f490"},{"aliases":["cherry_blossom"],"filename":"1f338"},{"aliases":["tulip"],"filename":"1f337"},{"aliases":["four_leaf_clover"],"filename":"1f340"},{"aliases":["rose"],"filename":"1f339"},{"aliases":["sunflower"],"filename":"1f33b"},{"aliases":["hibiscus"],"filename":"1f33a"},{"aliases":["maple_leaf"],"filename":"1f341"},{"aliases":["leaves"],"filename":"1f343"},{"aliases":["fallen_leaf"],"filename":"1f342"},{"aliases":["herb"],"filename":"1f33f"},{"aliases":["ear_of_rice"],"filename":"1f33e"},{"aliases":["mushroom"],"filename":"1f344"},{"aliases":["cactus"],"filename":"1f335"},{"aliases":["palm_tree"],"filename":"1f334"},{"aliases":["evergreen_tree"],"filename":"1f332"},{"aliases":["deciduous_tree"],"filename":"1f333"},{"aliases":["chestnut"],"filename":"1f330"},{"aliases":["seedling"],"filename":"1f331"},{"aliases":["blossom"],"filename":"1f33c"},{"aliases":["globe_with_meridians"],"filename":"1f310"},{"aliases":["sun_with_face"],"filename":"1f31e"},{"aliases":["full_moon_with_face"],"filename":"1f31d"},{"aliases":["new_moon_with_face"],"filename":"1f31a"},{"aliases":["new_moon"],"filename":"1f311"},{"aliases":["waxing_crescent_moon"],"filename":"1f312"},{"aliases":["first_quarter_moon"],"filename":"1f313"},{"aliases":["moon","waxing_gibbous_moon"],"filename":"1f314"},{"aliases":["full_moon"],"filename":"1f315"},{"aliases":["waning_gibbous_moon"],"filename":"1f316"},{"aliases":["last_quarter_moon"],"filename":"1f317"},{"aliases":["waning_crescent_moon"],"filename":"1f318"},{"aliases":["last_quarter_moon_with_face"],"filename":"1f31c"},{"aliases":["first_quarter_moon_with_face"],"filename":"1f31b"},{"aliases":["crescent_moon"],"filename":"1f319"},{"aliases":["earth_africa"],"filename":"1f30d"},{"aliases":["earth_americas"],"filename":"1f30e"},{"aliases":["earth_asia"],"filename":"1f30f"},{"aliases":["volcano"],"filename":"1f30b"},{"aliases":["milky_way"],"filename":"1f30c"},{"aliases":["stars"],"filename":"1f320"},{"aliases":["star"],"filename":"2b50"},{"aliases":["sunny"],"filename":"2600"},{"aliases":["partly_sunny"],"filename":"26c5"},{"aliases":["cloud"],"filename":"2601"},{"aliases":["zap"],"filename":"26a1"},{"aliases":["umbrella"],"filename":"2614"},{"aliases":["snowflake"],"filename":"2744"},{"aliases":["snowman"],"filename":"26c4"},{"aliases":["cyclone"],"filename":"1f300"},{"aliases":["foggy"],"filename":"1f301"},{"aliases":["rainbow"],"filename":"1f308"},{"aliases":["ocean"],"filename":"1f30a"},{"aliases":["bamboo"],"filename":"1f38d"},{"aliases":["gift_heart"],"filename":"1f49d"},{"aliases":["dolls"],"filename":"1f38e"},{"aliases":["school_satchel"],"filename":"1f392"},{"aliases":["mortar_board"],"filename":"1f393"},{"aliases":["flags"],"filename":"1f38f"},{"aliases":["fireworks"],"filename":"1f386"},{"aliases":["sparkler"],"filename":"1f387"},{"aliases":["wind_chime"],"filename":"1f390"},{"aliases":["rice_scene"],"filename":"1f391"},{"aliases":["jack_o_lantern"],"filename":"1f383"},{"aliases":["ghost"],"filename":"1f47b"},{"aliases":["santa"],"filename":"1f385"},{"aliases":["christmas_tree"],"filename":"1f384"},{"aliases":["gift"],"filename":"1f381"},{"aliases":["tanabata_tree"],"filename":"1f38b"},{"aliases":["tada"],"filename":"1f389"},{"aliases":["confetti_ball"],"filename":"1f38a"},{"aliases":["balloon"],"filename":"1f388"},{"aliases":["crossed_flags"],"filename":"1f38c"},{"aliases":["crystal_ball"],"filename":"1f52e"},{"aliases":["movie_camera"],"filename":"1f3a5"},{"aliases":["camera"],"filename":"1f4f7"},{"aliases":["video_camera"],"filename":"1f4f9"},{"aliases":["vhs"],"filename":"1f4fc"},{"aliases":["cd"],"filename":"1f4bf"},{"aliases":["dvd"],"filename":"1f4c0"},{"aliases":["minidisc"],"filename":"1f4bd"},{"aliases":["floppy_disk"],"filename":"1f4be"},{"aliases":["computer"],"filename":"1f4bb"},{"aliases":["iphone"],"filename":"1f4f1"},{"aliases":["phone","telephone"],"filename":"260e"},{"aliases":["telephone_receiver"],"filename":"1f4de"},{"aliases":["pager"],"filename":"1f4df"},{"aliases":["fax"],"filename":"1f4e0"},{"aliases":["satellite"],"filename":"1f4e1"},{"aliases":["tv"],"filename":"1f4fa"},{"aliases":["radio"],"filename":"1f4fb"},{"aliases":["loud_sound"],"filename":"1f50a"},{"aliases":["sound"],"filename":"1f509"},{"aliases":["speaker"],"filename":"1f508"},{"aliases":["mute"],"filename":"1f507"},{"aliases":["bell"],"filename":"1f514"},{"aliases":["no_bell"],"filename":"1f515"},{"aliases":["loudspeaker"],"filename":"1f4e2"},{"aliases":["mega"],"filename":"1f4e3"},{"aliases":["hourglass_flowing_sand"],"filename":"23f3"},{"aliases":["hourglass"],"filename":"231b"},{"aliases":["alarm_clock"],"filename":"23f0"},{"aliases":["watch"],"filename":"231a"},{"aliases":["unlock"],"filename":"1f513"},{"aliases":["lock"],"filename":"1f512"},{"aliases":["lock_with_ink_pen"],"filename":"1f50f"},{"aliases":["closed_lock_with_key"],"filename":"1f510"},{"aliases":["key"],"filename":"1f511"},{"aliases":["mag_right"],"filename":"1f50e"},{"aliases":["bulb"],"filename":"1f4a1"},{"aliases":["flashlight"],"filename":"1f526"},{"aliases":["high_brightness"],"filename":"1f506"},{"aliases":["low_brightness"],"filename":"1f505"},{"aliases":["electric_plug"],"filename":"1f50c"},{"aliases":["battery"],"filename":"1f50b"},{"aliases":["mag"],"filename":"1f50d"},{"aliases":["bathtub"],"filename":"1f6c1"},{"aliases":["bath"],"filename":"1f6c0"},{"aliases":["shower"],"filename":"1f6bf"},{"aliases":["toilet"],"filename":"1f6bd"},{"aliases":["wrench"],"filename":"1f527"},{"aliases":["nut_and_bolt"],"filename":"1f529"},{"aliases":["hammer"],"filename":"1f528"},{"aliases":["door"],"filename":"1f6aa"},{"aliases":["smoking"],"filename":"1f6ac"},{"aliases":["bomb"],"filename":"1f4a3"},{"aliases":["gun"],"filename":"1f52b"},{"aliases":["hocho","knife"],"filename":"1f52a"},{"aliases":["pill"],"filename":"1f48a"},{"aliases":["syringe"],"filename":"1f489"},{"aliases":["moneybag"],"filename":"1f4b0"},{"aliases":["yen"],"filename":"1f4b4"},{"aliases":["dollar"],"filename":"1f4b5"},{"aliases":["pound"],"filename":"1f4b7"},{"aliases":["euro"],"filename":"1f4b6"},{"aliases":["credit_card"],"filename":"1f4b3"},{"aliases":["money_with_wings"],"filename":"1f4b8"},{"aliases":["calling"],"filename":"1f4f2"},{"aliases":["e-mail"],"filename":"1f4e7"},{"aliases":["inbox_tray"],"filename":"1f4e5"},{"aliases":["outbox_tray"],"filename":"1f4e4"},{"aliases":["email","envelope"],"filename":"2709"},{"aliases":["envelope_with_arrow"],"filename":"1f4e9"},{"aliases":["incoming_envelope"],"filename":"1f4e8"},{"aliases":["postal_horn"],"filename":"1f4ef"},{"aliases":["mailbox"],"filename":"1f4eb"},{"aliases":["mailbox_closed"],"filename":"1f4ea"},{"aliases":["mailbox_with_mail"],"filename":"1f4ec"},{"aliases":["mailbox_with_no_mail"],"filename":"1f4ed"},{"aliases":["postbox"],"filename":"1f4ee"},{"aliases":["package"],"filename":"1f4e6"},{"aliases":["memo","pencil"],"filename":"1f4dd"},{"aliases":["page_facing_up"],"filename":"1f4c4"},{"aliases":["page_with_curl"],"filename":"1f4c3"},{"aliases":["bookmark_tabs"],"filename":"1f4d1"},{"aliases":["bar_chart"],"filename":"1f4ca"},{"aliases":["chart_with_upwards_trend"],"filename":"1f4c8"},{"aliases":["chart_with_downwards_trend"],"filename":"1f4c9"},{"aliases":["scroll"],"filename":"1f4dc"},{"aliases":["clipboard"],"filename":"1f4cb"},{"aliases":["date"],"filename":"1f4c5"},{"aliases":["calendar"],"filename":"1f4c6"},{"aliases":["card_index"],"filename":"1f4c7"},{"aliases":["file_folder"],"filename":"1f4c1"},{"aliases":["open_file_folder"],"filename":"1f4c2"},{"aliases":["scissors"],"filename":"2702"},{"aliases":["pushpin"],"filename":"1f4cc"},{"aliases":["paperclip"],"filename":"1f4ce"},{"aliases":["black_nib"],"filename":"2712"},{"aliases":["pencil2"],"filename":"270f"},{"aliases":["straight_ruler"],"filename":"1f4cf"},{"aliases":["triangular_ruler"],"filename":"1f4d0"},{"aliases":["closed_book"],"filename":"1f4d5"},{"aliases":["green_book"],"filename":"1f4d7"},{"aliases":["blue_book"],"filename":"1f4d8"},{"aliases":["orange_book"],"filename":"1f4d9"},{"aliases":["notebook"],"filename":"1f4d3"},{"aliases":["notebook_with_decorative_cover"],"filename":"1f4d4"},{"aliases":["ledger"],"filename":"1f4d2"},{"aliases":["books"],"filename":"1f4da"},{"aliases":["book","open_book"],"filename":"1f4d6"},{"aliases":["bookmark"],"filename":"1f516"},{"aliases":["name_badge"],"filename":"1f4db"},{"aliases":["microscope"],"filename":"1f52c"},{"aliases":["telescope"],"filename":"1f52d"},{"aliases":["newspaper"],"filename":"1f4f0"},{"aliases":["art"],"filename":"1f3a8"},{"aliases":["clapper"],"filename":"1f3ac"},{"aliases":["microphone"],"filename":"1f3a4"},{"aliases":["headphones"],"filename":"1f3a7"},{"aliases":["musical_score"],"filename":"1f3bc"},{"aliases":["musical_note"],"filename":"1f3b5"},{"aliases":["notes"],"filename":"1f3b6"},{"aliases":["musical_keyboard"],"filename":"1f3b9"},{"aliases":["violin"],"filename":"1f3bb"},{"aliases":["trumpet"],"filename":"1f3ba"},{"aliases":["saxophone"],"filename":"1f3b7"},{"aliases":["guitar"],"filename":"1f3b8"},{"aliases":["space_invader"],"filename":"1f47e"},{"aliases":["video_game"],"filename":"1f3ae"},{"aliases":["black_joker"],"filename":"1f0cf"},{"aliases":["flower_playing_cards"],"filename":"1f3b4"},{"aliases":["mahjong"],"filename":"1f004"},{"aliases":["game_die"],"filename":"1f3b2"},{"aliases":["dart"],"filename":"1f3af"},{"aliases":["football"],"filename":"1f3c8"},{"aliases":["basketball"],"filename":"1f3c0"},{"aliases":["soccer"],"filename":"26bd"},{"aliases":["baseball"],"filename":"26be"},{"aliases":["tennis"],"filename":"1f3be"},{"aliases":["8ball"],"filename":"1f3b1"},{"aliases":["rugby_football"],"filename":"1f3c9"},{"aliases":["bowling"],"filename":"1f3b3"},{"aliases":["golf"],"filename":"26f3"},{"aliases":["mountain_bicyclist"],"filename":"1f6b5"},{"aliases":["bicyclist"],"filename":"1f6b4"},{"aliases":["checkered_flag"],"filename":"1f3c1"},{"aliases":["horse_racing"],"filename":"1f3c7"},{"aliases":["trophy"],"filename":"1f3c6"},{"aliases":["ski"],"filename":"1f3bf"},{"aliases":["snowboarder"],"filename":"1f3c2"},{"aliases":["swimmer"],"filename":"1f3ca"},{"aliases":["surfer"],"filename":"1f3c4"},{"aliases":["fishing_pole_and_fish"],"filename":"1f3a3"},{"aliases":["coffee"],"filename":"2615"},{"aliases":["tea"],"filename":"1f375"},{"aliases":["sake"],"filename":"1f376"},{"aliases":["baby_bottle"],"filename":"1f37c"},{"aliases":["beer"],"filename":"1f37a"},{"aliases":["beers"],"filename":"1f37b"},{"aliases":["cocktail"],"filename":"1f378"},{"aliases":["tropical_drink"],"filename":"1f379"},{"aliases":["wine_glass"],"filename":"1f377"},{"aliases":["fork_and_knife"],"filename":"1f374"},{"aliases":["pizza"],"filename":"1f355"},{"aliases":["hamburger"],"filename":"1f354"},{"aliases":["fries"],"filename":"1f35f"},{"aliases":["poultry_leg"],"filename":"1f357"},{"aliases":["meat_on_bone"],"filename":"1f356"},{"aliases":["spaghetti"],"filename":"1f35d"},{"aliases":["curry"],"filename":"1f35b"},{"aliases":["fried_shrimp"],"filename":"1f364"},{"aliases":["bento"],"filename":"1f371"},{"aliases":["sushi"],"filename":"1f363"},{"aliases":["fish_cake"],"filename":"1f365"},{"aliases":["rice_ball"],"filename":"1f359"},{"aliases":["rice_cracker"],"filename":"1f358"},{"aliases":["rice"],"filename":"1f35a"},{"aliases":["ramen"],"filename":"1f35c"},{"aliases":["stew"],"filename":"1f372"},{"aliases":["oden"],"filename":"1f362"},{"aliases":["dango"],"filename":"1f361"},{"aliases":["egg"],"filename":"1f373"},{"aliases":["bread"],"filename":"1f35e"},{"aliases":["doughnut"],"filename":"1f369"},{"aliases":["custard"],"filename":"1f36e"},{"aliases":["icecream"],"filename":"1f366"},{"aliases":["ice_cream"],"filename":"1f368"},{"aliases":["shaved_ice"],"filename":"1f367"},{"aliases":["birthday"],"filename":"1f382"},{"aliases":["cake"],"filename":"1f370"},{"aliases":["cookie"],"filename":"1f36a"},{"aliases":["chocolate_bar"],"filename":"1f36b"},{"aliases":["candy"],"filename":"1f36c"},{"aliases":["lollipop"],"filename":"1f36d"},{"aliases":["honey_pot"],"filename":"1f36f"},{"aliases":["apple"],"filename":"1f34e"},{"aliases":["green_apple"],"filename":"1f34f"},{"aliases":["tangerine","orange","mandarin"],"filename":"1f34a"},{"aliases":["lemon"],"filename":"1f34b"},{"aliases":["cherries"],"filename":"1f352"},{"aliases":["grapes"],"filename":"1f347"},{"aliases":["watermelon"],"filename":"1f349"},{"aliases":["strawberry"],"filename":"1f353"},{"aliases":["peach"],"filename":"1f351"},{"aliases":["melon"],"filename":"1f348"},{"aliases":["banana"],"filename":"1f34c"},{"aliases":["pear"],"filename":"1f350"},{"aliases":["pineapple"],"filename":"1f34d"},{"aliases":["sweet_potato"],"filename":"1f360"},{"aliases":["eggplant"],"filename":"1f346"},{"aliases":["tomato"],"filename":"1f345"},{"aliases":["corn"],"filename":"1f33d"},{"aliases":["house"],"filename":"1f3e0"},{"aliases":["house_with_garden"],"filename":"1f3e1"},{"aliases":["school"],"filename":"1f3eb"},{"aliases":["office"],"filename":"1f3e2"},{"aliases":["post_office"],"filename":"1f3e3"},{"aliases":["hospital"],"filename":"1f3e5"},{"aliases":["bank"],"filename":"1f3e6"},{"aliases":["convenience_store"],"filename":"1f3ea"},{"aliases":["love_hotel"],"filename":"1f3e9"},{"aliases":["hotel"],"filename":"1f3e8"},{"aliases":["wedding"],"filename":"1f492"},{"aliases":["church"],"filename":"26ea"},{"aliases":["department_store"],"filename":"1f3ec"},{"aliases":["european_post_office"],"filename":"1f3e4"},{"aliases":["city_sunrise"],"filename":"1f307"},{"aliases":["city_sunset"],"filename":"1f306"},{"aliases":["japanese_castle"],"filename":"1f3ef"},{"aliases":["european_castle"],"filename":"1f3f0"},{"aliases":["tent"],"filename":"26fa"},{"aliases":["factory"],"filename":"1f3ed"},{"aliases":["tokyo_tower"],"filename":"1f5fc"},{"aliases":["japan"],"filename":"1f5fe"},{"aliases":["mount_fuji"],"filename":"1f5fb"},{"aliases":["sunrise_over_mountains"],"filename":"1f304"},{"aliases":["sunrise"],"filename":"1f305"},{"aliases":["night_with_stars"],"filename":"1f303"},{"aliases":["statue_of_liberty"],"filename":"1f5fd"},{"aliases":["bridge_at_night"],"filename":"1f309"},{"aliases":["carousel_horse"],"filename":"1f3a0"},{"aliases":["ferris_wheel"],"filename":"1f3a1"},{"aliases":["fountain"],"filename":"26f2"},{"aliases":["roller_coaster"],"filename":"1f3a2"},{"aliases":["ship"],"filename":"1f6a2"},{"aliases":["boat","sailboat"],"filename":"26f5"},{"aliases":["speedboat"],"filename":"1f6a4"},{"aliases":["rowboat"],"filename":"1f6a3"},{"aliases":["anchor"],"filename":"2693"},{"aliases":["rocket"],"filename":"1f680"},{"aliases":["airplane"],"filename":"2708"},{"aliases":["seat"],"filename":"1f4ba"},{"aliases":["helicopter"],"filename":"1f681"},{"aliases":["steam_locomotive"],"filename":"1f682"},{"aliases":["tram"],"filename":"1f68a"},{"aliases":["station"],"filename":"1f689"},{"aliases":["mountain_railway"],"filename":"1f69e"},{"aliases":["train2"],"filename":"1f686"},{"aliases":["bullettrain_side"],"filename":"1f684"},{"aliases":["bullettrain_front"],"filename":"1f685"},{"aliases":["light_rail"],"filename":"1f688"},{"aliases":["metro"],"filename":"1f687"},{"aliases":["monorail"],"filename":"1f69d"},{"aliases":["train"],"filename":"1f68b"},{"aliases":["railway_car"],"filename":"1f683"},{"aliases":["trolleybus"],"filename":"1f68e"},{"aliases":["bus"],"filename":"1f68c"},{"aliases":["oncoming_bus"],"filename":"1f68d"},{"aliases":["blue_car"],"filename":"1f699"},{"aliases":["oncoming_automobile"],"filename":"1f698"},{"aliases":["car","red_car"],"filename":"1f697"},{"aliases":["taxi"],"filename":"1f695"},{"aliases":["oncoming_taxi"],"filename":"1f696"},{"aliases":["articulated_lorry"],"filename":"1f69b"},{"aliases":["truck"],"filename":"1f69a"},{"aliases":["rotating_light"],"filename":"1f6a8"},{"aliases":["police_car"],"filename":"1f693"},{"aliases":["oncoming_police_car"],"filename":"1f694"},{"aliases":["fire_engine"],"filename":"1f692"},{"aliases":["ambulance"],"filename":"1f691"},{"aliases":["minibus"],"filename":"1f690"},{"aliases":["bike"],"filename":"1f6b2"},{"aliases":["aerial_tramway"],"filename":"1f6a1"},{"aliases":["suspension_railway"],"filename":"1f69f"},{"aliases":["mountain_cableway"],"filename":"1f6a0"},{"aliases":["tractor"],"filename":"1f69c"},{"aliases":["barber"],"filename":"1f488"},{"aliases":["busstop"],"filename":"1f68f"},{"aliases":["ticket"],"filename":"1f3ab"},{"aliases":["vertical_traffic_light"],"filename":"1f6a6"},{"aliases":["traffic_light"],"filename":"1f6a5"},{"aliases":["warning"],"filename":"26a0"},{"aliases":["construction"],"filename":"1f6a7"},{"aliases":["beginner"],"filename":"1f530"},{"aliases":["fuelpump"],"filename":"26fd"},{"aliases":["izakaya_lantern","lantern"],"filename":"1f3ee"},{"aliases":["slot_machine"],"filename":"1f3b0"},{"aliases":["hotsprings"],"filename":"2668"},{"aliases":["moyai"],"filename":"1f5ff"},{"aliases":["circus_tent"],"filename":"1f3aa"},{"aliases":["performing_arts"],"filename":"1f3ad"},{"aliases":["round_pushpin"],"filename":"1f4cd"},{"aliases":["triangular_flag_on_post"],"filename":"1f6a9"},{"aliases":["jp"],"filename":"1f1ef-1f1f5"},{"aliases":["kr"],"filename":"1f1f0-1f1f7"},{"aliases":["de"],"filename":"1f1e9-1f1ea"},{"aliases":["cn"],"filename":"1f1e8-1f1f3"},{"aliases":["us"],"filename":"1f1fa-1f1f8"},{"aliases":["fr"],"filename":"1f1eb-1f1f7"},{"aliases":["es"],"filename":"1f1ea-1f1f8"},{"aliases":["it"],"filename":"1f1ee-1f1f9"},{"aliases":["ru"],"filename":"1f1f7-1f1fa"},{"aliases":["gb","uk"],"filename":"1f1ec-1f1e7"},{"aliases":["one"],"filename":"0031-20e3"},{"aliases":["two"],"filename":"0032-20e3"},{"aliases":["three"],"filename":"0033-20e3"},{"aliases":["four"],"filename":"0034-20e3"},{"aliases":["five"],"filename":"0035-20e3"},{"aliases":["six"],"filename":"0036-20e3"},{"aliases":["seven"],"filename":"0037-20e3"},{"aliases":["eight"],"filename":"0038-20e3"},{"aliases":["nine"],"filename":"0039-20e3"},{"aliases":["zero"],"filename":"0030-20e3"},{"aliases":["keycap_ten"],"filename":"1f51f"},{"aliases":["1234"],"filename":"1f522"},{"aliases":["hash"],"filename":"0023-20e3"},{"aliases":["symbols"],"filename":"1f523"},{"aliases":["arrow_up"],"filename":"2b06"},{"aliases":["arrow_down"],"filename":"2b07"},{"aliases":["arrow_left"],"filename":"2b05"},{"aliases":["arrow_right"],"filename":"27a1"},{"aliases":["capital_abcd"],"filename":"1f520"},{"aliases":["abcd"],"filename":"1f521"},{"aliases":["abc"],"filename":"1f524"},{"aliases":["arrow_upper_right"],"filename":"2197"},{"aliases":["arrow_upper_left"],"filename":"2196"},{"aliases":["arrow_lower_right"],"filename":"2198"},{"aliases":["arrow_lower_left"],"filename":"2199"},{"aliases":["left_right_arrow"],"filename":"2194"},{"aliases":["arrow_up_down"],"filename":"2195"},{"aliases":["arrows_counterclockwise"],"filename":"1f504"},{"aliases":["arrow_backward"],"filename":"25c0"},{"aliases":["arrow_forward"],"filename":"25b6"},{"aliases":["arrow_up_small"],"filename":"1f53c"},{"aliases":["arrow_down_small"],"filename":"1f53d"},{"aliases":["leftwards_arrow_with_hook"],"filename":"21a9"},{"aliases":["arrow_right_hook"],"filename":"21aa"},{"aliases":["information_source"],"filename":"2139"},{"aliases":["rewind"],"filename":"23ea"},{"aliases":["fast_forward"],"filename":"23e9"},{"aliases":["arrow_double_up"],"filename":"23eb"},{"aliases":["arrow_double_down"],"filename":"23ec"},{"aliases":["arrow_heading_down"],"filename":"2935"},{"aliases":["arrow_heading_up"],"filename":"2934"},{"aliases":["ok"],"filename":"1f197"},{"aliases":["twisted_rightwards_arrows"],"filename":"1f500"},{"aliases":["repeat"],"filename":"1f501"},{"aliases":["repeat_one"],"filename":"1f502"},{"aliases":["new"],"filename":"1f195"},{"aliases":["up"],"filename":"1f199"},{"aliases":["cool"],"filename":"1f192"},{"aliases":["free"],"filename":"1f193"},{"aliases":["ng"],"filename":"1f196"},{"aliases":["signal_strength"],"filename":"1f4f6"},{"aliases":["cinema"],"filename":"1f3a6"},{"aliases":["koko"],"filename":"1f201"},{"aliases":["u6307"],"filename":"1f22f"},{"aliases":["u7a7a"],"filename":"1f233"},{"aliases":["u6e80"],"filename":"1f235"},{"aliases":["u5408"],"filename":"1f234"},{"aliases":["u7981"],"filename":"1f232"},{"aliases":["ideograph_advantage"],"filename":"1f250"},{"aliases":["u5272"],"filename":"1f239"},{"aliases":["u55b6"],"filename":"1f23a"},{"aliases":["u6709"],"filename":"1f236"},{"aliases":["u7121"],"filename":"1f21a"},{"aliases":["restroom"],"filename":"1f6bb"},{"aliases":["mens"],"filename":"1f6b9"},{"aliases":["womens"],"filename":"1f6ba"},{"aliases":["baby_symbol"],"filename":"1f6bc"},{"aliases":["wc"],"filename":"1f6be"},{"aliases":["potable_water"],"filename":"1f6b0"},{"aliases":["put_litter_in_its_place"],"filename":"1f6ae"},{"aliases":["parking"],"filename":"1f17f"},{"aliases":["wheelchair"],"filename":"267f"},{"aliases":["no_smoking"],"filename":"1f6ad"},{"aliases":["u6708"],"filename":"1f237"},{"aliases":["u7533"],"filename":"1f238"},{"aliases":["sa"],"filename":"1f202"},{"aliases":["m"],"filename":"24c2"},{"aliases":["passport_control"],"filename":"1f6c2"},{"aliases":["baggage_claim"],"filename":"1f6c4"},{"aliases":["left_luggage"],"filename":"1f6c5"},{"aliases":["customs"],"filename":"1f6c3"},{"aliases":["accept"],"filename":"1f251"},{"aliases":["secret"],"filename":"3299"},{"aliases":["congratulations"],"filename":"3297"},{"aliases":["cl"],"filename":"1f191"},{"aliases":["sos"],"filename":"1f198"},{"aliases":["id"],"filename":"1f194"},{"aliases":["no_entry_sign"],"filename":"1f6ab"},{"aliases":["underage"],"filename":"1f51e"},{"aliases":["no_mobile_phones"],"filename":"1f4f5"},{"aliases":["do_not_litter"],"filename":"1f6af"},{"aliases":["non-potable_water"],"filename":"1f6b1"},{"aliases":["no_bicycles"],"filename":"1f6b3"},{"aliases":["no_pedestrians"],"filename":"1f6b7"},{"aliases":["children_crossing"],"filename":"1f6b8"},{"aliases":["no_entry"],"filename":"26d4"},{"aliases":["eight_spoked_asterisk"],"filename":"2733"},{"aliases":["sparkle"],"filename":"2747"},{"aliases":["negative_squared_cross_mark"],"filename":"274e"},{"aliases":["white_check_mark"],"filename":"2705"},{"aliases":["eight_pointed_black_star"],"filename":"2734"},{"aliases":["heart_decoration"],"filename":"1f49f"},{"aliases":["vs"],"filename":"1f19a"},{"aliases":["vibration_mode"],"filename":"1f4f3"},{"aliases":["mobile_phone_off"],"filename":"1f4f4"},{"aliases":["a"],"filename":"1f170"},{"aliases":["b"],"filename":"1f171"},{"aliases":["ab"],"filename":"1f18e"},{"aliases":["o2"],"filename":"1f17e"},{"aliases":["diamond_shape_with_a_dot_inside"],"filename":"1f4a0"},{"aliases":["loop"],"filename":"27bf"},{"aliases":["recycle"],"filename":"267b"},{"aliases":["aries"],"filename":"2648"},{"aliases":["taurus"],"filename":"2649"},{"aliases":["gemini"],"filename":"264a"},{"aliases":["cancer"],"filename":"264b"},{"aliases":["leo"],"filename":"264c"},{"aliases":["virgo"],"filename":"264d"},{"aliases":["libra"],"filename":"264e"},{"aliases":["scorpius"],"filename":"264f"},{"aliases":["sagittarius"],"filename":"2650"},{"aliases":["capricorn"],"filename":"2651"},{"aliases":["aquarius"],"filename":"2652"},{"aliases":["pisces"],"filename":"2653"},{"aliases":["ophiuchus"],"filename":"26ce"},{"aliases":["six_pointed_star"],"filename":"1f52f"},{"aliases":["atm"],"filename":"1f3e7"},{"aliases":["chart"],"filename":"1f4b9"},{"aliases":["heavy_dollar_sign"],"filename":"1f4b2"},{"aliases":["currency_exchange"],"filename":"1f4b1"},{"aliases":["copyright"],"filename":"00a9"},{"aliases":["registered"],"filename":"00ae"},{"aliases":["tm"],"filename":"2122"},{"aliases":["x"],"filename":"274c"},{"aliases":["bangbang"],"filename":"203c"},{"aliases":["interrobang"],"filename":"2049"},{"aliases":["exclamation","heavy_exclamation_mark"],"filename":"2757"},{"aliases":["question"],"filename":"2753"},{"aliases":["grey_exclamation"],"filename":"2755"},{"aliases":["grey_question"],"filename":"2754"},{"aliases":["o"],"filename":"2b55"},{"aliases":["top"],"filename":"1f51d"},{"aliases":["end"],"filename":"1f51a"},{"aliases":["back"],"filename":"1f519"},{"aliases":["on"],"filename":"1f51b"},{"aliases":["soon"],"filename":"1f51c"},{"aliases":["arrows_clockwise"],"filename":"1f503"},{"aliases":["clock12"],"filename":"1f55b"},{"aliases":["clock1230"],"filename":"1f567"},{"aliases":["clock1"],"filename":"1f550"},{"aliases":["clock130"],"filename":"1f55c"},{"aliases":["clock2"],"filename":"1f551"},{"aliases":["clock230"],"filename":"1f55d"},{"aliases":["clock3"],"filename":"1f552"},{"aliases":["clock330"],"filename":"1f55e"},{"aliases":["clock4"],"filename":"1f553"},{"aliases":["clock430"],"filename":"1f55f"},{"aliases":["clock5"],"filename":"1f554"},{"aliases":["clock530"],"filename":"1f560"},{"aliases":["clock6"],"filename":"1f555"},{"aliases":["clock7"],"filename":"1f556"},{"aliases":["clock8"],"filename":"1f557"},{"aliases":["clock9"],"filename":"1f558"},{"aliases":["clock10"],"filename":"1f559"},{"aliases":["clock11"],"filename":"1f55a"},{"aliases":["clock630"],"filename":"1f561"},{"aliases":["clock730"],"filename":"1f562"},{"aliases":["clock830"],"filename":"1f563"},{"aliases":["clock930"],"filename":"1f564"},{"aliases":["clock1030"],"filename":"1f565"},{"aliases":["clock1130"],"filename":"1f566"},{"aliases":["heavy_multiplication_x"],"filename":"2716"},{"aliases":["heavy_plus_sign"],"filename":"2795"},{"aliases":["heavy_minus_sign"],"filename":"2796"},{"aliases":["heavy_division_sign"],"filename":"2797"},{"aliases":["spades"],"filename":"2660"},{"aliases":["hearts"],"filename":"2665"},{"aliases":["clubs"],"filename":"2663"},{"aliases":["diamonds"],"filename":"2666"},{"aliases":["white_flower"],"filename":"1f4ae"},{"aliases":["100"],"filename":"1f4af"},{"aliases":["heavy_check_mark"],"filename":"2714"},{"aliases":["ballot_box_with_check"],"filename":"2611"},{"aliases":["radio_button"],"filename":"1f518"},{"aliases":["link"],"filename":"1f517"},{"aliases":["curly_loop"],"filename":"27b0"},{"aliases":["wavy_dash"],"filename":"3030"},{"aliases":["part_alternation_mark"],"filename":"303d"},{"aliases":["trident"],"filename":"1f531"},{"aliases":["black_medium_square"],"filename":"25fc"},{"aliases":["white_medium_square"],"filename":"25fb"},{"aliases":["black_medium_small_square"],"filename":"25fe"},{"aliases":["white_medium_small_square"],"filename":"25fd"},{"aliases":["black_small_square"],"filename":"25aa"},{"aliases":["white_small_square"],"filename":"25ab"},{"aliases":["small_red_triangle"],"filename":"1f53a"},{"aliases":["black_square_button"],"filename":"1f532"},{"aliases":["white_square_button"],"filename":"1f533"},{"aliases":["black_circle"],"filename":"26ab"},{"aliases":["white_circle"],"filename":"26aa"},{"aliases":["red_circle"],"filename":"1f534"},{"aliases":["large_blue_circle"],"filename":"1f535"},{"aliases":["small_red_triangle_down"],"filename":"1f53b"},{"aliases":["white_large_square"],"filename":"2b1c"},{"aliases":["black_large_square"],"filename":"2b1b"},{"aliases":["large_orange_diamond"],"filename":"1f536"},{"aliases":["large_blue_diamond"],"filename":"1f537"},{"aliases":["small_orange_diamond"],"filename":"1f538"},{"aliases":["small_blue_diamond"],"filename":"1f539"},{"aliases":["ca"],"filename":"1f1e8-1f1e6"},{"aliases":["pk"],"filename":"1f1f5-1f1f0"},{"aliases":["za"],"filename":"1f1ff-1f1e6"},{"aliases":["slightly_smiling_face"],"filename":"1f642"},{"aliases":["slightly_frowning_face"],"filename":"1f641"},{"aliases":["upside_down_face"],"filename":"1f643"},{"aliases":["mattermost"],"filename":""},{"aliases":["bowtie"],"filename":""},{"aliases":["feelsgood"],"filename":""},{"aliases":["finnadie"],"filename":""},{"aliases":["fu"],"filename":""},{"aliases":["goberserk"],"filename":""},{"aliases":["godmode"],"filename":""},{"aliases":["hurtrealbad"],"filename":""},{"aliases":["metal"],"filename":""},{"aliases":["neckbeard"],"filename":""},{"aliases":["octocat"],"filename":""},{"aliases":["rage1"],"filename":""},{"aliases":["rage2"],"filename":""},{"aliases":["rage3"],"filename":""},{"aliases":["rage4"],"filename":""},{"aliases":["shipit","squirrel"],"filename":""},{"aliases":["suspect"],"filename":""},{"aliases":["taco"],"filename":""},{"aliases":["trollface"],"filename":""}];
+
+export const EmojiIndicesByAlias = new Map([["+1",105],["-1",106],["100",816],["1234",647],["8ball",462],["a",741],["ab",743],["abc",656],["abcd",655],["accept",717],["aerial_tramway",605],["airplane",573],["alarm_clock",353],["alien",88],["ambulance",602],["anchor",571],["angel",71],["anger",95],["angry",33],["anguished",46],["ant",219],["apple",518],["aquarius",758],["aries",748],["arrow_backward",664],["arrow_double_down",674],["arrow_double_up",673],["arrow_down",651],["arrow_down_small",667],["arrow_forward",665],["arrow_heading_down",675],["arrow_heading_up",676],["arrow_left",652],["arrow_lower_left",660],["arrow_lower_right",659],["arrow_right",653],["arrow_right_hook",669],["arrow_up",650],["arrow_up_down",662],["arrow_up_small",666],["arrow_upper_left",658],["arrow_upper_right",657],["arrows_clockwise",782],["arrows_counterclockwise",663],["art",438],["articulated_lorry",596],["astonished",43],["athletic_shoe",147],["atm",762],["b",742],["baby",63],["baby_bottle",479],["baby_chick",211],["baby_symbol",702],["back",779],["baggage_claim",714],["balloon",323],["ballot_box_with_check",818],["bamboo",305],["banana",528],["bangbang",770],["bank",541],["bar_chart",407],["barber",609],["baseball",460],["basketball",458],["bath",369],["bathtub",368],["battery",366],["bear",198],["bee",218],["beer",480],["beers",481],["beetle",220],["beginner",616],["bell",347],["bento",494],["bicyclist",467],["bike",604],["bikini",159],["bird",210],["birthday",511],["black_circle",834],["black_joker",452],["black_large_square",840],["black_medium_small_square",827],["black_medium_square",825],["black_nib",420],["black_small_square",829],["black_square_button",832],["blossom",271],["blowfish",244],["blue_book",426],["blue_car",591],["blue_heart",169],["blush",3],["boar",202],["boat",568],["bomb",377],["book",432],["bookmark",433],["bookmark_tabs",406],["books",431],["boom",94],["boot",151],["bouquet",252],["bow",143],["bowling",464],["bowtie",852],["boy",64],["bread",505],["bride_with_veil",140],["bridge_at_night",562],["briefcase",160],["broken_heart",173],["bug",217],["bulb",361],["bullettrain_front",582],["bullettrain_side",581],["bus",589],["busstop",610],["bust_in_silhouette",184],["busts_in_silhouette",185],["ca",845],["cactus",265],["cake",512],["calendar",413],["calling",389],["camel",246],["camera",327],["cancer",751],["candy",515],["capital_abcd",654],["capricorn",757],["car",593],["card_index",414],["carousel_horse",563],["cat",191],["cat2",249],["cd",330],["chart",763],["chart_with_downwards_trend",409],["chart_with_upwards_trend",408],["checkered_flag",468],["cherries",522],["cherry_blossom",253],["chestnut",269],["chicken",214],["children_crossing",730],["chocolate_bar",514],["christmas_tree",318],["church",546],["cinema",687],["circus_tent",622],["city_sunrise",549],["city_sunset",550],["cl",720],["clap",121],["clapper",439],["clipboard",411],["clock1",785],["clock10",799],["clock1030",805],["clock11",800],["clock1130",806],["clock12",783],["clock1230",784],["clock130",786],["clock2",787],["clock230",788],["clock3",789],["clock330",790],["clock4",791],["clock430",792],["clock5",793],["clock530",794],["clock6",795],["clock630",801],["clock7",796],["clock730",802],["clock8",797],["clock830",803],["clock9",798],["clock930",804],["closed_book",424],["closed_lock_with_key",358],["closed_umbrella",166],["cloud",296],["clubs",813],["cn",629],["cocktail",482],["coffee",476],["cold_sweat",26],["collision",94],["computer",334],["confetti_ball",322],["confounded",36],["confused",52],["congratulations",719],["construction",615],["construction_worker",61],["convenience_store",542],["cookie",513],["cool",683],["cop",60],["copyright",766],["corn",534],["couple",126],["couple_with_heart",131],["couplekiss",130],["cow",201],["cow2",229],["credit_card",387],["crescent_moon",286],["crocodile",245],["crossed_flags",324],["crown",145],["cry",21],["crying_cat_face",79],["crystal_ball",325],["cupid",179],["curly_loop",821],["currency_exchange",765],["curry",492],["custard",507],["customs",716],["cyclone",301],["dancer",125],["dancers",132],["dango",503],["dart",456],["dash",99],["date",412],["de",628],["deciduous_tree",268],["department_store",547],["diamond_shape_with_a_dot_inside",745],["diamonds",814],["disappointed",19],["disappointed_relieved",25],["dizzy",93],["dizzy_face",42],["do_not_litter",726],["dog",189],["dog2",239],["dollar",384],["dolls",307],["dolphin",226],["door",375],["doughnut",506],["dragon",235],["dragon_face",243],["dress",155],["dromedary_camel",247],["droplet",97],["dvd",331],["e-mail",390],["ear",100],["ear_of_rice",263],["earth_africa",287],["earth_americas",288],["earth_asia",289],["egg",504],["eggplant",532],["eight",643],["eight_pointed_black_star",736],["eight_spoked_asterisk",732],["electric_plug",365],["elephant",207],["email",393],["end",778],["envelope",393],["envelope_with_arrow",394],["es",632],["euro",386],["european_castle",552],["european_post_office",548],["evergreen_tree",267],["exclamation",772],["expressionless",57],["eyeglasses",164],["eyes",101],["facepunch",108],["factory",554],["fallen_leaf",261],["family",127],["fast_forward",672],["fax",339],["fearful",31],["feelsgood",853],["feet",251],["ferris_wheel",564],["file_folder",415],["finnadie",854],["fire",90],["fire_engine",601],["fireworks",311],["first_quarter_moon",278],["first_quarter_moon_with_face",285],["fish",225],["fish_cake",496],["fishing_pole_and_fish",475],["fist",109],["five",640],["flags",310],["flashlight",362],["flipper",226],["floppy_disk",333],["flower_playing_cards",453],["flushed",14],["foggy",302],["football",457],["footprints",187],["fork_and_knife",485],["fountain",565],["four",639],["four_leaf_clover",255],["fr",631],["free",684],["fried_shrimp",493],["fries",488],["frog",195],["frowning",45],["fu",855],["fuelpump",617],["full_moon",280],["full_moon_with_face",274],["game_die",455],["gb",635],["gem",183],["gemini",750],["ghost",316],["gift",319],["gift_heart",306],["girl",65],["globe_with_meridians",272],["goat",237],["goberserk",856],["godmode",857],["golf",465],["grapes",523],["green_apple",519],["green_book",425],["green_heart",171],["grey_exclamation",774],["grey_question",775],["grimacing",50],["grin",15],["grinning",2],["guardsman",62],["guitar",449],["gun",378],["haircut",138],["hamburger",487],["hammer",374],["hamster",193],["hand",112],["handbag",161],["hankey",89],["hash",648],["hatched_chick",212],["hatching_chick",213],["headphones",441],["hear_no_evil",85],["heart",172],["heart_decoration",737],["heart_eyes",6],["heart_eyes_cat",75],["heartbeat",175],["heartpulse",174],["hearts",812],["heavy_check_mark",817],["heavy_division_sign",810],["heavy_dollar_sign",764],["heavy_exclamation_mark",772],["heavy_minus_sign",809],["heavy_multiplication_x",807],["heavy_plus_sign",808],["helicopter",575],["herb",262],["hibiscus",258],["high_brightness",363],["high_heel",150],["hocho",379],["honey_pot",517],["honeybee",218],["horse",205],["horse_racing",469],["hospital",540],["hotel",544],["hotsprings",620],["hourglass",352],["hourglass_flowing_sand",351],["house",535],["house_with_garden",536],["hurtrealbad",858],["hushed",53],["ice_cream",509],["icecream",508],["id",722],["ideograph_advantage",694],["imp",48],["inbox_tray",391],["incoming_envelope",395],["information_desk_person",135],["information_source",670],["innocent",55],["interrobang",771],["iphone",335],["it",633],["izakaya_lantern",618],["jack_o_lantern",315],["japan",556],["japanese_castle",551],["japanese_goblin",83],["japanese_ogre",82],["jeans",157],["joy",22],["joy_cat",80],["jp",626],["key",359],["keycap_ten",646],["kimono",158],["kiss",181],["kissing",9],["kissing_cat",76],["kissing_closed_eyes",8],["kissing_heart",7],["kissing_smiling_eyes",10],["knife",379],["koala",197],["koko",688],["kr",627],["lantern",618],["large_blue_circle",837],["large_blue_diamond",842],["large_orange_diamond",841],["last_quarter_moon",282],["last_quarter_moon_with_face",284],["laughing",37],["leaves",260],["ledger",430],["left_luggage",715],["left_right_arrow",661],["leftwards_arrow_with_hook",668],["lemon",521],["leo",752],["leopard",248],["libra",754],["light_rail",583],["link",820],["lips",104],["lipstick",167],["lock",356],["lock_with_ink_pen",357],["lollipop",516],["loop",746],["loud_sound",343],["loudspeaker",349],["love_hotel",543],["love_letter",180],["low_brightness",364],["m",712],["mag",367],["mag_right",360],["mahjong",454],["mailbox",397],["mailbox_closed",398],["mailbox_with_mail",399],["mailbox_with_no_mail",400],["man",66],["man_with_gua_pi_mao",58],["man_with_turban",59],["mandarin",520],["mans_shoe",148],["maple_leaf",259],["mask",39],["massage",137],["mattermost",851],["meat_on_bone",490],["mega",350],["melon",527],["memo",403],["mens",700],["metal",859],["metro",584],["microphone",440],["microscope",435],["milky_way",291],["minibus",603],["minidisc",332],["mobile_phone_off",740],["money_with_wings",388],["moneybag",382],["monkey",204],["monkey_face",203],["monorail",585],["moon",279],["mortar_board",309],["mount_fuji",557],["mountain_bicyclist",466],["mountain_cableway",607],["mountain_railway",579],["mouse",192],["mouse2",241],["movie_camera",326],["moyai",621],["muscle",122],["mushroom",264],["musical_keyboard",445],["musical_note",443],["musical_score",442],["mute",346],["nail_care",139],["name_badge",434],["neckbeard",860],["necktie",153],["negative_squared_cross_mark",734],["neutral_face",51],["new",681],["new_moon",276],["new_moon_with_face",275],["newspaper",437],["ng",685],["ng_woman",134],["night_with_stars",560],["nine",644],["no_bell",348],["no_bicycles",728],["no_entry",731],["no_entry_sign",723],["no_good",134],["no_mobile_phones",725],["no_mouth",54],["no_pedestrians",729],["no_smoking",708],["non-potable_water",727],["nose",102],["notebook",428],["notebook_with_decorative_cover",429],["notes",444],["nut_and_bolt",373],["o",776],["o2",744],["ocean",304],["octocat",861],["octopus",222],["oden",502],["office",538],["ok",677],["ok_hand",107],["ok_woman",133],["older_man",68],["older_woman",69],["on",780],["oncoming_automobile",592],["oncoming_bus",590],["oncoming_police_car",600],["oncoming_taxi",595],["one",636],["open_book",432],["open_file_folder",416],["open_hands",113],["open_mouth",49],["ophiuchus",760],["orange",520],["orange_book",427],["outbox_tray",392],["ox",242],["package",402],["page_facing_up",404],["page_with_curl",405],["pager",338],["palm_tree",266],["panda_face",208],["paperclip",419],["parking",706],["part_alternation_mark",823],["partly_sunny",295],["passport_control",713],["paw_prints",251],["peach",526],["pear",529],["pencil",403],["pencil2",421],["penguin",209],["pensive",16],["performing_arts",623],["persevere",20],["person_frowning",142],["person_with_blond_hair",70],["person_with_pouting_face",141],["phone",336],["pig",199],["pig2",240],["pig_nose",200],["pill",380],["pineapple",530],["pisces",759],["pizza",486],["pk",846],["point_down",115],["point_left",117],["point_right",116],["point_up",120],["point_up_2",114],["police_car",599],["poodle",250],["poop",89],["post_office",539],["postal_horn",396],["postbox",401],["potable_water",704],["pouch",162],["poultry_leg",489],["pound",385],["pout",34],["pouting_cat",81],["pray",119],["princess",72],["punch",108],["purple_heart",170],["purse",163],["pushpin",418],["put_litter_in_its_place",705],["question",773],["rabbit",194],["rabbit2",234],["racehorse",236],["radio",342],["radio_button",819],["rage",34],["rage1",862],["rage2",863],["rage3",864],["rage4",865],["railway_car",587],["rainbow",303],["raised_hand",112],["raised_hands",118],["raising_hand",136],["ram",230],["ramen",500],["rat",231],["recycle",747],["red_car",593],["red_circle",836],["registered",767],["relaxed",4],["relieved",17],["repeat",679],["repeat_one",680],["restroom",699],["revolving_hearts",178],["rewind",671],["ribbon",165],["rice",499],["rice_ball",497],["rice_cracker",498],["rice_scene",314],["ring",182],["rocket",572],["roller_coaster",566],["rooster",238],["rose",256],["rotating_light",598],["round_pushpin",624],["rowboat",570],["ru",634],["rugby_football",463],["runner",124],["running",124],["running_shirt_with_sash",156],["sa",711],["sagittarius",756],["sailboat",568],["sake",478],["sandal",149],["santa",317],["satellite",340],["satisfied",37],["saxophone",448],["school",537],["school_satchel",308],["scissors",417],["scorpius",755],["scream",32],["scream_cat",78],["scroll",410],["seat",574],["secret",718],["see_no_evil",84],["seedling",270],["seven",642],["shaved_ice",510],["sheep",206],["shell",223],["ship",567],["shipit",866],["shirt",152],["shit",89],["shoe",148],["shower",370],["signal_strength",686],["six",641],["six_pointed_star",761],["ski",471],["skull",87],["sleeping",41],["sleepy",24],["slightly_frowning_face",849],["slightly_smiling_face",848],["slot_machine",619],["small_blue_diamond",844],["small_orange_diamond",843],["small_red_triangle",831],["small_red_triangle_down",838],["smile",0],["smile_cat",74],["smiley",1],["smiley_cat",73],["smiling_imp",47],["smirk",56],["smirk_cat",77],["smoking",376],["snail",221],["snake",215],["snowboarder",472],["snowflake",299],["snowman",300],["sob",23],["soccer",459],["soon",781],["sos",721],["sound",344],["space_invader",450],["spades",811],["spaghetti",491],["sparkle",733],["sparkler",312],["sparkles",91],["sparkling_heart",177],["speak_no_evil",86],["speaker",345],["speech_balloon",186],["speedboat",569],["squirrel",866],["star",293],["star2",92],["stars",292],["station",578],["statue_of_liberty",561],["steam_locomotive",576],["stew",501],["straight_ruler",422],["strawberry",525],["stuck_out_tongue",13],["stuck_out_tongue_closed_eyes",12],["stuck_out_tongue_winking_eye",11],["sun_with_face",273],["sunflower",257],["sunglasses",40],["sunny",294],["sunrise",559],["sunrise_over_mountains",558],["surfer",474],["sushi",495],["suspect",867],["suspension_railway",606],["sweat",28],["sweat_drops",96],["sweat_smile",27],["sweet_potato",531],["swimmer",473],["symbols",649],["syringe",381],["taco",868],["tada",321],["tanabata_tree",320],["tangerine",520],["taurus",749],["taxi",594],["tea",477],["telephone",336],["telephone_receiver",337],["telescope",436],["tennis",461],["tent",553],["thought_balloon",188],["three",638],["thumbsdown",106],["thumbsup",105],["ticket",611],["tiger",196],["tiger2",233],["tired_face",30],["tm",768],["toilet",371],["tokyo_tower",555],["tomato",533],["tongue",103],["top",777],["tophat",144],["tractor",608],["traffic_light",613],["train",586],["train2",580],["tram",577],["triangular_flag_on_post",625],["triangular_ruler",423],["trident",824],["triumph",35],["trolleybus",588],["trollface",869],["trophy",470],["tropical_drink",483],["tropical_fish",224],["truck",597],["trumpet",447],["tshirt",152],["tulip",254],["turtle",216],["tv",341],["twisted_rightwards_arrows",678],["two",637],["two_hearts",176],["two_men_holding_hands",128],["two_women_holding_hands",129],["u5272",695],["u5408",692],["u55b6",696],["u6307",689],["u6708",709],["u6709",697],["u6e80",691],["u7121",698],["u7533",710],["u7981",693],["u7a7a",690],["uk",635],["umbrella",298],["unamused",18],["underage",724],["unlock",355],["up",682],["upside_down_face",850],["us",630],["v",110],["vertical_traffic_light",612],["vhs",329],["vibration_mode",739],["video_camera",328],["video_game",451],["violin",446],["virgo",753],["volcano",290],["vs",738],["walking",123],["waning_crescent_moon",283],["waning_gibbous_moon",281],["warning",614],["watch",354],["water_buffalo",232],["watermelon",524],["wave",111],["wavy_dash",822],["waxing_crescent_moon",277],["waxing_gibbous_moon",279],["wc",703],["weary",29],["wedding",545],["whale",227],["whale2",228],["wheelchair",707],["white_check_mark",735],["white_circle",835],["white_flower",815],["white_large_square",839],["white_medium_small_square",828],["white_medium_square",826],["white_small_square",830],["white_square_button",833],["wind_chime",313],["wine_glass",484],["wink",5],["wolf",190],["woman",67],["womans_clothes",154],["womans_hat",146],["womens",701],["worried",44],["wrench",372],["x",769],["yellow_heart",168],["yen",383],["yum",38],["za",847],["zap",297],["zero",645],["zzz",98]]);
+
+export const EmojiIndicesByUnicode = new Map([["1f604",0],["1f603",1],["1f600",2],["1f60a",3],["263a",4],["1f609",5],["1f60d",6],["1f618",7],["1f61a",8],["1f617",9],["1f619",10],["1f61c",11],["1f61d",12],["1f61b",13],["1f633",14],["1f601",15],["1f614",16],["1f60c",17],["1f612",18],["1f61e",19],["1f623",20],["1f622",21],["1f602",22],["1f62d",23],["1f62a",24],["1f625",25],["1f630",26],["1f605",27],["1f613",28],["1f629",29],["1f62b",30],["1f628",31],["1f631",32],["1f620",33],["1f621",34],["1f624",35],["1f616",36],["1f606",37],["1f60b",38],["1f637",39],["1f60e",40],["1f634",41],["1f635",42],["1f632",43],["1f61f",44],["1f626",45],["1f627",46],["1f608",47],["1f47f",48],["1f62e",49],["1f62c",50],["1f610",51],["1f615",52],["1f62f",53],["1f636",54],["1f607",55],["1f60f",56],["1f611",57],["1f472",58],["1f473",59],["1f46e",60],["1f477",61],["1f482",62],["1f476",63],["1f466",64],["1f467",65],["1f468",66],["1f469",67],["1f474",68],["1f475",69],["1f471",70],["1f47c",71],["1f478",72],["1f63a",73],["1f638",74],["1f63b",75],["1f63d",76],["1f63c",77],["1f640",78],["1f63f",79],["1f639",80],["1f63e",81],["1f479",82],["1f47a",83],["1f648",84],["1f649",85],["1f64a",86],["1f480",87],["1f47d",88],["1f4a9",89],["1f525",90],["2728",91],["1f31f",92],["1f4ab",93],["1f4a5",94],["1f4a2",95],["1f4a6",96],["1f4a7",97],["1f4a4",98],["1f4a8",99],["1f442",100],["1f440",101],["1f443",102],["1f445",103],["1f444",104],["1f44d",105],["1f44e",106],["1f44c",107],["1f44a",108],["270a",109],["270c",110],["1f44b",111],["270b",112],["1f450",113],["1f446",114],["1f447",115],["1f449",116],["1f448",117],["1f64c",118],["1f64f",119],["261d",120],["1f44f",121],["1f4aa",122],["1f6b6",123],["1f3c3",124],["1f483",125],["1f46b",126],["1f46a",127],["1f46c",128],["1f46d",129],["1f48f",130],["1f491",131],["1f46f",132],["1f646",133],["1f645",134],["1f481",135],["1f64b",136],["1f486",137],["1f487",138],["1f485",139],["1f470",140],["1f64e",141],["1f64d",142],["1f647",143],["1f3a9",144],["1f451",145],["1f452",146],["1f45f",147],["1f45e",148],["1f461",149],["1f460",150],["1f462",151],["1f455",152],["1f454",153],["1f45a",154],["1f457",155],["1f3bd",156],["1f456",157],["1f458",158],["1f459",159],["1f4bc",160],["1f45c",161],["1f45d",162],["1f45b",163],["1f453",164],["1f380",165],["1f302",166],["1f484",167],["1f49b",168],["1f499",169],["1f49c",170],["1f49a",171],["2764",172],["1f494",173],["1f497",174],["1f493",175],["1f495",176],["1f496",177],["1f49e",178],["1f498",179],["1f48c",180],["1f48b",181],["1f48d",182],["1f48e",183],["1f464",184],["1f465",185],["1f4ac",186],["1f463",187],["1f4ad",188],["1f436",189],["1f43a",190],["1f431",191],["1f42d",192],["1f439",193],["1f430",194],["1f438",195],["1f42f",196],["1f428",197],["1f43b",198],["1f437",199],["1f43d",200],["1f42e",201],["1f417",202],["1f435",203],["1f412",204],["1f434",205],["1f411",206],["1f418",207],["1f43c",208],["1f427",209],["1f426",210],["1f424",211],["1f425",212],["1f423",213],["1f414",214],["1f40d",215],["1f422",216],["1f41b",217],["1f41d",218],["1f41c",219],["1f41e",220],["1f40c",221],["1f419",222],["1f41a",223],["1f420",224],["1f41f",225],["1f42c",226],["1f433",227],["1f40b",228],["1f404",229],["1f40f",230],["1f400",231],["1f403",232],["1f405",233],["1f407",234],["1f409",235],["1f40e",236],["1f410",237],["1f413",238],["1f415",239],["1f416",240],["1f401",241],["1f402",242],["1f432",243],["1f421",244],["1f40a",245],["1f42b",246],["1f42a",247],["1f406",248],["1f408",249],["1f429",250],["1f43e",251],["1f490",252],["1f338",253],["1f337",254],["1f340",255],["1f339",256],["1f33b",257],["1f33a",258],["1f341",259],["1f343",260],["1f342",261],["1f33f",262],["1f33e",263],["1f344",264],["1f335",265],["1f334",266],["1f332",267],["1f333",268],["1f330",269],["1f331",270],["1f33c",271],["1f310",272],["1f31e",273],["1f31d",274],["1f31a",275],["1f311",276],["1f312",277],["1f313",278],["1f314",279],["1f315",280],["1f316",281],["1f317",282],["1f318",283],["1f31c",284],["1f31b",285],["1f319",286],["1f30d",287],["1f30e",288],["1f30f",289],["1f30b",290],["1f30c",291],["1f320",292],["2b50",293],["2600",294],["26c5",295],["2601",296],["26a1",297],["2614",298],["2744",299],["26c4",300],["1f300",301],["1f301",302],["1f308",303],["1f30a",304],["1f38d",305],["1f49d",306],["1f38e",307],["1f392",308],["1f393",309],["1f38f",310],["1f386",311],["1f387",312],["1f390",313],["1f391",314],["1f383",315],["1f47b",316],["1f385",317],["1f384",318],["1f381",319],["1f38b",320],["1f389",321],["1f38a",322],["1f388",323],["1f38c",324],["1f52e",325],["1f3a5",326],["1f4f7",327],["1f4f9",328],["1f4fc",329],["1f4bf",330],["1f4c0",331],["1f4bd",332],["1f4be",333],["1f4bb",334],["1f4f1",335],["260e",336],["1f4de",337],["1f4df",338],["1f4e0",339],["1f4e1",340],["1f4fa",341],["1f4fb",342],["1f50a",343],["1f509",344],["1f508",345],["1f507",346],["1f514",347],["1f515",348],["1f4e2",349],["1f4e3",350],["23f3",351],["231b",352],["23f0",353],["231a",354],["1f513",355],["1f512",356],["1f50f",357],["1f510",358],["1f511",359],["1f50e",360],["1f4a1",361],["1f526",362],["1f506",363],["1f505",364],["1f50c",365],["1f50b",366],["1f50d",367],["1f6c1",368],["1f6c0",369],["1f6bf",370],["1f6bd",371],["1f527",372],["1f529",373],["1f528",374],["1f6aa",375],["1f6ac",376],["1f4a3",377],["1f52b",378],["1f52a",379],["1f48a",380],["1f489",381],["1f4b0",382],["1f4b4",383],["1f4b5",384],["1f4b7",385],["1f4b6",386],["1f4b3",387],["1f4b8",388],["1f4f2",389],["1f4e7",390],["1f4e5",391],["1f4e4",392],["2709",393],["1f4e9",394],["1f4e8",395],["1f4ef",396],["1f4eb",397],["1f4ea",398],["1f4ec",399],["1f4ed",400],["1f4ee",401],["1f4e6",402],["1f4dd",403],["1f4c4",404],["1f4c3",405],["1f4d1",406],["1f4ca",407],["1f4c8",408],["1f4c9",409],["1f4dc",410],["1f4cb",411],["1f4c5",412],["1f4c6",413],["1f4c7",414],["1f4c1",415],["1f4c2",416],["2702",417],["1f4cc",418],["1f4ce",419],["2712",420],["270f",421],["1f4cf",422],["1f4d0",423],["1f4d5",424],["1f4d7",425],["1f4d8",426],["1f4d9",427],["1f4d3",428],["1f4d4",429],["1f4d2",430],["1f4da",431],["1f4d6",432],["1f516",433],["1f4db",434],["1f52c",435],["1f52d",436],["1f4f0",437],["1f3a8",438],["1f3ac",439],["1f3a4",440],["1f3a7",441],["1f3bc",442],["1f3b5",443],["1f3b6",444],["1f3b9",445],["1f3bb",446],["1f3ba",447],["1f3b7",448],["1f3b8",449],["1f47e",450],["1f3ae",451],["1f0cf",452],["1f3b4",453],["1f004",454],["1f3b2",455],["1f3af",456],["1f3c8",457],["1f3c0",458],["26bd",459],["26be",460],["1f3be",461],["1f3b1",462],["1f3c9",463],["1f3b3",464],["26f3",465],["1f6b5",466],["1f6b4",467],["1f3c1",468],["1f3c7",469],["1f3c6",470],["1f3bf",471],["1f3c2",472],["1f3ca",473],["1f3c4",474],["1f3a3",475],["2615",476],["1f375",477],["1f376",478],["1f37c",479],["1f37a",480],["1f37b",481],["1f378",482],["1f379",483],["1f377",484],["1f374",485],["1f355",486],["1f354",487],["1f35f",488],["1f357",489],["1f356",490],["1f35d",491],["1f35b",492],["1f364",493],["1f371",494],["1f363",495],["1f365",496],["1f359",497],["1f358",498],["1f35a",499],["1f35c",500],["1f372",501],["1f362",502],["1f361",503],["1f373",504],["1f35e",505],["1f369",506],["1f36e",507],["1f366",508],["1f368",509],["1f367",510],["1f382",511],["1f370",512],["1f36a",513],["1f36b",514],["1f36c",515],["1f36d",516],["1f36f",517],["1f34e",518],["1f34f",519],["1f34a",520],["1f34b",521],["1f352",522],["1f347",523],["1f349",524],["1f353",525],["1f351",526],["1f348",527],["1f34c",528],["1f350",529],["1f34d",530],["1f360",531],["1f346",532],["1f345",533],["1f33d",534],["1f3e0",535],["1f3e1",536],["1f3eb",537],["1f3e2",538],["1f3e3",539],["1f3e5",540],["1f3e6",541],["1f3ea",542],["1f3e9",543],["1f3e8",544],["1f492",545],["26ea",546],["1f3ec",547],["1f3e4",548],["1f307",549],["1f306",550],["1f3ef",551],["1f3f0",552],["26fa",553],["1f3ed",554],["1f5fc",555],["1f5fe",556],["1f5fb",557],["1f304",558],["1f305",559],["1f303",560],["1f5fd",561],["1f309",562],["1f3a0",563],["1f3a1",564],["26f2",565],["1f3a2",566],["1f6a2",567],["26f5",568],["1f6a4",569],["1f6a3",570],["2693",571],["1f680",572],["2708",573],["1f4ba",574],["1f681",575],["1f682",576],["1f68a",577],["1f689",578],["1f69e",579],["1f686",580],["1f684",581],["1f685",582],["1f688",583],["1f687",584],["1f69d",585],["1f68b",586],["1f683",587],["1f68e",588],["1f68c",589],["1f68d",590],["1f699",591],["1f698",592],["1f697",593],["1f695",594],["1f696",595],["1f69b",596],["1f69a",597],["1f6a8",598],["1f693",599],["1f694",600],["1f692",601],["1f691",602],["1f690",603],["1f6b2",604],["1f6a1",605],["1f69f",606],["1f6a0",607],["1f69c",608],["1f488",609],["1f68f",610],["1f3ab",611],["1f6a6",612],["1f6a5",613],["26a0",614],["1f6a7",615],["1f530",616],["26fd",617],["1f3ee",618],["1f3b0",619],["2668",620],["1f5ff",621],["1f3aa",622],["1f3ad",623],["1f4cd",624],["1f6a9",625],["1f1ef-1f1f5",626],["1f1f0-1f1f7",627],["1f1e9-1f1ea",628],["1f1e8-1f1f3",629],["1f1fa-1f1f8",630],["1f1eb-1f1f7",631],["1f1ea-1f1f8",632],["1f1ee-1f1f9",633],["1f1f7-1f1fa",634],["1f1ec-1f1e7",635],["0031-20e3",636],["0032-20e3",637],["0033-20e3",638],["0034-20e3",639],["0035-20e3",640],["0036-20e3",641],["0037-20e3",642],["0038-20e3",643],["0039-20e3",644],["0030-20e3",645],["1f51f",646],["1f522",647],["0023-20e3",648],["1f523",649],["2b06",650],["2b07",651],["2b05",652],["27a1",653],["1f520",654],["1f521",655],["1f524",656],["2197",657],["2196",658],["2198",659],["2199",660],["2194",661],["2195",662],["1f504",663],["25c0",664],["25b6",665],["1f53c",666],["1f53d",667],["21a9",668],["21aa",669],["2139",670],["23ea",671],["23e9",672],["23eb",673],["23ec",674],["2935",675],["2934",676],["1f197",677],["1f500",678],["1f501",679],["1f502",680],["1f195",681],["1f199",682],["1f192",683],["1f193",684],["1f196",685],["1f4f6",686],["1f3a6",687],["1f201",688],["1f22f",689],["1f233",690],["1f235",691],["1f234",692],["1f232",693],["1f250",694],["1f239",695],["1f23a",696],["1f236",697],["1f21a",698],["1f6bb",699],["1f6b9",700],["1f6ba",701],["1f6bc",702],["1f6be",703],["1f6b0",704],["1f6ae",705],["1f17f",706],["267f",707],["1f6ad",708],["1f237",709],["1f238",710],["1f202",711],["24c2",712],["1f6c2",713],["1f6c4",714],["1f6c5",715],["1f6c3",716],["1f251",717],["3299",718],["3297",719],["1f191",720],["1f198",721],["1f194",722],["1f6ab",723],["1f51e",724],["1f4f5",725],["1f6af",726],["1f6b1",727],["1f6b3",728],["1f6b7",729],["1f6b8",730],["26d4",731],["2733",732],["2747",733],["274e",734],["2705",735],["2734",736],["1f49f",737],["1f19a",738],["1f4f3",739],["1f4f4",740],["1f170",741],["1f171",742],["1f18e",743],["1f17e",744],["1f4a0",745],["27bf",746],["267b",747],["2648",748],["2649",749],["264a",750],["264b",751],["264c",752],["264d",753],["264e",754],["264f",755],["2650",756],["2651",757],["2652",758],["2653",759],["26ce",760],["1f52f",761],["1f3e7",762],["1f4b9",763],["1f4b2",764],["1f4b1",765],["00a9",766],["00ae",767],["2122",768],["274c",769],["203c",770],["2049",771],["2757",772],["2753",773],["2755",774],["2754",775],["2b55",776],["1f51d",777],["1f51a",778],["1f519",779],["1f51b",780],["1f51c",781],["1f503",782],["1f55b",783],["1f567",784],["1f550",785],["1f55c",786],["1f551",787],["1f55d",788],["1f552",789],["1f55e",790],["1f553",791],["1f55f",792],["1f554",793],["1f560",794],["1f555",795],["1f556",796],["1f557",797],["1f558",798],["1f559",799],["1f55a",800],["1f561",801],["1f562",802],["1f563",803],["1f564",804],["1f565",805],["1f566",806],["2716",807],["2795",808],["2796",809],["2797",810],["2660",811],["2665",812],["2663",813],["2666",814],["1f4ae",815],["1f4af",816],["2714",817],["2611",818],["1f518",819],["1f517",820],["27b0",821],["3030",822],["303d",823],["1f531",824],["25fc",825],["25fb",826],["25fe",827],["25fd",828],["25aa",829],["25ab",830],["1f53a",831],["1f532",832],["1f533",833],["26ab",834],["26aa",835],["1f534",836],["1f535",837],["1f53b",838],["2b1c",839],["2b1b",840],["1f536",841],["1f537",842],["1f538",843],["1f539",844],["1f1e8-1f1e6",845],["1f1f5-1f1f0",846],["1f1ff-1f1e6",847],["1f642",848],["1f641",849],["1f643",850]]);
+
+export const CategoryNames = ["people","nature","symbols","activity","objects","travel","food","flags","custom"];
+
+export const EmojiIndicesByCategory = new Map([["people",[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,87,88,89,98,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,157,158,159,160,161,162,163,164,166,167,181,182,184,185,187,308,309,316,317,848,849,850,859]],["nature",[84,85,86,90,91,92,93,94,96,97,99,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,293,294,295,296,297,298,299,300,304,305,315,318,320]],["symbols",[95,168,169,170,171,172,173,174,175,176,177,178,179,186,188,272,301,306,343,344,345,346,347,348,349,350,363,364,434,443,444,452,453,454,614,616,620,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844]],["activity",[156,369,438,439,440,441,442,445,446,447,448,449,450,451,455,456,457,458,459,460,461,462,463,464,465,466,467,469,470,471,472,473,474,475,570,611,619,622,623]],["objects",[165,180,183,307,310,313,319,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,351,352,353,354,355,356,357,358,359,360,361,362,365,366,367,368,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,435,436,437,609,618,621,624,625,820]],["travel",[290,291,292,302,303,311,312,314,468,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,610,612,613,615,617]],["food",[476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,868]],["flags",[626,627,628,629,630,631,632,633,634,635,845,846,847]],["custom",[851,852,853,854,855,856,857,858,860,861,862,863,864,865,866,867,869]]]);
+
+/* eslint-enable */
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index b6d3b076f..e1ce4e81c 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -539,6 +539,7 @@ export function applyTheme(theme) {
if (theme.mentionColor) {
changeCss('.sidebar--left .nav-pills__unread-indicator', 'color:' + theme.mentionColor);
changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor + '!important;');
+ changeCss('.app__body .post-reaction--current-user', 'background-color:' + changeOpacity(theme.mentionColor, 0.4));
}
if (theme.centerChannelBg) {
@@ -628,6 +629,8 @@ export function applyTheme(theme) {
changeCss('.app__body .post.post--comment.current--user .post__body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.app__body .channel-header__info .status .offline--icon', 'fill:' + theme.centerChannelColor);
changeCss('.app__body .navbar .status .offline--icon', 'fill:' + theme.centerChannelColor);
+ changeCss('.app__body .post-reaction:not(.post-reaction--current-user)', 'background-color:' + changeOpacity(theme.centerChannelColor, 0.2));
+ changeCss('.app__body .post-reaction', 'border-color:' + theme.centerChannelColor);
}
if (theme.newMessageSeparator) {