From f6b482be515ea4f0281050f71296ffe0ec2ab305 Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Tue, 11 Feb 2020 12:24:51 +0000
Subject: [PATCH] Emoji Reactions - fixes and improvements

---
 src/App.scss                                  |   2 +-
 src/_variables.scss                           |   2 +
 .../emoji_reactions/emoji_reactions.js        |  48 +++++++-
 .../emoji_reactions/emoji_reactions.vue       | 103 ++++++++++++++++--
 src/components/notification/notification.vue  |   7 ++
 .../notifications/notifications.scss          |   4 +
 src/components/react_button/react_button.js   |   7 +-
 src/components/react_button/react_button.vue  |   4 +
 src/components/settings/settings.vue          |  10 ++
 src/components/status/status.vue              |   1 +
 src/i18n/en.json                              |   5 +-
 src/i18n/fi.json                              |   5 +-
 src/modules/config.js                         |   4 +-
 src/modules/statuses.js                       |  36 ++++--
 src/services/api/api.service.js               |  28 ++---
 .../entity_normalizer.service.js              |   1 +
 .../notification_utils/notification_utils.js  |   3 +-
 test/unit/specs/modules/statuses.spec.js      |  13 ++-
 18 files changed, 236 insertions(+), 47 deletions(-)

diff --git a/src/App.scss b/src/App.scss
index 754ca62e..922e39b6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -75,7 +75,7 @@ button {
   border-radius: $fallback--btnRadius;
   border-radius: var(--btnRadius, $fallback--btnRadius);
   cursor: pointer;
-  box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+  box-shadow: $fallback--buttonShadow;
   box-shadow: var(--buttonShadow);
   font-size: 14px;
   font-family: sans-serif;
diff --git a/src/_variables.scss b/src/_variables.scss
index e18101f0..30dc3e42 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px;
 $fallback--avatarRadius: 4px;
 $fallback--avatarAltRadius: 10px;
 $fallback--attachmentRadius: 10px;
+
+$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index 95d52cb6..b799ac9a 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -1,17 +1,55 @@
+import UserAvatar from '../user_avatar/user_avatar.vue'
+
+const EMOJI_REACTION_COUNT_CUTOFF = 12
 
 const EmojiReactions = {
   name: 'EmojiReactions',
+  components: {
+    UserAvatar
+  },
   props: ['status'],
+  data: () => ({
+    showAll: false,
+    popperOptions: {
+      modifiers: {
+        preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
+      }
+    }
+  }),
   computed: {
+    tooManyReactions () {
+      return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
+    },
     emojiReactions () {
-      return this.status.emoji_reactions
+      return this.showAll
+        ? this.status.emoji_reactions
+        : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
+    },
+    showMoreString () {
+      return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
+    },
+    accountsForEmoji () {
+      return this.status.emoji_reactions.reduce((acc, reaction) => {
+        acc[reaction.name] = reaction.accounts || []
+        return acc
+      }, {})
+    },
+    loggedIn () {
+      return !!this.$store.state.users.currentUser
     }
   },
   methods: {
+    toggleShowAll () {
+      this.showAll = !this.showAll
+    },
     reactedWith (emoji) {
-      const user = this.$store.state.users.currentUser
-      const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
-      return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
+      return this.status.emoji_reactions.find(r => r.name === emoji).me
+    },
+    fetchEmojiReactionsByIfMissing () {
+      const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
+      if (hasNoAccounts) {
+        this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
+      }
     },
     reactWith (emoji) {
       this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
@@ -20,6 +58,8 @@ const EmojiReactions = {
       this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
     },
     emojiOnClick (emoji, event) {
+      if (!this.loggedIn) return
+
       if (this.reactedWith(emoji)) {
         this.unreact(emoji)
       } else {
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 00d6d2b7..e5b6d9f5 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,16 +1,58 @@
 <template>
   <div class="emoji-reactions">
-    <button
+    <v-popover
       v-for="(reaction) in emojiReactions"
-      :key="reaction.emoji"
-      class="emoji-reaction btn btn-default"
-      :class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
-      @click="emojiOnClick(reaction.emoji, $event)"
+      :key="reaction.name"
+      :popper-options="popperOptions"
+      trigger="hover"
+      placement="top"
     >
-      <span class="reaction-emoji">{{ reaction.emoji }}</span>
-      <span>{{ reaction.count }}</span>
-    </button>
+
+      <div
+        slot="popover"
+        class="reacted-users"
+      >
+        <div v-if="accountsForEmoji[reaction.name].length">
+          <div
+            v-for="(account) in accountsForEmoji[reaction.name]"
+            :key="account.id"
+            class="reacted-user"
+          >
+            <UserAvatar
+              :user="account"
+              class="avatar-small"
+              :compact="true"
+            />
+            <div class="reacted-user-names">
+              <span class="reacted-user-name" v-html="account.name_html" />
+              <span class="reacted-user-screen-name">{{ account.screen_name }}</span>
+            </div>
+          </div>
+        </div>
+        <div v-else>
+          <i class="icon-spin4 animate-spin" />
+        </div>
+      </div>
+      <button
+        class="emoji-reaction btn btn-default"
+        :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
+        @click="emojiOnClick(reaction.name, $event)"
+        @mouseenter="fetchEmojiReactionsByIfMissing()"
+      >
+        <span class="reaction-emoji">{{ reaction.name }}</span>
+        <span>{{ reaction.count }}</span>
+      </button>
+    </v-popover>
+    <a
+        v-if="tooManyReactions"
+        @click="toggleShowAll"
+        class="emoji-reaction-expand faint"
+        href='javascript:void(0)'
+      >
+        {{ showAll ? $t('general.show_less') : showMoreString }}
+      </a>
   </div>
+
 </template>
 
 <script src="./emoji_reactions.js" ></script>
@@ -23,6 +65,31 @@
   flex-wrap: wrap;
 }
 
+.reacted-users {
+  padding: 0.5em;
+}
+
+.reacted-user {
+  padding: 0.25em;
+  display: flex;
+  flex-direction: row;
+
+  .reacted-user-names {
+    display: flex;
+    flex-direction: column;
+    margin-left: 0.5em;
+
+    img {
+      width: 1em;
+      height: 1em;
+    }
+  }
+
+  .reacted-user-screen-name {
+    font-size: 9px;
+  }
+}
+
 .emoji-reaction {
   padding: 0 0.5em;
   margin-right: 0.5em;
@@ -38,6 +105,26 @@
   &:focus {
     outline: none;
   }
+
+  &.not-clickable {
+    cursor: default;
+    &:hover {
+      box-shadow: $fallback--buttonShadow;
+      box-shadow: var(--buttonShadow);
+    }
+  }
+}
+
+.emoji-reaction-expand {
+  padding: 0 0.5em;
+  margin-right: 0.5em;
+  margin-top: 0.5em;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  &:hover {
+    text-decoration: underline;
+  }
 }
 
 .picked-reaction {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 16124e50..411c0271 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -78,6 +78,13 @@
               <i class="fa icon-arrow-curved lit" />
               <small>{{ $t('notifications.migrated_to') }}</small>
             </span>
+            <span v-if="notification.type === 'pleroma:emoji_reaction'">
+              <small>
+                <i18n path="notifications.reacted_with">
+                  <span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
+                </i18n>
+              </small>
+            </span>
           </div>
           <div
             v-if="notification.type === 'follow' || notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 148ac7f2..8d819a56 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -94,6 +94,10 @@
     min-width: 0;
   }
 
+  .emoji-reaction-emoji {
+    font-size: 16px;
+  }
+
   .notification-details {
     min-width: 0px;
     word-wrap: break-word;
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 6fb2a780..a6cf5b94 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -22,7 +22,12 @@ const ReactButton = {
       this.showTooltip = false
     },
     addReaction (event, emoji) {
-      this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+      const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
+      if (existingReaction && existingReaction.me) {
+        this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+      } else {
+        this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+      }
       this.closeReactionSelect()
     }
   },
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index c925dd71..fb43ebaf 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -54,6 +54,10 @@
 
 .reaction-picker-filter {
   padding: 0.5em;
+  display: flex;
+  input {
+    flex: 1;
+  }
 }
 
 .reaction-picker-divider {
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index cef492f3..60cb8a87 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -92,6 +92,11 @@
                     {{ $t('settings.reply_link_preview') }}
                   </Checkbox>
                 </li>
+                <li>
+                  <Checkbox v-model="emojiReactionsOnTimeline">
+                    {{ $t('settings.emoji_reactions_on_timeline') }}
+                  </Checkbox>
+                </li>
               </ul>
             </div>
 
@@ -328,6 +333,11 @@
                       {{ $t('settings.notification_visibility_moves') }}
                     </Checkbox>
                   </li>
+                  <li>
+                    <Checkbox v-model="notificationVisibility.emojiReactions">
+                      {{ $t('settings.notification_visibility_emoji_reactions') }}
+                    </Checkbox>
+                  </li>
                 </ul>
               </div>
               <div>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 0a82dcbe..83f07dac 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -369,6 +369,7 @@
           </transition>
 
           <EmojiReactions
+            v-if="(mergedConfig.emojiReactionsOnTimeline || isFocused) && (!noHeading && !isPreview)"
             :status="status"
           />
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 74e71fc8..d0d654d3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -126,7 +126,8 @@
     "read": "Read!",
     "repeated_you": "repeated your status",
     "no_more_notifications": "No more notifications",
-    "migrated_to": "migrated to"
+    "migrated_to": "migrated to",
+    "reacted_with": "reacted with {0}"
   },
   "polls": {
     "add_poll": "Add Poll",
@@ -283,6 +284,7 @@
     "domain_mutes": "Domains",
     "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
     "pad_emoji": "Pad emoji with spaces when adding from picker",
+    "emoji_reactions_on_timeline": "Show emoji reactions on timeline",
     "export_theme": "Save preset",
     "filtering": "Filtering",
     "filtering_explanation": "All statuses containing these words will be muted, one per line",
@@ -331,6 +333,7 @@
     "notification_visibility_mentions": "Mentions",
     "notification_visibility_repeats": "Repeats",
     "notification_visibility_moves": "User Migrates",
+    "notification_visibility_emoji_reactions": "Reactions",
     "no_rich_text_description": "Strip rich text formatting from all posts",
     "no_blocks": "No blocks",
     "no_mutes": "No mutes",
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index e7ed5408..ac8b2ac9 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -53,7 +53,8 @@
     "notifications": "Ilmoitukset",
     "read": "Lue!",
     "repeated_you": "toisti viestisi",
-    "no_more_notifications": "Ei enempää ilmoituksia"
+    "no_more_notifications": "Ei enempää ilmoituksia",
+    "reacted_with": "lisäsi reaktion {0}"
   },
   "polls": {
     "add_poll": "Lisää äänestys",
@@ -140,6 +141,7 @@
     "delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
     "delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
     "delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
+    "emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
     "export_theme": "Tallenna teema",
     "filtering": "Suodatus",
     "filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
@@ -183,6 +185,7 @@
     "notification_visibility_likes": "Tykkäykset",
     "notification_visibility_mentions": "Maininnat",
     "notification_visibility_repeats": "Toistot",
+    "notification_visibility_emoji_reactions": "Reaktiot",
     "no_rich_text_description": "Älä näytä tekstin muotoilua.",
     "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
     "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
diff --git a/src/modules/config.js b/src/modules/config.js
index de9f041b..8381fa53 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -20,6 +20,7 @@ export const defaultState = {
   autoLoad: true,
   streaming: false,
   hoverPreview: true,
+  emojiReactionsOnTimeline: true,
   autohideFloatingPostButton: false,
   pauseOnUnfocused: true,
   stopGifs: false,
@@ -29,7 +30,8 @@ export const defaultState = {
     mentions: true,
     likes: true,
     repeats: true,
-    moves: true
+    moves: true,
+    emojiReactions: false
   },
   webPushNotifications: false,
   muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index ea0c1749..25b62ac7 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -81,7 +81,8 @@ const visibleNotificationTypes = (rootState) => {
     rootState.config.notificationVisibility.mentions && 'mention',
     rootState.config.notificationVisibility.repeats && 'repeat',
     rootState.config.notificationVisibility.follows && 'follow',
-    rootState.config.notificationVisibility.moves && 'move'
+    rootState.config.notificationVisibility.moves && 'move',
+    rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
   ].filter(_ => _)
 }
 
@@ -325,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
       notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
     }
 
+    if (notification.type === 'pleroma:emoji_reaction') {
+      dispatch('fetchEmojiReactionsBy', notification.status.id)
+    }
+
     // Only add a new notification if we don't have one for the same action
     if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
       state.notifications.maxId = notification.id > state.notifications.maxId
@@ -358,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
             break
         }
 
-        if (i18nString) {
+        if (notification.type === 'pleroma:emoji_reaction') {
+          notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
+        } else if (i18nString) {
           notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
         } else {
           notifObj.body = notification.status.text
@@ -371,10 +378,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
         }
 
         if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
-          let notification = new window.Notification(title, notifObj)
+          let desktopNotification = new window.Notification(title, notifObj)
           // Chrome is known for not closing notifications automatically
           // according to MDN, anyway.
-          setTimeout(notification.close.bind(notification), 5000)
+          setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
         }
       }
     } else if (notification.seen) {
@@ -537,12 +544,13 @@ export const mutations = {
   },
   addOwnReaction (state, { id, emoji, currentUser }) {
     const status = state.allStatusesObject[id]
-    const reactionIndex = findIndex(status.emoji_reactions, { emoji })
-    const reaction = status.emoji_reactions[reactionIndex] || { emoji, count: 0, accounts: [] }
+    const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
+    const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] }
 
     const newReaction = {
       ...reaction,
       count: reaction.count + 1,
+      me: true,
       accounts: [
         ...reaction.accounts,
         currentUser
@@ -558,21 +566,23 @@ export const mutations = {
   },
   removeOwnReaction (state, { id, emoji, currentUser }) {
     const status = state.allStatusesObject[id]
-    const reactionIndex = findIndex(status.emoji_reactions, { emoji })
+    const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
     if (reactionIndex < 0) return
 
     const reaction = status.emoji_reactions[reactionIndex]
+    const accounts = reaction.accounts || []
 
     const newReaction = {
       ...reaction,
       count: reaction.count - 1,
-      accounts: reaction.accounts.filter(acc => acc.id === currentUser.id)
+      me: false,
+      accounts: 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))
+      set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
     }
   },
   updateStatusWithPoll (state, { id, poll }) {
@@ -681,18 +691,22 @@ const statuses = {
     },
     reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
       const currentUser = rootState.users.currentUser
+      if (!currentUser) return
+
       commit('addOwnReaction', { id, emoji, currentUser })
       rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
-        status => {
+        ok => {
           dispatch('fetchEmojiReactionsBy', id)
         }
       )
     },
     unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
       const currentUser = rootState.users.currentUser
+      if (!currentUser) return
+
       commit('removeOwnReaction', { id, emoji, currentUser })
       rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
-        status => {
+        ok => {
           dispatch('fetchEmojiReactionsBy', id)
         }
       )
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index b794fd58..20eaa9a0 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -74,9 +74,9 @@ const MASTODON_SEARCH_2 = `/api/v2/search`
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
 const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_STREAMING = '/api/v1/streaming'
-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 PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
+const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 
 const oldfetch = window.fetch
 
@@ -888,25 +888,27 @@ 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 fetchEmojiReactions = ({ id, credentials }) => {
+  return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials })
+    .then((reactions) => reactions.map(r => {
+      r.accounts = r.accounts.map(parseUser)
+      return r
+    }))
 }
 
 const reactWithEmoji = ({ id, emoji, credentials }) => {
   return promisedRequest({
-    url: PLEROMA_EMOJI_REACT_URL(id),
-    method: 'POST',
-    credentials,
-    payload: { emoji }
+    url: PLEROMA_EMOJI_REACT_URL(id, emoji),
+    method: 'PUT',
+    credentials
   }).then(parseStatus)
 }
 
 const unreactWithEmoji = ({ id, emoji, credentials }) => {
   return promisedRequest({
-    url: PLEROMA_EMOJI_UNREACT_URL(id),
-    method: 'POST',
-    credentials,
-    payload: { emoji }
+    url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
+    method: 'DELETE',
+    credentials
   }).then(parseStatus)
 }
 
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 0a8abbbd..84169a7b 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -354,6 +354,7 @@ export const parseNotification = (data) => {
       ? null
       : parseUser(data.target)
     output.from_profile = parseUser(data.account)
+    output.emoji = data.emoji
   } else {
     const parsedNotice = parseStatus(data.notice)
     output.type = data.ntype
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 860620fc..b17bd7bf 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -7,7 +7,8 @@ export const visibleTypes = store => ([
   store.state.config.notificationVisibility.mentions && 'mention',
   store.state.config.notificationVisibility.repeats && 'repeat',
   store.state.config.notificationVisibility.follows && 'follow',
-  store.state.config.notificationVisibility.moves && 'move'
+  store.state.config.notificationVisibility.moves && 'move',
+  store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
 ].filter(_ => _))
 
 const sortById = (a, b) => {
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index e53aa388..fe42e85b 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -245,11 +245,12 @@ describe('Statuses module', () => {
     it('increments count in existing reaction', () => {
       const state = defaultState()
       const status = makeMockStatus({ id: '1' })
-      status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ]
+      status.emoji_reactions = [ { name: '😂', 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].me).to.eql(true)
       expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
     })
 
@@ -261,27 +262,29 @@ describe('Statuses module', () => {
       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].me).to.eql(true)
       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' }] } ]
+      status.emoji_reactions = [ { name: '😂', count: 2, accounts: [{ id: 'me' }] } ]
 
       mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
-      mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+      mutations.removeOwnReaction(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].me).to.eql(false)
       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' }] }]
+      status.emoji_reactions = [{ name: '😂', count: 1, accounts: [{ id: 'me' }] }]
 
       mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
-      mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+      mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
       expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
     })
   })