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: add fetching for emoji reactions, draft design

---
 src/components/status/status.vue | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

(limited to 'src/components/status/status.vue')

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 {
-- 
cgit v1.2.3-70-g09d2


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: 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

(limited to 'src/components/status/status.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":{
-- 
cgit v1.2.3-70-g09d2


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: 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(-)

(limited to 'src/components/status/status.vue')

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,
-- 
cgit v1.2.3-70-g09d2


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: clean up code, fix prediction bug

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

(limited to 'src/components/status/status.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;
+
+    flex-basis: 20%;
+    line-height: 1.5em;
+    align-content: center;
+
+    &:hover {
+      transform: scale(1.25);
+    }
+  }
 }
 
-.fav-active {
+.add-reaction-button {
   cursor: pointer;
-  animation-duration: 0.6s;
 
   &:hover {
-    color: $fallback--cOrange;
-    color: var(--cOrange, $fallback--cOrange);
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
   }
 }
 
-.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 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 }) {
-- 
cgit v1.2.3-70-g09d2


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: change emoji reactions to use new format

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

(limited to 'src/components/status/status.vue')

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()
-- 
cgit v1.2.3-70-g09d2

