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] 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,