From d0075026290c90d8406c7ac81413259a8ae58ec7 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Fri, 15 Nov 2019 08:39:21 +0200
Subject: [PATCH 01/11] add fetching for emoji reactions, draft design

---
 src/components/conversation/conversation.js   |  1 +
 src/components/status/status.js               |  6 ++++
 src/components/status/status.vue              | 28 +++++++++++++++++++
 src/modules/statuses.js                       | 14 +++++++++-
 src/services/api/api.service.js               |  6 ++++
 .../backend_interactor_service.js             |  2 ++
 6 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 72ee9c39..715804ff 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -149,6 +149,7 @@ const conversation = {
       if (!id) return
       this.highlight = id
       this.$store.dispatch('fetchFavsAndRepeats', id)
+      this.$store.dispatch('fetchEmojiReactions', id)
     },
     getHighlight () {
       return this.isExpanded ? this.highlight : null
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4fbd5ac3..8268e615 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -278,6 +278,12 @@ const Status = {
     hidePostStats () {
       return this.mergedConfig.hidePostStats
     },
+    emojiReactions () {
+      return {
+        '🤔': [{ 'id': 'xyz..' }, { 'id': 'zyx...' }],
+        '🐻': [{ 'id': 'abc...' }]
+      }
+    },
     ...mapGetters(['mergedConfig'])
   },
   components: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 65778b2e..aae58a5e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -354,6 +354,17 @@
             </div>
           </transition>
 
+          <div class="emoji-reactions">
+            <button
+              class="emoji-reaction btn btn-default"
+              v-for="(users, emoji) in emojiReactions"
+              :key="emoji"
+            >
+              <span>{{users.length}}</span>
+              <span>{{emoji}}</span>
+            </button>
+          </div>
+
           <div
             v-if="!noHeading && !isPreview"
             class="status-actions media-body"
@@ -771,6 +782,23 @@ $status-margin: 0.75em;
   }
 }
 
+.emoji-reactions {
+  display: flex;
+  margin-top: 0.75em;
+}
+
+.emoji-reaction {
+  padding: 0 0.5em;
+  margin-right: 0.5em;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  :first-child {
+    margin-right: 0.25em;
+  }
+}
+
 .button-icon.icon-reply {
   &:not(.button-icon-disabled):hover,
   &.button-icon-active {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f11ffdcd..c285b452 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,4 @@
-import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
+import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy, findKey } from 'lodash'
 import { set } from 'vue'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
@@ -510,6 +510,11 @@ export const mutations = {
     newStatus.fave_num = newStatus.favoritedBy.length
     newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
   },
+  addEmojiReactions (state, { id, emojiReactions, currentUser }) {
+    const status = state.allStatusesObject[id]
+    status.emojiReactions = emojiReactions
+    status.reactedWithEmoji = findKey(emojiReactions, { id: currentUser.id })
+  },
   updateStatusWithPoll (state, { id, poll }) {
     const status = state.allStatusesObject[id]
     status.poll = poll
@@ -611,6 +616,13 @@ const statuses = {
         commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
       })
     },
