From aff6caa4c075b8fc39fd5943ae942aa246e71d79 Mon Sep 17 00:00:00 2001
From: Mergan <mergan@noreply.akkoma>
Date: Wed, 3 Aug 2022 20:56:56 +0000
Subject: [PATCH] Emoji Pack Picker (#102)

Reviewed-on: https://akkoma.dev/AkkomaGang/pleroma-fe/pulls/102
Co-authored-by: Mergan <mergan@noreply.akkoma>
Co-committed-by: Mergan <mergan@noreply.akkoma>
---
 src/components/emoji_picker/emoji_picker.js   | 87 ++++++++++---------
 src/components/emoji_picker/emoji_picker.scss | 34 ++++++--
 src/components/emoji_picker/emoji_picker.vue  |  9 +-
 3 files changed, 79 insertions(+), 51 deletions(-)

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index bd5c2e39..49438a4b 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -6,7 +6,7 @@ import {
   faStickyNote,
   faSmileBeam
 } from '@fortawesome/free-solid-svg-icons'
-import { trim } from 'lodash'
+import { trim, escapeRegExp, startCase } from 'lodash'
 
 library.add(
   faBoxOpen,
@@ -21,23 +21,6 @@ const LOAD_EMOJI_BY = 60
 // When to start loading new batch emoji, in pixels
 const LOAD_EMOJI_MARGIN = 64
 
-const filterByKeyword = (list, keyword = '') => {
-  if (keyword === '') return list
-
-  const keywordLowercase = keyword.toLowerCase()
-  let orderedEmojiList = []
-  for (const emoji of list) {
-    const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
-    if (indexOfKeyword > -1) {
-      if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
-        orderedEmojiList[indexOfKeyword] = []
-      }
-      orderedEmojiList[indexOfKeyword].push(emoji)
-    }
-  }
-  return orderedEmojiList.flat()
-}
-
 const EmojiPicker = {
   props: {
     enableStickerPicker: {
@@ -49,7 +32,7 @@ const EmojiPicker = {
   data () {
     return {
       keyword: '',
-      activeGroup: 'custom',
+      activeGroup: 'standard',
       showingStickers: false,
       groupsScrolledClass: 'scrolled-top',
       keepOpen: false,
@@ -80,13 +63,8 @@ const EmojiPicker = {
       this.triggerLoadMore(target)
     },
     highlight (key) {
-      const ref = this.$refs['group-' + key]
-      const top = ref.offsetTop
       this.setShowStickers(false)
       this.activeGroup = key
-      this.$nextTick(() => {
-        this.$refs['emoji-groups'].scrollTop = top + 1
-      })
     },
     updateScrolledClass (target) {
       if (target.scrollTop <= 5) {
@@ -155,6 +133,13 @@ const EmojiPicker = {
     },
     setShowStickers (value) {
       this.showingStickers = value
+    },
+    filterByKeyword (list) {
+      if (this.keyword === '') return list
+      const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
+      return list.filter(emoji => {
+        return regex.test(emoji.displayText)
+      })
     }
   },
   watch: {
@@ -175,9 +160,8 @@ const EmojiPicker = {
       return 0
     },
     filteredEmoji () {
-      return filterByKeyword(
-        this.$store.state.instance.customEmoji || [],
-        trim(this.keyword)
+      return this.filterByKeyword(
+        this.$store.state.instance.customEmoji || []
       )
     },
     customEmojiBuffer () {
@@ -185,25 +169,50 @@ const EmojiPicker = {
     },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
-      const customEmojis = this.customEmojiBuffer
-
+      const customEmojis = this.sortedEmoji
+      const emojiPacks = []
+      customEmojis.forEach((pack, id) => {
+        emojiPacks.push({
+          id: id.replace(/^pack:/, ''),
+          text: startCase(id.replace(/^pack:/, '')),
+          first: pack[0],
+          emojis: this.filterByKeyword(pack)
+        })
+      })
       return [
-        {
-          id: 'custom',
-          text: this.$t('emoji.custom'),
-          icon: 'smile-beam',
-          emojis: customEmojis
-        },
         {
           id: 'standard',
           text: this.$t('emoji.unicode'),
-          icon: 'box-open',
-          emojis: filterByKeyword(standardEmojis, trim(this.keyword))
+          first: {
+            imageUrl: '',
+            replacement: '🥴'
+          },
+          emojis: this.filterByKeyword(standardEmojis)
         }
-      ]
+      ].concat(emojiPacks)
+    },
+    sortedEmoji () {
+      const customEmojis = this.$store.state.instance.customEmoji || []
+      const sortedEmojiGroups = new Map()
+      customEmojis.forEach((emoji) => {
+        if (!sortedEmojiGroups.has(emoji.tags[0])) {
+          sortedEmojiGroups.set(emoji.tags[0], [emoji])
+        } else {
+          sortedEmojiGroups.get(emoji.tags[0]).push(emoji)
+        }
+      })
+      return new Map([...sortedEmojiGroups.entries()].sort())
     },
     emojisView () {
-      return this.emojis.filter(value => value.emojis.length > 0)
+      if (this.keyword === '') {
+        return this.emojis.filter(pack => {
+          return pack.id === this.activeGroup
+        })
+      } else {
+        return this.emojis.filter(pack => {
+          return pack.emojis.length > 0
+        })
+      }
     },
     stickerPickerEnabled () {
       return (this.$store.state.instance.stickers || []).length !== 0
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 2055e02e..9c814e15 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -35,9 +35,8 @@
   }
 
   .heading {
-    display: flex;
-    height: 32px;
-    padding: 10px 7px 5px;
+    margin-top: 10px;
+    height: 4.8em;
   }
 
   .content {
@@ -65,15 +64,34 @@
 
   .additional-tabs,
   .emoji-tabs {
+    position: absolute;
     display: block;
-    min-width: 0;
-    flex-basis: auto;
-    flex-shrink: 1;
+    flex-wrap: nowrap;
 
+    overflow: auto;
+    width: 100%;
+
+    white-space: nowrap;
+    
     &-item {
-      padding: 0 7px;
+      vertical-align: top;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 32px;
+      height: 32px;
+      padding: .4em;
       cursor: pointer;
-      font-size: 1.85em;
+
+      img {
+        max-width: 100%;
+        max-height: 100%;
+        object-fit: contain;
+      }
+
+      span {
+        font-size: 1.9em;
+      }
 
       &.disabled {
         opacity: 0.5;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index a7269120..408048d2 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -13,10 +13,11 @@
           :title="group.text"
           @click.prevent="highlight(group.id)"
         >
-          <FAIcon
-            :icon="group.icon"
-            fixed-width
-          />
+          <span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
+          <img
+            v-else
+            :src="group.first.imageUrl"
+          >
         </span>
       </span>
       <span