+    fetchEmojiReactions ({ rootState, commit }, id) {
+      rootState.api.backendInteractor.fetchEmojiReactions(id).then(
+        emojiReactions => {
+          commit('addEmojiReactions', { id, emojiReactions, currentUser: rootState.users.currentUser })
+        }
+      )
+    },
     fetchFavs ({ rootState, commit }, id) {
       rootState.api.backendInteractor.fetchFavoritedByUsers(id)
         .then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 8f5eb416..7ef4b74a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -71,6 +71,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
 const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
 const MASTODON_SEARCH_2 = `/api/v2/search`
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by`
 
 const oldfetch = window.fetch
 
@@ -864,6 +865,10 @@ const fetchRebloggedByUsers = ({ id }) => {
   return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
 }
 
+const fetchEmojiReactions = ({ id }) => {
+  return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id) })
+}
+
 const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
   return promisedRequest({
     url: MASTODON_REPORT_USER_URL,
@@ -997,6 +1002,7 @@ const apiService = {
   fetchPoll,
   fetchFavoritedByUsers,
   fetchRebloggedByUsers,
+  fetchEmojiReactions,
   reportUser,
   updateNotificationSettings,
   search2,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index d6617276..52234fcc 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -143,6 +143,7 @@ const backendInteractorService = credentials => {
 
   const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
   const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
+  const fetchEmojiReactions = (id) => apiService.fetchEmojiReactions({ id })
   const reportUser = (params) => apiService.reportUser({ credentials, ...params })
 
   const favorite = (id) => apiService.favorite({ id, credentials })
@@ -210,6 +211,7 @@ const backendInteractorService = credentials => {
     fetchPoll,
     fetchFavoritedByUsers,
     fetchRebloggedByUsers,
+    fetchEmojiReactions,
     reportUser,
     favorite,
     unfavorite,

From de945ba3e9470b28dd010fb32f658b42053f19d3 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Fri, 15 Nov 2019 16:29:25 +0200
Subject: [PATCH 02/11] wip commit, add basic popover for emoji reaction select

---
 src/components/react_button/react_button.js  | 50 +++++++++++++
 src/components/react_button/react_button.vue | 78 ++++++++++++++++++++
 src/components/status/status.js              |  2 +
 src/components/status/status.vue             | 10 ++-
 src/i18n/en.json                             |  1 +
 5 files changed, 138 insertions(+), 3 deletions(-)
 create mode 100644 src/components/react_button/react_button.js
 create mode 100644 src/components/react_button/react_button.vue

diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
new file mode 100644
index 00000000..d1d15d93
--- /dev/null
+++ b/src/components/react_button/react_button.js
@@ -0,0 +1,50 @@
+import { mapGetters } from 'vuex'
+
+const ReactButton = {
+  props: ['status', 'loggedIn'],
+  data () {
+    return {
+      animated: false,
+      showTooltip: false,
+      popperOptions: {
+        modifiers: {
+          preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
+        }
+      }
+    }
+  },
+  methods: {
+    openReactionSelect () {
+      console.log('test')
+      this.showTooltip = true
+    },
+    closeReactionSelect () {
+      this.showTooltip = false
+    },
+    favorite () {
+      if (!this.status.favorited) {
+        this.$store.dispatch('favorite', { id: this.status.id })
+      } else {
+        this.$store.dispatch('unfavorite', { id: this.status.id })
+      }
+      this.animated = true
+      setTimeout(() => {
+        this.animated = false
+      }, 500)
+    }
+  },
+  computed: {
+    emojis () {
+      return this.$store.state.instance.emoji || []
+    },
+    classes () {
+      return {
+        'icon-smile': true,
+        'animate-spin': this.animated
+      }
+    },
+    ...mapGetters(['mergedConfig'])
+  }
+}
+
+export default ReactButton
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
new file mode 100644
index 00000000..93638770
--- /dev/null
+++ b/src/components/react_button/react_button.vue
@@ -0,0 +1,78 @@
+<template>
+  <v-popover
+    :popper-options="popperOptions"
+    :open="showTooltip"
+    trigger="manual"
+    placement="top"
+    class="react-button-popover"
+    @close-group="closeReactionSelect"
+  >
+    <div slot="popover">
+      <div class="reaction-picker">
+        <span
+          v-for="(emoji, key) in emojis"
+          :key="key"
+          class="emoji-reaction-button"
+        >
+          {{ emoji.replacement }}
+        </span>
+        <div class="reaction-bottom-fader" />
+      </div>
+    </div>
+    <div @click.prevent="openReactionSelect" v-if="loggedIn">
+      <i
+        :class="classes"
+        class="button-icon favorite-button fav-active"
+        :title="$t('tool_tip.add_reaction')"
+      />
+      <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
+    </div>
+  </v-popover>
+</template>
+
+<script src="./react_button.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.reaction-picker {
+  width: 10em;
+  height: 8em;
+  font-size: 1.5em;
+  overflow-y: scroll;
+  display: flex;
+  flex-wrap: wrap;
+  padding: 0.5em;
+  text-align:center;
+
+  mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+    linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+    linear-gradient(to top, white, white);
+  transition: mask-size 150ms;
+  mask-size: 100% 20px, 100% 20px, auto;
+  // Autoprefixed seem to ignore this one, and also syntax is different
+  -webkit-mask-composite: xor;
+  mask-composite: exclude;
+}
+
+.emoji-reaction-button {
+  flex-basis: 20%;
+  line-height: 1.5em;
+  align-content: center;
+}
+
+.fav-active {
+  cursor: pointer;
+  animation-duration: 0.6s;
+
+  &:hover {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+}
+
+.favorite-button.icon-star {
+  color: $fallback--cOrange;
+  color: var(--cOrange, $fallback--cOrange);
+}
+</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 8268e615..8c6fc0cf 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,5 +1,6 @@
 import Attachment from '../attachment/attachment.vue'
 import FavoriteButton from '../favorite_button/favorite_button.vue'
+import ReactButton from '../react_button/react_button.vue'
 import RetweetButton from '../retweet_button/retweet_button.vue'
 import Poll from '../poll/poll.vue'
 import ExtraButtons from '../extra_buttons/extra_buttons.vue'
@@ -289,6 +290,7 @@ const Status = {
   components: {
     Attachment,
     FavoriteButton,
+    ReactButton,
     RetweetButton,
     ExtraButtons,
     PostStatusForm,
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index aae58a5e..d455ccf6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -356,12 +356,12 @@
 
           <div class="emoji-reactions">
             <button
-              class="emoji-reaction btn btn-default"
               v-for="(users, emoji) in emojiReactions"
               :key="emoji"
+              class="emoji-reaction btn btn-default"
             >
-              <span>{{users.length}}</span>
-              <span>{{emoji}}</span>
+              <span>{{ users.length }}</span>
+              <span>{{ emoji }}</span>
             </button>
           </div>
 
@@ -393,6 +393,10 @@
               :logged-in="loggedIn"
               :status="status"
             />
+            <ReactButton
+              :logged-in="loggedIn"
+              :status="status"
+            />
             <extra-buttons
               :status="status"
               @onError="showError"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ad3e671d..febbf2ea 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -632,6 +632,7 @@
     "repeat": "Repeat",
     "reply": "Reply",
     "favorite": "Favorite",
+    "add_reaction": "Add Reaction",
     "user_settings": "User Settings"
   },
   "upload":{

From 33abbed5a1e1d1cf99d21d481b2a22481d7533b2 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Mon, 13 Jan 2020 23:34:39 +0200
Subject: [PATCH 03/11] usable-but-buggy: picker, adding/removing reaction on
 click, search, styles

---
 src/components/react_button/react_button.js   | 25 ++++----
 src/components/react_button/react_button.vue  | 37 +++++++++--
 src/components/status/status.js               | 21 +++++--
 src/components/status/status.vue              | 24 ++++++--
 src/modules/statuses.js                       | 61 ++++++++++++++++++-
 src/services/api/api.service.js               | 22 +++++++
 .../backend_interactor_service.js             |  4 ++
 7 files changed, 166 insertions(+), 28 deletions(-)

diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index d1d15d93..76a49305 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -6,6 +6,7 @@ const ReactButton = {
     return {
       animated: false,
       showTooltip: false,
+      filterWord: '',
       popperOptions: {
         modifiers: {
           preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
@@ -14,27 +15,25 @@ const ReactButton = {
     }
   },
   methods: {
-    openReactionSelect () {
-      console.log('test')
-      this.showTooltip = true
+    toggleReactionSelect () {
+      this.showTooltip = !this.showTooltip
     },
     closeReactionSelect () {
       this.showTooltip = false
     },
-    favorite () {
-      if (!this.status.favorited) {
-        this.$store.dispatch('favorite', { id: this.status.id })
-      } else {
-        this.$store.dispatch('unfavorite', { id: this.status.id })
-      }
-      this.animated = true
-      setTimeout(() => {
-        this.animated = false
-      }, 500)
+    addReaction (event, emoji) {
+      this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+      this.closeReactionSelect()
     }
   },
   computed: {
+    commonEmojis () {
+      return ['💖', '😠', '👀', '😂', '🔥']
+    },
     emojis () {
+      if (this.filterWord !== '') {
+        return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
+      }
       return this.$store.state.instance.emoji || []
     },
     classes () {
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index 93638770..f7015316 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -5,21 +5,37 @@
     trigger="manual"
     placement="top"
     class="react-button-popover"
-    @close-group="closeReactionSelect"
+    @hide="closeReactionSelect"
   >
     <div slot="popover">
+      <div class="reaction-picker-filter">
+        <input v-model="filterWord" placeholder="Search...">
+      </div>
       <div class="reaction-picker">
+        <span
+          v-for="emoji in commonEmojis"
+          :key="emoji"
+          class="emoji-reaction-button"
+          @click="addReaction($event, emoji)"
+        >
+          {{ emoji }}
+        </span>
+        <div class="reaction-picker-divider" />
         <span
           v-for="(emoji, key) in emojis"
           :key="key"
           class="emoji-reaction-button"
+          @click="addReaction($event, emoji.replacement)"
         >
           {{ emoji.replacement }}
         </span>
         <div class="reaction-bottom-fader" />
       </div>
     </div>
-    <div @click.prevent="openReactionSelect" v-if="loggedIn">
+    <div
+      v-if="loggedIn"
+      @click.prevent="toggleReactionSelect"
+    >
       <i
         :class="classes"
         class="button-icon favorite-button fav-active"
@@ -35,15 +51,28 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
+.reaction-picker-filter {
+  padding: 0.5em;
+}
+
+.reaction-picker-divider {
+  height: 1px;
+  width: 100%;
+  margin: 0.4em;
+  background-color: var(--border, $fallback--border);
+}
+
 .reaction-picker {
   width: 10em;
-  height: 8em;
+  height: 9em;
   font-size: 1.5em;
   overflow-y: scroll;
   display: flex;
   flex-wrap: wrap;
   padding: 0.5em;
-  text-align:center;
+  text-align: center;
+  align-content: flex-start;
+  user-select: none;
 
   mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
     linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 8c6fc0cf..cc0c9e06 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -280,10 +280,7 @@ const Status = {
       return this.mergedConfig.hidePostStats
     },
     emojiReactions () {
-      return {
-        '🤔': [{ 'id': 'xyz..' }, { 'id': 'zyx...' }],
-        '🐻': [{ 'id': 'abc...' }]
-      }
+      return this.status.emojiReactions
     },
     ...mapGetters(['mergedConfig'])
   },
@@ -385,6 +382,22 @@ const Status = {
     setMedia () {
       const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
       return () => this.$store.dispatch('setMedia', attachments)
+    },
+    reactedWith (emoji) {
+      return this.status.reactedWithEmoji.includes(emoji)
+    },
+    reactWith (emoji) {
+      this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+    },
+    unreact (emoji) {
+      this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+    },
+    emojiOnClick (emoji, event) {
+      if (this.reactedWith(emoji)) {
+        this.unreact(emoji)
+      } else {
+        this.reactWith(emoji)
+      }
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d455ccf6..503de98d 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -354,13 +354,15 @@
             </div>
           </transition>
 
-          <div class="emoji-reactions">
+          <div v-if="isFocused" class="emoji-reactions">
             <button
               v-for="(users, emoji) in emojiReactions"
               :key="emoji"
               class="emoji-reaction btn btn-default"
+              :class="{ 'picked-reaction': reactedWith(emoji) }"
+              @click="emojiOnClick(emoji, $event)"
             >
-              <span>{{ users.length }}</span>
+              <span v-if="users">{{ users.length }}</span>
               <span>{{ emoji }}</span>
             </button>
           </div>
@@ -788,19 +790,33 @@ $status-margin: 0.75em;
 
 .emoji-reactions {
   display: flex;
-  margin-top: 0.75em;
+  margin-top: 0.25em;
+  flex-wrap: wrap;
 }
 
 .emoji-reaction {
   padding: 0 0.5em;
   margin-right: 0.5em;
+  margin-top: 0.5em;
   display: flex;
   align-items: center;
   justify-content: center;
-
+  box-sizing: border-box;
   :first-child {
     margin-right: 0.25em;
   }
+  :last-child {
+    width: 1.5em;
+  }
+  &:focus {
+    outline: none;
+  }
+}
+
+.picked-reaction {
+  border: 1px solid var(--link, $fallback--link);
+  margin-left: -1px; // offset the border, can't use inset shadows either
+  margin-right: calc(0.5em - 1px);
 }
 
 .button-icon.icon-reply {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index c285b452..fcb6d1f3 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,20 @@
-import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy, findKey } from 'lodash'
+import {
+  remove,
+  slice,
+  each,
+  findIndex,
+  find,
+  maxBy,
+  minBy,
+  merge,
+  first,
+  last,
+  isArray,
+  omitBy,
+  flow,
+  filter,
+  keys
+} from 'lodash'
 import { set } from 'vue'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
@@ -512,8 +528,29 @@ export const mutations = {
   },
   addEmojiReactions (state, { id, emojiReactions, currentUser }) {
     const status = state.allStatusesObject[id]
-    status.emojiReactions = emojiReactions
-    status.reactedWithEmoji = findKey(emojiReactions, { id: currentUser.id })
+    set(status, 'emojiReactions', emojiReactions)
+    const reactedWithEmoji = flow(keys, filter(reaction => find(reaction, { id: currentUser.id })))(emojiReactions)
+    set(status, 'reactedWithEmoji', reactedWithEmoji)
+  },
+  addOwnReaction (state, { id, emoji, currentUser }) {
+    const status = state.allStatusesObject[id]
+    status.emojiReactions = status.emojiReactions || {}
+    const listOfUsers = (status.emojiReactions && status.emojiReactions[emoji]) || []
+    const hasSelfAlready = !!find(listOfUsers, { id: currentUser.id })
+    if (!hasSelfAlready) {
+      set(status.emojiReactions, emoji, listOfUsers.concat([{ id: currentUser.id }]))
+      set(status, 'reactedWithEmoji', emoji)
+    }
+  },
+  removeOwnReaction (state, { id, emoji, currentUser }) {
+    const status = state.allStatusesObject[id]
+    const listOfUsers = status.emojiReactions[emoji] || []
+    const hasSelfAlready = !!find(listOfUsers, { id: currentUser.id })
+    if (hasSelfAlready) {
+      const newUsers = filter(listOfUsers, user => user.id !== currentUser.id)
+      set(status.emojiReactions, emoji, newUsers)
+      set(status, 'reactedWith', emoji)
+    }
   },
   updateStatusWithPoll (state, { id, poll }) {
     const status = state.allStatusesObject[id]
@@ -616,6 +653,24 @@ const statuses = {
         commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
       })
     },
+    reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+      const currentUser = rootState.users.currentUser
+      commit('addOwnReaction', { id, emoji, currentUser })
+      rootState.api.backendInteractor.reactWithEmoji(id, emoji).then(
+        status => {
+          dispatch('fetchEmojiReactions', id)
+        }
+      )
+    },
+    unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+      const currentUser = rootState.users.currentUser
+      commit('removeOwnReaction', { id, emoji, currentUser })
+      rootState.api.backendInteractor.unreactWithEmoji(id, emoji).then(
+        status => {
+          dispatch('fetchEmojiReactions', id)
+        }
+      )
+    },
     fetchEmojiReactions ({ rootState, commit }, id) {
       rootState.api.backendInteractor.fetchEmojiReactions(id).then(
         emojiReactions => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7ef4b74a..2e96264a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,6 +72,8 @@ const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
 const MASTODON_SEARCH_2 = `/api/v2/search`
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by`
+const PLEROMA_EMOJI_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji`
+const PLEROMA_EMOJI_UNREACT_URL = id => `/api/v1/pleroma/statuses/${id}/unreact_with_emoji`
 
 const oldfetch = window.fetch
 
@@ -869,6 +871,24 @@ const fetchEmojiReactions = ({ id }) => {
   return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id) })
 }
 
+const reactWithEmoji = ({ id, emoji, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_EMOJI_REACT_URL(id),
+    method: 'POST',
+    credentials,
+    payload: { emoji }
+  }).then(status => parseStatus(status))
+}
+
+const unreactWithEmoji = ({ id, emoji, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_EMOJI_UNREACT_URL(id),
+    method: 'POST',
+    credentials,
+    payload: { emoji }
+  }).then(parseStatus)
+}
+
 const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
   return promisedRequest({
     url: MASTODON_REPORT_USER_URL,
@@ -1003,6 +1023,8 @@ const apiService = {
   fetchFavoritedByUsers,
   fetchRebloggedByUsers,
   fetchEmojiReactions,
+  reactWithEmoji,
+  unreactWithEmoji,
   reportUser,
   updateNotificationSettings,
   search2,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 52234fcc..44233a24 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -144,6 +144,8 @@ const backendInteractorService = credentials => {
   const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
   const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
   const fetchEmojiReactions = (id) => apiService.fetchEmojiReactions({ id })
+  const reactWithEmoji = (id, emoji) => apiService.reactWithEmoji({ id, emoji, credentials })
+  const unreactWithEmoji = (id, emoji) => apiService.unreactWithEmoji({ id, emoji, credentials })
   const reportUser = (params) => apiService.reportUser({ credentials, ...params })
 
   const favorite = (id) => apiService.favorite({ id, credentials })
@@ -212,6 +214,8 @@ const backendInteractorService = credentials => {
     fetchFavoritedByUsers,
     fetchRebloggedByUsers,
     fetchEmojiReactions,
+    reactWithEmoji,
+    unreactWithEmoji,
     reportUser,
     favorite,
     unfavorite,

From b10b92a876eb185a88e751d028e69063c9117298 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Tue, 14 Jan 2020 10:06:14 +0200
Subject: [PATCH 04/11] clean up code, fix prediction bug

---
 .../emoji_reactions/emoji_reactions.js        | 30 +++++++++++
 .../emoji_reactions/emoji_reactions.vue       | 51 +++++++++++++++++++
 src/components/react_button/react_button.js   |  5 +-
 src/components/react_button/react_button.vue  | 46 +++++++++--------
 src/components/status/status.js               | 23 ++-------
 src/components/status/status.vue              | 47 ++---------------
 src/modules/statuses.js                       |  9 ++--
 7 files changed, 122 insertions(+), 89 deletions(-)
 create mode 100644 src/components/emoji_reactions/emoji_reactions.js
 create mode 100644 src/components/emoji_reactions/emoji_reactions.vue

diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
new file mode 100644
index 00000000..e81e6e25
--- /dev/null
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -0,0 +1,30 @@
+
+const EmojiReactions = {
+  name: 'EmojiReactions',
+  props: ['status'],
+  computed: {
+    emojiReactions () {
+      return this.status.emojiReactions
+    }
+  },
+  methods: {
+    reactedWith (emoji) {
+      return this.status.reactedWithEmoji.includes(emoji)
+    },
+    reactWith (emoji) {
+      this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+    },
+    unreact (emoji) {
+      this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+    },
+    emojiOnClick (emoji, event) {
+      if (this.reactedWith(emoji)) {
+        this.unreact(emoji)
+      } else {
+        this.reactWith(emoji)
+      }
+    }
+  }
+}
+
+export default EmojiReactions
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
new file mode 100644
index 00000000..d83f60b6
--- /dev/null
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -0,0 +1,51 @@
+<template>
+  <div class="emoji-reactions">
+    <button
+      v-for="(users, emoji) in emojiReactions"
+      :key="emoji"
+      class="emoji-reaction btn btn-default"
+      :class="{ 'picked-reaction': reactedWith(emoji) }"
+      @click="emojiOnClick(emoji, $event)"
+    >
+      <span v-if="users">{{ users.length }}</span>
+      <span>{{ emoji }}</span>
+    </button>
+  </div>
+</template>
+
+<script src="./emoji_reactions.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.emoji-reactions {
+  display: flex;
+  margin-top: 0.25em;
+  flex-wrap: wrap;
+}
+
+.emoji-reaction {
+  padding: 0 0.5em;
+  margin-right: 0.5em;
+  margin-top: 0.5em;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+  :first-child {
+    margin-right: 0.25em;
+  }
+  :last-child {
+    width: 1.5em;
+  }
+  &:focus {
+    outline: none;
+  }
+}
+
+.picked-reaction {
+  border: 1px solid var(--link, $fallback--link);
+  margin-left: -1px; // offset the border, can't use inset shadows either
+  margin-right: calc(0.5em - 1px);
+}
+
+</style>
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 76a49305..d1a179bc 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -15,8 +15,9 @@ const ReactButton = {
     }
   },
   methods: {
-    toggleReactionSelect () {
-      this.showTooltip = !this.showTooltip
+    openReactionSelect () {
+      this.showTooltip = true
+      this.filterWord = ''
     },
     closeReactionSelect () {
       this.showTooltip = false
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index f7015316..ae975dee 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -9,13 +9,16 @@
   >
     <div slot="popover">
       <div class="reaction-picker-filter">
-        <input v-model="filterWord" placeholder="Search...">
+        <input
+          v-model="filterWord"
+          :placeholder="$t('emoji.search_emoji')"
+        >
       </div>
       <div class="reaction-picker">
         <span
           v-for="emoji in commonEmojis"
           :key="emoji"
-          class="emoji-reaction-button"
+          class="emoji-button"
           @click="addReaction($event, emoji)"
         >
           {{ emoji }}
@@ -24,7 +27,7 @@
         <span
           v-for="(emoji, key) in emojis"
           :key="key"
-          class="emoji-reaction-button"
+          class="emoji-button"
           @click="addReaction($event, emoji.replacement)"
         >
           {{ emoji.replacement }}
@@ -34,11 +37,11 @@
     </div>
     <div
       v-if="loggedIn"
-      @click.prevent="toggleReactionSelect"
+      @click.prevent="openReactionSelect"
     >
       <i
         :class="classes"
-        class="button-icon favorite-button fav-active"
+        class="button-icon add-reaction-button"
         :title="$t('tool_tip.add_reaction')"
       />
       <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
@@ -58,7 +61,7 @@
 .reaction-picker-divider {
   height: 1px;
   width: 100%;
-  margin: 0.4em;
+  margin: 0.5em;
   background-color: var(--border, $fallback--border);
 }
 
@@ -82,26 +85,27 @@
   // Autoprefixed seem to ignore this one, and also syntax is different
   -webkit-mask-composite: xor;
   mask-composite: exclude;
-}
 
-.emoji-reaction-button {
-  flex-basis: 20%;
-  line-height: 1.5em;
-  align-content: center;
-}
+  .emoji-button {
+    cursor: pointer;
 
-.fav-active {
-  cursor: pointer;
-  animation-duration: 0.6s;
+    flex-basis: 20%;
+    line-height: 1.5em;
+    align-content: center;
 
-  &:hover {
-    color: $fallback--cOrange;
-    color: var(--cOrange, $fallback--cOrange);
+    &:hover {
+      transform: scale(1.25);
+    }
   }
 }
 
-.favorite-button.icon-star {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
+.add-reaction-button {
+  cursor: pointer;
+
+  &:hover {
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
+  }
 }
+
 </style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 18617938..81b57667 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -12,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
 import AvatarList from '../avatar_list/avatar_list.vue'
 import Timeago from '../timeago/timeago.vue'
 import StatusPopover from '../status_popover/status_popover.vue'
+import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import fileType from 'src/services/file_type/file_type.service'
 import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
@@ -311,9 +312,6 @@ const Status = {
     hidePostStats () {
       return this.mergedConfig.hidePostStats
     },
-    emojiReactions () {
-      return this.status.emojiReactions
-    },
     ...mapGetters(['mergedConfig']),
     ...mapState({
       betterShadow: state => state.interface.browserSupport.cssFilter,
@@ -334,7 +332,8 @@ const Status = {
     LinkPreview,
     AvatarList,
     Timeago,
-    StatusPopover
+    StatusPopover,
+    EmojiReactions
   },
   methods: {
     visibilityIcon (visibility) {
@@ -418,22 +417,6 @@ const Status = {
     setMedia () {
       const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
       return () => this.$store.dispatch('setMedia', attachments)
-    },
-    reactedWith (emoji) {
-      return this.status.reactedWithEmoji.includes(emoji)
-    },
-    reactWith (emoji) {
-      this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
-    },
-    unreact (emoji) {
-      this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
-    },
-    emojiOnClick (emoji, event) {
-      if (this.reactedWith(emoji)) {
-        this.unreact(emoji)
-      } else {
-        this.reactWith(emoji)
-      }
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 4ea1b74b..87e8b5da 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -354,18 +354,10 @@
             </div>
           </transition>
 
-          <div v-if="isFocused" class="emoji-reactions">
-            <button
-              v-for="(users, emoji) in emojiReactions"
-              :key="emoji"
-              class="emoji-reaction btn btn-default"
-              :class="{ 'picked-reaction': reactedWith(emoji) }"
-              @click="emojiOnClick(emoji, $event)"
-            >
-              <span v-if="users">{{ users.length }}</span>
-              <span>{{ emoji }}</span>
-            </button>
-          </div>
+          <EmojiReactions
+            v-if="isFocused"
+            :status="status"
+          />
 
           <div
             v-if="!noHeading && !isPreview"
@@ -789,37 +781,6 @@ $status-margin: 0.75em;
   }
 }
 
-.emoji-reactions {
-  display: flex;
-  margin-top: 0.25em;
-  flex-wrap: wrap;
-}
-
-.emoji-reaction {
-  padding: 0 0.5em;
-  margin-right: 0.5em;
-  margin-top: 0.5em;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-sizing: border-box;
-  :first-child {
-    margin-right: 0.25em;
-  }
-  :last-child {
-    width: 1.5em;
-  }
-  &:focus {
-    outline: none;
-  }
-}
-
-.picked-reaction {
-  border: 1px solid var(--link, $fallback--link);
-  margin-left: -1px; // offset the border, can't use inset shadows either
-  margin-right: calc(0.5em - 1px);
-}
-
 .button-icon.icon-reply {
   &:not(.button-icon-disabled):hover,
   &.button-icon-active {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index ae6f6853..dbae9d38 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -537,7 +537,10 @@ export const mutations = {
   addEmojiReactions (state, { id, emojiReactions, currentUser }) {
     const status = state.allStatusesObject[id]
     set(status, 'emojiReactions', emojiReactions)
-    const reactedWithEmoji = flow(keys, filter(reaction => find(reaction, { id: currentUser.id })))(emojiReactions)
+    const reactedWithEmoji = flow(
+      keys,
+      filter(reaction => find(reaction, { id: currentUser.id }))
+    )(emojiReactions)
     set(status, 'reactedWithEmoji', reactedWithEmoji)
   },
   addOwnReaction (state, { id, emoji, currentUser }) {
@@ -547,7 +550,7 @@ export const mutations = {
     const hasSelfAlready = !!find(listOfUsers, { id: currentUser.id })
     if (!hasSelfAlready) {
       set(status.emojiReactions, emoji, listOfUsers.concat([{ id: currentUser.id }]))
-      set(status, 'reactedWithEmoji', emoji)
+      set(status, 'reactedWithEmoji', [...status.reactedWithEmoji, emoji])
     }
   },
   removeOwnReaction (state, { id, emoji, currentUser }) {
@@ -557,7 +560,7 @@ export const mutations = {
     if (hasSelfAlready) {
       const newUsers = filter(listOfUsers, user => user.id !== currentUser.id)
       set(status.emojiReactions, emoji, newUsers)
-      set(status, 'reactedWith', emoji)
+      set(status, 'reactedWithEmoji', status.reactedWithEmoji.filter(e => e !== emoji))
     }
   },
   updateStatusWithPoll (state, { id, poll }) {

From a018ea622c4ae34fd204e840b20aba53f84cd051 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Sun, 26 Jan 2020 15:45:12 +0200
Subject: [PATCH 05/11] change emoji reactions to use new format

---
 src/components/conversation/conversation.js   |  2 +-
 .../emoji_reactions/emoji_reactions.js        |  9 ++-
 .../emoji_reactions/emoji_reactions.vue       | 12 ++--
 src/components/status/status.vue              |  1 -
 src/modules/statuses.js                       | 66 +++++++++++--------
 .../entity_normalizer.service.js              |  1 +
 test/unit/specs/modules/statuses.spec.js      | 45 +++++++++++++
 7 files changed, 99 insertions(+), 37 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 7ff0ac08..45fb2bf6 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -150,7 +150,7 @@ const conversation = {
       if (!id) return
       this.highlight = id
       this.$store.dispatch('fetchFavsAndRepeats', id)
-      this.$store.dispatch('fetchEmojiReactions', id)
+      this.$store.dispatch('fetchEmojiReactionsBy', id)
     },
     getHighlight () {
       return this.isExpanded ? this.highlight : null
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index e81e6e25..b37cce3d 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -4,12 +4,17 @@ const EmojiReactions = {
   props: ['status'],
   computed: {
     emojiReactions () {
-      return this.status.emojiReactions
+      console.log(this.status.emoji_reactions)
+      return this.status.emoji_reactions
     }
   },
   methods: {
     reactedWith (emoji) {
-      return this.status.reactedWithEmoji.includes(emoji)
+      // return []
+      const user = this.$store.state.users.currentUser
+      const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
+      console.log(reaction)
+      return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
     },
     reactWith (emoji) {
       this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index d83f60b6..8a229240 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,14 +1,14 @@
 <template>
   <div class="emoji-reactions">
     <button
-      v-for="(users, emoji) in emojiReactions"
-      :key="emoji"
+      v-for="(reaction) in emojiReactions"
+      :key="reaction.emoji"
       class="emoji-reaction btn btn-default"
-      :class="{ 'picked-reaction': reactedWith(emoji) }"
-      @click="emojiOnClick(emoji, $event)"
+      :class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
+      @click="emojiOnClick(reaction.emoji, $event)"
     >
-      <span v-if="users">{{ users.length }}</span>
-      <span>{{ emoji }}</span>
+      <span>{{ reaction.count }}</span>
+      <span>{{ reaction.emoji }}</span>
     </button>
   </div>
 </template>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 87e8b5da..d5739304 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -355,7 +355,6 @@
           </transition>
 
           <EmojiReactions
-            v-if="isFocused"
             :status="status"
           />
 
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index dbae9d38..ea0c1749 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -10,10 +10,7 @@ import {
   first,
   last,
   isArray,
-  omitBy,
-  flow,
-  filter,
-  keys
+  omitBy
 } from 'lodash'
 import { set } from 'vue'
 import apiService from '../services/api/api.service.js'
@@ -534,33 +531,48 @@ export const mutations = {
     newStatus.fave_num = newStatus.favoritedBy.length
     newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
   },
-  addEmojiReactions (state, { id, emojiReactions, currentUser }) {
+  addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
     const status = state.allStatusesObject[id]
-    set(status, 'emojiReactions', emojiReactions)
-    const reactedWithEmoji = flow(
-      keys,
-      filter(reaction => find(reaction, { id: currentUser.id }))
-    )(emojiReactions)
-    set(status, 'reactedWithEmoji', reactedWithEmoji)
+    set(status, 'emoji_reactions', emojiReactions)
   },
   addOwnReaction (state, { id, emoji, currentUser }) {
     const status = state.allStatusesObject[id]
-    status.emojiReactions = status.emojiReactions || {}
-    const listOfUsers = (status.emojiReactions && status.emojiReactions[emoji]) || []
-    const hasSelfAlready = !!find(listOfUsers, { id: currentUser.id })
-    if (!hasSelfAlready) {
-      set(status.emojiReactions, emoji, listOfUsers.concat([{ id: currentUser.id }]))
-      set(status, 'reactedWithEmoji', [...status.reactedWithEmoji, emoji])
+    const reactionIndex = findIndex(status.emoji_reactions, { emoji })
+    const reaction = status.emoji_reactions[reactionIndex] || { emoji, count: 0, accounts: [] }
+
+    const newReaction = {
+      ...reaction,
+      count: reaction.count + 1,
+      accounts: [
+        ...reaction.accounts,
+        currentUser
+      ]
+    }
+
+    // Update count of existing reaction if it exists, otherwise append at the end
+    if (reactionIndex >= 0) {
+      set(status.emoji_reactions, reactionIndex, newReaction)
+    } else {
+      set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
     }
   },
   removeOwnReaction (state, { id, emoji, currentUser }) {
     const status = state.allStatusesObject[id]
-    const listOfUsers = status.emojiReactions[emoji] || []
-    const hasSelfAlready = !!find(listOfUsers, { id: currentUser.id })
-    if (hasSelfAlready) {
-      const newUsers = filter(listOfUsers, user => user.id !== currentUser.id)
-      set(status.emojiReactions, emoji, newUsers)
-      set(status, 'reactedWithEmoji', status.reactedWithEmoji.filter(e => e !== emoji))
+    const reactionIndex = findIndex(status.emoji_reactions, { emoji })
+    if (reactionIndex < 0) return
+
+    const reaction = status.emoji_reactions[reactionIndex]
+
+    const newReaction = {
+      ...reaction,
+      count: reaction.count - 1,
+      accounts: reaction.accounts.filter(acc => acc.id === currentUser.id)
+    }
+
+    if (newReaction.count > 0) {
+      set(status.emoji_reactions, reactionIndex, newReaction)
+    } else {
+      set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.emoji !== emoji))
     }
   },
   updateStatusWithPoll (state, { id, poll }) {
@@ -672,7 +684,7 @@ const statuses = {
       commit('addOwnReaction', { id, emoji, currentUser })
       rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
         status => {
-          dispatch('fetchEmojiReactions', id)
+          dispatch('fetchEmojiReactionsBy', id)
         }
       )
     },
@@ -681,14 +693,14 @@ const statuses = {
       commit('removeOwnReaction', { id, emoji, currentUser })
       rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
         status => {
-          dispatch('fetchEmojiReactions', id)
+          dispatch('fetchEmojiReactionsBy', id)
         }
       )
     },
-    fetchEmojiReactions ({ rootState, commit }, id) {
+    fetchEmojiReactionsBy ({ rootState, commit }, id) {
       rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
         emojiReactions => {
-          commit('addEmojiReactions', { id, emojiReactions, currentUser: rootState.users.currentUser })
+          commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
         }
       )
     },
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ee007bee..03eaa5d7 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -233,6 +233,7 @@ export const parseStatus = (data) => {
     output.statusnet_html = addEmojis(data.content, data.emojis)
 
     output.tags = data.tags
+    output.emoji_reactions = [{ emoji: 'A', count: 5 }] // data.pleroma.emoji_reactions
 
     if (data.pleroma) {
       const { pleroma } = data
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index f794997b..e53aa388 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -241,6 +241,51 @@ describe('Statuses module', () => {
     })
   })
 
+  describe('emojiReactions', () => {
+    it('increments count in existing reaction', () => {
+      const state = defaultState()
+      const status = makeMockStatus({ id: '1' })
+      status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ]
+
+      mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+      mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+      expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
+      expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+    })
+
+    it('adds a new reaction', () => {
+      const state = defaultState()
+      const status = makeMockStatus({ id: '1' })
+      status.emoji_reactions = []
+
+      mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+      mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+      expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+      expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+    })
+
+    it('decreases count in existing reaction', () => {
+      const state = defaultState()
+      const status = makeMockStatus({ id: '1' })
+      status.emoji_reactions = [ { emoji: '😂', count: 2, accounts: [{ id: 'me' }] } ]
+
+      mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+      mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+      expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+      expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([])
+    })
+
+    it('removes a reaction', () => {
+      const state = defaultState()
+      const status = makeMockStatus({ id: '1' })
+      status.emoji_reactions = [{ emoji: '😂', count: 1, accounts: [{ id: 'me' }] }]
+
+      mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+      mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+      expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
+    })
+  })
+
   describe('showNewStatuses', () => {
     it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
       const state = defaultState()

From 7cfe1b05e8d16fcbb6eab3b42f19e464d57ea35b Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Sun, 26 Jan 2020 15:57:40 +0200
Subject: [PATCH 06/11] remove mock data

---
 src/services/entity_normalizer/entity_normalizer.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 03eaa5d7..f66d09ac 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -233,7 +233,7 @@ export const parseStatus = (data) => {
     output.statusnet_html = addEmojis(data.content, data.emojis)
 
     output.tags = data.tags
-    output.emoji_reactions = [{ emoji: 'A', count: 5 }] // data.pleroma.emoji_reactions
+    output.emoji_reactions = data.pleroma.emoji_reactions
 
     if (data.pleroma) {
       const { pleroma } = data

From 0de627baae53d4d284920c1f6d7daf64769be4a6 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Sun, 26 Jan 2020 16:18:57 +0200
Subject: [PATCH 07/11] remove favs count from react button

---
 src/components/react_button/react_button.vue | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index ae975dee..7f1bc492 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -44,7 +44,6 @@
         class="button-icon add-reaction-button"
         :title="$t('tool_tip.add_reaction')"
       />
-      <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
     </div>
   </v-popover>
 </template>

From e4e3a28838f431872ab5fd6b10bb8db4a03af389 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Mon, 27 Jan 2020 15:49:05 +0200
Subject: [PATCH 08/11] remove logs/commented code

---
 src/components/emoji_reactions/emoji_reactions.js | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index b37cce3d..95d52cb6 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -4,16 +4,13 @@ const EmojiReactions = {
   props: ['status'],
   computed: {
     emojiReactions () {
-      console.log(this.status.emoji_reactions)
       return this.status.emoji_reactions
     }
   },
   methods: {
     reactedWith (emoji) {
-      // return []
       const user = this.$store.state.users.currentUser
       const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
-      console.log(reaction)
       return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
     },
     reactWith (emoji) {

From cb205036f931e143726790cbc3292e1b53f435ce Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 27 Jan 2020 14:18:15 +0000
Subject: [PATCH 09/11] Apply suggestion to src/services/api/api.service.js

---
 src/services/api/api.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index aa31f123..11aa0675 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -894,7 +894,7 @@ const reactWithEmoji = ({ id, emoji, credentials }) => {
     method: 'POST',
     credentials,
     payload: { emoji }
-  }).then(status => parseStatus(status))
+  }).then(parseStatus)
 }
 
 const unreactWithEmoji = ({ id, emoji, credentials }) => {

From e6291e4ee179ab85f212b1eef7d9e03565e6a8f8 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Mon, 27 Jan 2020 18:43:26 +0200
Subject: [PATCH 10/11] remove unnecessary anonymous function

---
 src/services/api/api.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index aa31f123..11aa0675 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -894,7 +894,7 @@ const reactWithEmoji = ({ id, emoji, credentials }) => {
     method: 'POST',
     credentials,
     payload: { emoji }
-  }).then(status => parseStatus(status))
+  }).then(parseStatus)
 }
 
 const unreactWithEmoji = ({ id, emoji, credentials }) => {

From 6afff4f8c205ec70d3694564c706f6a46a61db9e Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Tue, 28 Jan 2020 17:09:25 +0200
Subject: [PATCH 11/11] review changes

---
 src/components/emoji_reactions/emoji_reactions.vue       | 6 +++---
 src/components/react_button/react_button.js              | 9 +--------
 src/components/react_button/react_button.vue             | 3 +--
 .../entity_normalizer/entity_normalizer.service.js       | 2 +-
 4 files changed, 6 insertions(+), 14 deletions(-)

diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 8a229240..741fc11e 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -7,8 +7,8 @@
       :class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
       @click="emojiOnClick(reaction.emoji, $event)"
     >
-      <span>{{ reaction.count }}</span>
       <span>{{ reaction.emoji }}</span>
+      <span>{{ reaction.count }}</span>
     </button>
   </div>
 </template>
@@ -31,10 +31,10 @@
   align-items: center;
   justify-content: center;
   box-sizing: border-box;
-  :first-child {
+  &:first-child {
     margin-right: 0.25em;
   }
-  :last-child {
+  &:last-child {
     width: 1.5em;
   }
   &:focus {
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index d1a179bc..6fb2a780 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -4,7 +4,6 @@ const ReactButton = {
   props: ['status', 'loggedIn'],
   data () {
     return {
-      animated: false,
       showTooltip: false,
       filterWord: '',
       popperOptions: {
@@ -29,7 +28,7 @@ const ReactButton = {
   },
   computed: {
     commonEmojis () {
-      return ['💖', '😠', '👀', '😂', '🔥']
+      return ['❤️', '😠', '👀', '😂', '🔥']
     },
     emojis () {
       if (this.filterWord !== '') {
@@ -37,12 +36,6 @@ const ReactButton = {
       }
       return this.$store.state.instance.emoji || []
     },
-    classes () {
-      return {
-        'icon-smile': true,
-        'animate-spin': this.animated
-      }
-    },
     ...mapGetters(['mergedConfig'])
   }
 }
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index 7f1bc492..c925dd71 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -40,8 +40,7 @@
       @click.prevent="openReactionSelect"
     >
       <i
-        :class="classes"
-        class="button-icon add-reaction-button"
+        class="icon-smile button-icon add-reaction-button"
         :title="$t('tool_tip.add_reaction')"
       />
     </div>
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index f66d09ac..a3d0b782 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -233,7 +233,6 @@ export const parseStatus = (data) => {
     output.statusnet_html = addEmojis(data.content, data.emojis)
 
     output.tags = data.tags
-    output.emoji_reactions = data.pleroma.emoji_reactions
 
     if (data.pleroma) {
       const { pleroma } = data
@@ -243,6 +242,7 @@ export const parseStatus = (data) => {
       output.is_local = pleroma.local
       output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
       output.thread_muted = pleroma.thread_muted
+      output.emoji_reactions = pleroma.emoji_reactions
     } else {
       output.text = data.content
       output.summary = data.spoiler_text