From 9c8738ff22a0563b16b19e57e8042c65c4a0b7e2 Mon Sep 17 00:00:00 2001
From: Lain Soykaf <lain@lain.com>
Date: Sun, 12 Jun 2022 13:38:12 +0200
Subject: EmojiPicker: Workaround to search immediately on mobile

See https://github.com/vuejs/vue/pull/9814
---
 src/components/emoji_picker/emoji_picker.vue | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 3262a3d9..a7269120 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -47,6 +47,7 @@
             type="text"
             class="form-control"
             :placeholder="$t('emoji.search_emoji')"
+            @input="$event.target.composing = false"
           >
         </div>
         <div
-- 
cgit v1.2.3-70-g09d2


From 69b3102fb2396edb63abe98b4a69ebe311e22a70 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 14 Aug 2021 21:10:24 -0400
Subject: Group custom emojis by pack in emoji picker

---
 src/components/emoji_picker/emoji_picker.js   | 29 ++++++++++++++++++++-------
 src/components/emoji_picker/emoji_picker.scss | 13 ++++++++++++
 src/components/emoji_picker/emoji_picker.vue  | 10 +++++++++
 3 files changed, 45 insertions(+), 7 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index f6920208..9e398176 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -183,17 +183,32 @@ const EmojiPicker = {
     customEmojiBuffer () {
       return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
     },
+    groupedCustomEmojis () {
+      const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
+      return this.customEmojiBuffer.reduce((res, emoji) => {
+        const pack = packOf(emoji)
+        if (!res[pack]) {
+          res[pack] = {
+            id: `custom-${pack}`,
+            text: pack,
+            /// FIXME
+            // icon: 'smile-beam',
+            image: emoji.imageUrl,
+            emojis: []
+          }
+        }
+        res[pack].emojis.push(emoji)
+        return res
+      }, {})
+    },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
-      const customEmojis = this.customEmojiBuffer
+      // const customEmojis = this.customEmojiBuffer
 
       return [
-        {
-          id: 'custom',
-          text: this.$t('emoji.custom'),
-          icon: 'smile-beam',
-          emojis: customEmojis
-        },
+        ...Object
+          .keys(this.groupedCustomEmojis)
+          .map(k => this.groupedCustomEmojis[k]),
         {
           id: 'standard',
           text: this.$t('emoji.unicode'),
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index a2f17c51..ccb12a2a 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -19,6 +19,19 @@
   --lightText: var(--popoverLightText, $fallback--lightText);
   --icon: var(--popoverIcon, $fallback--icon);
 
+  &-header-image {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 30px;
+    height: 24px;
+    img {
+      max-width: 100%;
+      max-height: 100%;
+      object-fit: contain;
+    }
+  }
+
   .keep-open,
   .too-many-emoji {
     padding: 7px;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index a7269120..16549c08 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -13,7 +13,17 @@
           :title="group.text"
           @click.prevent="highlight(group.id)"
         >
+          <span
+            v-if="group.image"
+            class="emoji-picker-header-image"
+          >
+            <img
+              :alt="group.text"
+              :src="group.image"
+            >
+          </span>
           <FAIcon
+            v-else
             :icon="group.icon"
             fixed-width
           />
-- 
cgit v1.2.3-70-g09d2


From 992d57ef69540f4c63939fbc5abed9b1ea28ed2f Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 14 Aug 2021 21:50:58 -0400
Subject: Display all emoji groups on emoji picker header

---
 src/components/emoji_picker/emoji_picker.js   | 28 +++++++++++++++++++++++++--
 src/components/emoji_picker/emoji_picker.scss |  6 +++++-
 src/components/emoji_picker/emoji_picker.vue  |  4 ++--
 3 files changed, 33 insertions(+), 5 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 7d5a3d8f..d04649dc 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -38,6 +38,8 @@ const filterByKeyword = (list, keyword = '') => {
   return orderedEmojiList.flat()
 }
 
+const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
+
 const EmojiPicker = {
   props: {
     enableStickerPicker: {
@@ -174,9 +176,12 @@ const EmojiPicker = {
       }
       return 0
     },
+    allEmojis () {
+      return this.$store.state.instance.customEmoji || []
+    },
     filteredEmoji () {
       return filterByKeyword(
-        this.$store.state.instance.customEmoji || [],
+        this.allEmojis,
         trim(this.keyword)
       )
     },
@@ -184,7 +189,6 @@ const EmojiPicker = {
       return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
     },
     groupedCustomEmojis () {
-      const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
       return this.customEmojiBuffer.reduce((res, emoji) => {
         const pack = packOf(emoji)
         if (!res[pack]) {
@@ -201,6 +205,26 @@ const EmojiPicker = {
         return res
       }, {})
     },
+    allEmojiGroups () {
+      return this.allEmojis
+        .reduce((res, emoji) => {
+          const packName = packOf(emoji)
+          const packId = `custom-${packName}`
+          if (res.filter(k => k.id === packId).length === 0) {
+            res.push({
+              id: packId,
+              text: packName,
+              image: emoji.imageUrl
+            })
+          }
+          return res
+        }, [])
+        .concat({
+          id: 'standard',
+          text: this.$t('emoji.unicode'),
+          icon: 'box-open'
+        })
+    },
     emojis () {
       const standardEmojis = this.$store.state.instance.emoji || []
       // const customEmojis = this.customEmojiBuffer
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index ccb12a2a..0bd4363c 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -52,6 +52,7 @@
     display: flex;
     height: 32px;
     padding: 10px 7px 5px;
+    overflow-x: auto;
   }
 
   .content {
@@ -63,6 +64,9 @@
 
   .emoji-tabs {
     flex-grow: 1;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
   }
 
   .emoji-groups {
@@ -70,6 +74,7 @@
   }
 
   .additional-tabs {
+    display: block;
     border-left: 1px solid;
     border-left-color: $fallback--icon;
     border-left-color: var(--icon, $fallback--icon);
@@ -79,7 +84,6 @@
 
   .additional-tabs,
   .emoji-tabs {
-    display: block;
     min-width: 0;
     flex-basis: auto;
     flex-shrink: 1;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 16549c08..fe60cb5e 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,12 +3,12 @@
     <div class="heading">
       <span class="emoji-tabs">
         <span
-          v-for="group in emojis"
+          v-for="group in allEmojiGroups"
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
             active: activeGroupView === group.id,
-            disabled: group.emojis.length === 0
+            disabled: false
           }"
           :title="group.text"
           @click.prevent="highlight(group.id)"
-- 
cgit v1.2.3-70-g09d2


From ba1b006e05ef50c7e85f88c3dbd4eefc66d05806 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 14 Aug 2021 23:37:00 -0400
Subject: Load visible emoji groups when scrolling

---
 src/components/emoji_picker/emoji_picker.js  | 101 +++++++++++++++++++--------
 src/components/emoji_picker/emoji_picker.vue |   4 +-
 2 files changed, 75 insertions(+), 30 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index d04649dc..cab952f8 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -57,7 +57,8 @@ const EmojiPicker = {
       keepOpen: false,
       customEmojiBufferSlice: LOAD_EMOJI_BY,
       customEmojiTimeout: null,
-      customEmojiLoadAllConfirmed: false
+      customEmojiLoadAllConfirmed: false,
+      groupLoadedCount: {}
     }
   },
   components: {
@@ -79,7 +80,9 @@ const EmojiPicker = {
       const target = (e && e.target) || this.$refs['emoji-groups']
       this.updateScrolledClass(target)
       this.scrolledGroup(target)
-      this.triggerLoadMore(target)
+      this.$nextTick(() => {
+        this.triggerLoadMore(target)
+      })
     },
     highlight (key) {
       const ref = this.$refs['group-' + key]
@@ -88,6 +91,7 @@ const EmojiPicker = {
       this.activeGroup = key
       this.$nextTick(() => {
         this.$refs['emoji-groups'].scrollTop = top + 1
+        this.loadEmoji(key)
       })
     },
     updateScrolledClass (target) {
@@ -100,28 +104,40 @@ const EmojiPicker = {
       }
     },
     triggerLoadMore (target) {
-      const ref = this.$refs[`group-end-${this.lastNonUnicodeGroupId}`][0]
-      if (!ref) return
-      const bottom = ref.offsetTop + ref.offsetHeight
+      Object.keys(this.allCustomGroups)
+        .map(groupId => {
+          const ref = this.$refs[`group-end-${groupId}`][0]
+          if (!ref) return undefined
 
-      const scrollerBottom = target.scrollTop + target.clientHeight
-      const scrollerTop = target.scrollTop
-      const scrollerMax = target.scrollHeight
+          const bottom = ref.offsetTop + ref.offsetHeight
 
-      // Loads more emoji when they come into view
-      const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
-      // Always load when at the very top in case there's no scroll space yet
-      const atTop = scrollerTop < 5
-      // Don't load when looking at unicode category or at the very bottom
-      const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
-      if (!bottomAboveViewport && (approachingBottom || atTop)) {
-        this.loadEmoji()
-      }
+          const group = this.$refs[`group-${groupId}`][0]
+          const top = group.offsetTop
+
+          const scrollerBottom = target.scrollTop + target.clientHeight
+          const scrollerTop = target.scrollTop
+          const scrollerMax = target.scrollHeight
+
+          // Loads more emoji when they come into view
+          const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
+          // Always load when at the very top in case there's no scroll space yet
+          const atTop = scrollerTop < top + target.clientHeight / 2 && top < scrollerBottom
+          // Don't load when looking at unicode category or at the very bottom
+          const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
+          if (!bottomAboveViewport && (approachingBottom || atTop)) {
+            return groupId
+          }
+          return undefined
+        })
+        .filter(k => k)
+        .map(k => {
+          this.loadEmoji(k)
+        })
     },
     scrolledGroup (target) {
       const top = target.scrollTop + 5
       this.$nextTick(() => {
-        this.emojisView.forEach(group => {
+        this.allEmojiGroups.forEach(group => {
           const ref = this.$refs['group-' + group.id]
           if (ref.offsetTop <= top) {
             this.activeGroup = group.id
@@ -129,14 +145,21 @@ const EmojiPicker = {
         })
       })
     },
-    loadEmoji () {
-      const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
+    loadEmoji (loadGroup) {
+      if (!this.allCustomGroups[loadGroup]) {
+        return
+      }
+
+      const allLoaded = this.loadedCount[loadGroup] >= this.allCustomGroups[loadGroup].emojis.length
 
       if (allLoaded) {
         return
       }
 
-      this.customEmojiBufferSlice += LOAD_EMOJI_BY
+      this.groupLoadedCount = {
+        ...this.groupLoadedCount,
+        [loadGroup]: this.loadedCount[loadGroup] + LOAD_EMOJI_BY
+      }
     },
     startEmojiLoad (forceUpdate = false) {
       if (!forceUpdate) {
@@ -157,6 +180,9 @@ const EmojiPicker = {
     },
     setShowStickers (value) {
       this.showingStickers = value
+    },
+    limitedEmojis (list, groupId) {
+      return list.slice(0, this.loadedCount[groupId])
     }
   },
   watch: {
@@ -205,24 +231,36 @@ const EmojiPicker = {
         return res
       }, {})
     },
-    allEmojiGroups () {
-      return this.allEmojis
+    allCustomGroups () {
+      return this.filteredEmoji
         .reduce((res, emoji) => {
           const packName = packOf(emoji)
           const packId = `custom-${packName}`
-          if (res.filter(k => k.id === packId).length === 0) {
-            res.push({
+          if (!res[packId]) {
+            res[packId] = ({
               id: packId,
               text: packName,
-              image: emoji.imageUrl
+              image: emoji.imageUrl,
+              emojis: []
             })
           }
+          res[packId].emojis.push(emoji)
           return res
-        }, [])
+        }, {})
+    },
+    sensibleInitialAmountForAGroup () {
+      const groupCount = Object.keys(this.allCustomGroups).length
+      return Math.max(Math.floor(LOAD_EMOJI_BY / Math.max(groupCount, 1)), 1)
+    },
+    allEmojiGroups () {
+      const standardEmojis = this.$store.state.instance.emoji || []
+      return Object.entries(this.allCustomGroups)
+        .map(([_, v]) => v)
         .concat({
           id: 'standard',
           text: this.$t('emoji.unicode'),
-          icon: 'box-open'
+          icon: 'box-open',
+          emojis: filterByKeyword(standardEmojis, this.keyword)
         })
     },
     emojis () {
@@ -241,6 +279,13 @@ const EmojiPicker = {
         }
       ]
     },
+    loadedCount () {
+      return Object.keys(this.allCustomGroups)
+        .reduce((res, groupId) => {
+          res[groupId] = this.groupLoadedCount[groupId] || this.sensibleInitialAmountForAGroup
+          return res
+        }, {})
+    },
     lastNonUnicodeGroupId () {
       return this.emojis[this.emojis.length - 2].id
     },
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index fe60cb5e..277d5bf6 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -67,7 +67,7 @@
           @scroll="onScroll"
         >
           <div
-            v-for="group in emojisView"
+            v-for="group in allEmojiGroups"
             :key="group.id"
             class="emoji-group"
           >
@@ -78,7 +78,7 @@
               {{ group.text }}
             </h6>
             <span
-              v-for="emoji in group.emojis"
+              v-for="emoji in limitedEmojis(group.emojis, group.id)"
               :key="group.id + emoji.displayText"
               :title="emoji.displayText"
               class="emoji-item"
-- 
cgit v1.2.3-70-g09d2


From 123913f34ffd91917a9ed4dd1c9d406fb547ef87 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 15 Aug 2021 00:43:35 -0400
Subject: Optimise emoji picker loading process

---
 src/components/emoji_picker/emoji_picker.js  | 83 +++++-----------------------
 src/components/emoji_picker/emoji_picker.vue |  4 +-
 src/modules/instance.js                      | 18 ++++++
 3 files changed, 34 insertions(+), 71 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 322bb8ba..67c6d0cc 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -106,6 +106,7 @@ const EmojiPicker = {
     },
     triggerLoadMore (target) {
       Object.keys(this.allCustomGroups)
+        .filter(id => this.filteredEmojiGroups.filter(group => group.id === id).length > 0)
         .map(groupId => {
           const ref = this.$refs[`group-end-${groupId}`][0]
           if (!ref) return undefined
@@ -123,9 +124,10 @@ const EmojiPicker = {
           const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
           // Always load when at the very top in case there's no scroll space yet
           const atTop = scrollerTop < top + target.clientHeight / 2 && top < scrollerBottom
+          const unscrollable = top - bottom < target.clientHeight
           // Don't load when looking at unicode category or at the very bottom
           const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
-          if (!bottomAboveViewport && (approachingBottom || atTop)) {
+          if (!bottomAboveViewport && (approachingBottom || atTop || unscrollable)) {
             return groupId
           }
           return undefined
@@ -176,12 +178,6 @@ const EmojiPicker = {
           this.firstLoaded = true
         })
       })
-      const bufferSize = this.customEmojiBuffer.length
-      const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
-      if (bufferPrefilledAll && !forceUpdate) {
-        return
-      }
-      this.customEmojiBufferSlice = LOAD_EMOJI_BY
     },
     toggleStickers () {
       this.showingStickers = !this.showingStickers
@@ -191,6 +187,9 @@ const EmojiPicker = {
     },
     limitedEmojis (list, groupId) {
       return list.slice(0, this.loadedCount[groupId])
+    },
+    filterByKeyword (list, keyword) {
+      return filterByKeyword(list, keyword)
     }
   },
   watch: {
@@ -210,51 +209,8 @@ const EmojiPicker = {
       }
       return 0
     },
-    allEmojis () {
-      return this.$store.state.instance.customEmoji || []
-    },
-    filteredEmoji () {
-      return filterByKeyword(
-        this.allEmojis,
-        trim(this.keyword)
-      )
-    },
-    customEmojiBuffer () {
-      return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
-    },
-    groupedCustomEmojis () {
-      return this.customEmojiBuffer.reduce((res, emoji) => {
-        const pack = packOf(emoji)
-        if (!res[pack]) {
-          res[pack] = {
-            id: `custom-${pack}`,
-            text: pack,
-            /// FIXME
-            // icon: 'smile-beam',
-            image: emoji.imageUrl,
-            emojis: []
-          }
-        }
-        res[pack].emojis.push(emoji)
-        return res
-      }, {})
-    },
     allCustomGroups () {
-      return this.filteredEmoji
-        .reduce((res, emoji) => {
-          const packName = packOf(emoji)
-          const packId = `custom-${packName}`
-          if (!res[packId]) {
-            res[packId] = ({
-              id: packId,
-              text: packName,
-              image: emoji.imageUrl,
-              emojis: []
-            })
-          }
-          res[packId].emojis.push(emoji)
-          return res
-        }, {})
+      return this.$store.getters.groupedCustomEmojis
     },
     sensibleInitialAmountForAGroup () {
       const groupCount = Object.keys(this.allCustomGroups).length
@@ -271,21 +227,13 @@ const EmojiPicker = {
           emojis: filterByKeyword(standardEmojis, this.keyword)
         })
     },
-    emojis () {
-      const standardEmojis = this.$store.state.instance.emoji || []
-      // const customEmojis = this.customEmojiBuffer
-
-      return [
-        ...Object
-          .keys(this.groupedCustomEmojis)
-          .map(k => this.groupedCustomEmojis[k]),
-        {
-          id: 'standard',
-          text: this.$t('emoji.unicode'),
-          icon: 'box-open',
-          emojis: filterByKeyword(standardEmojis, trim(this.keyword))
-        }
-      ]
+    filteredEmojiGroups () {
+      return this.allEmojiGroups
+        .map(group => ({
+          ...group,
+          emojis: filterByKeyword(group.emojis, this.keyword)
+        }))
+        .filter(group => group.emojis.length > 0)
     },
     loadedCount () {
       return Object.keys(this.allCustomGroups)
@@ -297,9 +245,6 @@ const EmojiPicker = {
     lastNonUnicodeGroupId () {
       return this.emojis[this.emojis.length - 2].id
     },
-    emojisView () {
-      return this.emojis.filter(value => value.emojis.length > 0)
-    },
     stickerPickerEnabled () {
       return (this.$store.state.instance.stickers || []).length !== 0
     }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 277d5bf6..7b2b7fc8 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,7 +3,7 @@
     <div class="heading">
       <span class="emoji-tabs">
         <span
-          v-for="group in allEmojiGroups"
+          v-for="group in filteredEmojiGroups"
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
@@ -67,7 +67,7 @@
           @scroll="onScroll"
         >
           <div
-            v-for="group in allEmojiGroups"
+            v-for="group in filteredEmojiGroups"
             :key="group.id"
             class="emoji-group"
           >
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 23f534c3..8aadce77 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -115,6 +115,24 @@ const instance = {
         .map(key => [key, state[key]])
         .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
     },
+    groupedCustomEmojis (state) {
+      return state.customEmoji
+        .reduce((res, emoji) => {
+          emoji.tags.forEach(packName => {
+            const packId = `custom-${packName}`
+            if (!res[packId]) {
+              res[packId] = ({
+                id: packId,
+                text: packName,
+                image: emoji.imageUrl,
+                emojis: []
+              })
+            }
+            res[packId].emojis.push(emoji)
+          })
+          return res
+        }, {})
+    },
     instanceDomain (state) {
       return new URL(state.server).hostname
     }
-- 
cgit v1.2.3-70-g09d2


From c70cdbb873eb77bc1aaf7edb9defdda59bdba1e1 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 7 Oct 2021 23:23:58 -0400
Subject: Use lozad for lazy image loading

Ref: grouped-emoji-picker
---
 package.json                                 |  1 +
 src/components/emoji_picker/emoji_picker.js  |  6 +++++-
 src/components/emoji_picker/emoji_picker.vue |  3 ++-
 src/directives/lazy_image_container.js       | 13 +++++++++++++
 yarn.lock                                    |  5 +++++
 5 files changed, 26 insertions(+), 2 deletions(-)
 create mode 100644 src/directives/lazy_image_container.js

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

diff --git a/package.json b/package.json
index 50a6b55f..06fb4916 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
     "js-cookie": "3.0.1",
     "localforage": "1.10.0",
     "parse-link-header": "2.0.0",
+    "lozad": "^1.16.0",
     "phoenix": "1.6.2",
     "punycode.js": "2.1.0",
     "qrcode": "1.5.0",
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 8e11f83f..82e5ad0b 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,5 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
+import LazyImageContainer from '../../directives/lazy_image_container'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faBoxOpen,
@@ -64,6 +65,9 @@ const EmojiPicker = {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
     Checkbox
   },
+  directives: {
+    LazyImageContainer
+  },
   methods: {
     onStickerUploaded (e) {
       this.$emit('sticker-uploaded', e)
@@ -184,7 +188,7 @@ const EmojiPicker = {
       this.showingStickers = value
     },
     limitedEmojis (list, groupId) {
-      return list.slice(0, this.loadedCount[groupId])
+      return list // list.slice(0, this.loadedCount[groupId])
     },
     filterByKeyword (list, keyword) {
       return filterByKeyword(list, keyword)
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 7b2b7fc8..0e6c7e41 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -62,6 +62,7 @@
         </div>
         <div
           ref="emoji-groups"
+          v-lazy-image-container
           class="emoji-groups"
           :class="groupsScrolledClass"
           @scroll="onScroll"
@@ -87,7 +88,7 @@
               <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
               <img
                 v-else
-                :src="emoji.imageUrl"
+                :data-src="emoji.imageUrl"
               >
             </span>
             <span :ref="'group-end-' + group.id" />
diff --git a/src/directives/lazy_image_container.js b/src/directives/lazy_image_container.js
new file mode 100644
index 00000000..44adc828
--- /dev/null
+++ b/src/directives/lazy_image_container.js
@@ -0,0 +1,13 @@
+
+import lozad from 'lozad'
+
+const LazyImageContainer = {
+  inserted (el) {
+    const images = el.querySelectorAll('img')
+    console.log(images.length)
+    el.$observer = lozad(images)
+    el.$observer.observe()
+  }
+}
+
+export default LazyImageContainer
diff --git a/yarn.lock b/yarn.lock
index 2fefe76f..041fc1dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5733,6 +5733,11 @@ lower-case@^2.0.2:
   dependencies:
     tslib "^2.0.3"
 
+lozad@^1.16.0:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/lozad/-/lozad-1.16.0.tgz#86ce732c64c69926ccdebb81c8c90bb3735948b4"
+  integrity sha512-JBr9WjvEFeKoyim3svo/gsQPTkgG/mOHJmDctZ/+U9H3ymUuvEkqpn8bdQMFsvTMcyRJrdJkLv0bXqGm0sP72w==
+
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-- 
cgit v1.2.3-70-g09d2


From 8777b6eadd7deadf010dc36bb90514f75fc0da16 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 8 Oct 2021 01:02:16 -0400
Subject: Clean up legacy code in emoji picker

Ref: grouped-emoji-picker
---
 src/components/emoji_input/emoji_input.js    |   1 -
 src/components/emoji_picker/emoji_picker.js  | 130 ++++++---------------------
 src/components/emoji_picker/emoji_picker.vue |   3 +-
 src/directives/lazy_image_container.js       |  13 ---
 4 files changed, 28 insertions(+), 119 deletions(-)
 delete mode 100644 src/directives/lazy_image_container.js

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index b664d6b3..fb2096c9 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -207,7 +207,6 @@ const EmojiInput = {
     },
     triggerShowPicker () {
       this.showPicker = true
-      this.$refs.picker.startEmojiLoad()
       this.$nextTick(() => {
         this.scrollIntoView()
         this.focusPickerInput()
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 82e5ad0b..b0162479 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,6 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
-import LazyImageContainer from '../../directives/lazy_image_container'
+import lozad from 'lozad'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faBoxOpen,
@@ -54,7 +54,6 @@ const EmojiPicker = {
       showingStickers: false,
       groupsScrolledClass: 'scrolled-top',
       keepOpen: false,
-      customEmojiBufferSlice: LOAD_EMOJI_BY,
       customEmojiTimeout: null,
       customEmojiLoadAllConfirmed: false,
       groupLoadedCount: {},
@@ -65,9 +64,6 @@ const EmojiPicker = {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
     Checkbox
   },
-  directives: {
-    LazyImageContainer
-  },
   methods: {
     onStickerUploaded (e) {
       this.$emit('sticker-uploaded', e)
@@ -82,10 +78,6 @@ const EmojiPicker = {
     onScroll (e) {
       const target = (e && e.target) || this.$refs['emoji-groups']
       this.updateScrolledClass(target)
-      this.scrolledGroup(target)
-      this.$nextTick(() => {
-        this.triggerLoadMore(target)
-      })
     },
     highlight (key) {
       const ref = this.$refs['group-' + key]
@@ -94,7 +86,6 @@ const EmojiPicker = {
       this.activeGroup = key
       this.$nextTick(() => {
         this.$refs['emoji-groups'].scrollTop = top + 1
-        this.loadEmoji(key)
       })
     },
     updateScrolledClass (target) {
@@ -106,101 +97,48 @@ const EmojiPicker = {
         this.groupsScrolledClass = 'scrolled-middle'
       }
     },
-    triggerLoadMore (target) {
-      Object.keys(this.allCustomGroups)
-        .filter(id => this.filteredEmojiGroups.filter(group => group.id === id).length > 0)
-        .map(groupId => {
-          const ref = this.$refs[`group-end-${groupId}`][0]
-          if (!ref) return undefined
-
-          const bottom = ref.offsetTop + ref.offsetHeight
-
-          const group = this.$refs[`group-${groupId}`][0]
-          const top = group.offsetTop
-
-          const scrollerBottom = target.scrollTop + target.clientHeight
-          const scrollerTop = target.scrollTop
-          const scrollerMax = target.scrollHeight
-
-          // Loads more emoji when they come into view
-          const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
-          // Always load when at the very top in case there's no scroll space yet
-          const atTop = scrollerTop < top + target.clientHeight / 2 && top < scrollerBottom
-          const unscrollable = top - bottom < target.clientHeight
-          // Don't load when looking at unicode category or at the very bottom
-          const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
-          if (!bottomAboveViewport && (approachingBottom || atTop || unscrollable)) {
-            return groupId
-          }
-          return undefined
-        })
-        .filter(k => k)
-        .map(k => {
-          this.loadEmoji(k)
-        })
-    },
-    scrolledGroup (target) {
-      const top = target.scrollTop + 5
-      this.$nextTick(() => {
-        this.allEmojiGroups.forEach(group => {
-          const ref = this.$refs['group-' + group.id]
-          if (ref.offsetTop <= top) {
-            this.activeGroup = group.id
-          }
-        })
-      })
-    },
-    loadEmoji (loadGroup) {
-      if (!this.allCustomGroups[loadGroup]) {
-        return
-      }
-
-      const allLoaded = this.loadedCount[loadGroup] >= this.allCustomGroups[loadGroup].emojis.length
-
-      if (allLoaded) {
-        return
-      }
-
-      this.groupLoadedCount = {
-        ...this.groupLoadedCount,
-        [loadGroup]: this.loadedCount[loadGroup] + LOAD_EMOJI_BY
-      }
-    },
-    startEmojiLoad (forceUpdate = false) {
-      if (!forceUpdate) {
-        this.keyword = ''
-      }
-      this.$nextTick(() => {
-        this.$refs['emoji-groups'].scrollTop = 0
-        this.$nextTick(() => {
-          if (this.firstLoaded) {
-            return
-          }
-          this.triggerLoadMore(this.$refs['emoji-groups'])
-          this.firstLoaded = true
-        })
-      })
-    },
     toggleStickers () {
       this.showingStickers = !this.showingStickers
     },
     setShowStickers (value) {
       this.showingStickers = value
     },
-    limitedEmojis (list, groupId) {
-      return list // list.slice(0, this.loadedCount[groupId])
-    },
     filterByKeyword (list, keyword) {
       return filterByKeyword(list, keyword)
+    },
+    initializeLazyLoad () {
+      this.destroyLazyLoad()
+      this.$lozad = lozad('img', {})
+      this.$lozad.observe()
+    },
+    destroyLazyLoad () {
+      if (this.$lozad) {
+        if (this.$lozad.observer) {
+          this.$lozad.observer.disconnect()
+        }
+        if (this.$lozad.mutationObserver) {
+          this.$lozad.mutationObserver.disconnect()
+        }
+      }
     }
   },
   watch: {
     keyword () {
       this.customEmojiLoadAllConfirmed = false
       this.onScroll()
-      this.startEmojiLoad(true)
+      // Wait for the dom to change
+      this.$nextTick(() => this.initializeLazyLoad())
+    },
+    allCustomGroups () {
+      this.$nextTick(() => this.initializeLazyLoad())
     }
   },
+  mounted () {
+    this.initializeLazyLoad()
+  },
+  destroyed () {
+    this.destroyLazyLoad()
+  },
   computed: {
     activeGroupView () {
       return this.showingStickers ? '' : this.activeGroup
@@ -214,10 +152,6 @@ const EmojiPicker = {
     allCustomGroups () {
       return this.$store.getters.groupedCustomEmojis
     },
-    sensibleInitialAmountForAGroup () {
-      const groupCount = Object.keys(this.allCustomGroups).length
-      return Math.max(Math.floor(LOAD_EMOJI_BY / Math.max(groupCount, 1)), 1)
-    },
     allEmojiGroups () {
       const standardEmojis = this.$store.state.instance.emoji || []
       return Object.entries(this.allCustomGroups)
@@ -237,16 +171,6 @@ const EmojiPicker = {
         }))
         .filter(group => group.emojis.length > 0)
     },
-    loadedCount () {
-      return Object.keys(this.allCustomGroups)
-        .reduce((res, groupId) => {
-          res[groupId] = this.groupLoadedCount[groupId] || this.sensibleInitialAmountForAGroup
-          return res
-        }, {})
-    },
-    lastNonUnicodeGroupId () {
-      return this.emojis[this.emojis.length - 2].id
-    },
     stickerPickerEnabled () {
       return (this.$store.state.instance.stickers || []).length !== 0
     }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 0e6c7e41..0df33c24 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -62,7 +62,6 @@
         </div>
         <div
           ref="emoji-groups"
-          v-lazy-image-container
           class="emoji-groups"
           :class="groupsScrolledClass"
           @scroll="onScroll"
@@ -79,7 +78,7 @@
               {{ group.text }}
             </h6>
             <span
-              v-for="emoji in limitedEmojis(group.emojis, group.id)"
+              v-for="emoji in group.emojis"
               :key="group.id + emoji.displayText"
               :title="emoji.displayText"
               class="emoji-item"
diff --git a/src/directives/lazy_image_container.js b/src/directives/lazy_image_container.js
deleted file mode 100644
index 44adc828..00000000
--- a/src/directives/lazy_image_container.js
+++ /dev/null
@@ -1,13 +0,0 @@
-
-import lozad from 'lozad'
-
-const LazyImageContainer = {
-  inserted (el) {
-    const images = el.querySelectorAll('img')
-    console.log(images.length)
-    el.$observer = lozad(images)
-    el.$observer.observe()
-  }
-}
-
-export default LazyImageContainer
-- 
cgit v1.2.3-70-g09d2


From 031a01be7920e4cf3bda6ed5e4f3e589f9821121 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 8 Oct 2021 13:06:03 -0400
Subject: Remove useless class `disabled` in emoji picker

Ref: grouped-emoji-picker
---
 src/components/emoji_picker/emoji_picker.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 0df33c24..fb2eef25 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -7,8 +7,7 @@
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
-            active: activeGroupView === group.id,
-            disabled: false
+            active: activeGroupView === group.id
           }"
           :title="group.text"
           @click.prevent="highlight(group.id)"
-- 
cgit v1.2.3-70-g09d2


From 5ab51613b738bf089770c2ad6d0ae9354d49bcee Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 8 Oct 2021 13:17:47 -0400
Subject: Use StillImage for emoji group header

Ref: grouped-emoji-picker
---
 src/components/emoji_picker/emoji_picker.js  | 4 +++-
 src/components/emoji_picker/emoji_picker.vue | 4 ++--
 2 files changed, 5 insertions(+), 3 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index d60daab7..5b90c31e 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,5 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
+import StillImage from '../still-image/still-image.vue'
 import lozad from 'lozad'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -52,7 +53,8 @@ const EmojiPicker = {
   },
   components: {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
-    Checkbox
+    Checkbox,
+    StillImage
   },
   methods: {
     onStickerUploaded (e) {
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index fb2eef25..ed196066 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -16,10 +16,10 @@
             v-if="group.image"
             class="emoji-picker-header-image"
           >
-            <img
+            <still-image
               :alt="group.text"
               :src="group.image"
-            >
+            />
           </span>
           <FAIcon
             v-else
-- 
cgit v1.2.3-70-g09d2


From 06a636db3732ce2808c54d3b74eb4aabd866dbf6 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 8 Oct 2021 15:09:24 -0400
Subject: Lazy-load emoji picker in post form

When clicking the reply button, we used to load the whole emoji picker.
This causes a considerable delay even if the user is not going to use
the emoji picker. Now the content of the emoji picker is loaded only
after the user has explicitly opened the emoji picker.

Ref: grouped-emoji-picker
---
 src/components/emoji_input/emoji_input.vue   |  1 +
 src/components/emoji_picker/emoji_picker.js  | 24 +++++++++++++++++++-----
 src/components/emoji_picker/emoji_picker.vue |  9 +++++++--
 3 files changed, 27 insertions(+), 7 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 81b81913..eedde9aa 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -19,6 +19,7 @@
         v-if="enableEmojiPicker"
         ref="picker"
         :class="{ hide: !showPicker }"
+        :showing="showPicker"
         :enable-sticker-picker="enableStickerPicker"
         class="emoji-picker-panel"
         @emoji="insert"
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 5b90c31e..8b4f302f 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -39,6 +39,10 @@ const EmojiPicker = {
       required: false,
       type: Boolean,
       default: false
+    },
+    showing: {
+      required: true,
+      type: Boolean
     }
   },
   data () {
@@ -48,7 +52,9 @@ const EmojiPicker = {
       showingStickers: false,
       groupsScrolledClass: 'scrolled-top',
       keepOpen: false,
-      customEmojiTimeout: null
+      customEmojiTimeout: null,
+      // Lazy-load only after the first time `showing` becomes true.
+      contentLoaded: false
     }
   },
   components: {
@@ -115,6 +121,9 @@ const EmojiPicker = {
       this.$lozad = lozad('img', {})
       this.$lozad.observe()
     },
+    waitForDomAndInitializeLazyLoad() {
+      this.$nextTick(() => this.initializeLazyLoad())
+    },
     destroyLazyLoad () {
       if (this.$lozad) {
         if (this.$lozad.observer) {
@@ -129,18 +138,23 @@ const EmojiPicker = {
   watch: {
     keyword () {
       this.onScroll()
-      // Wait for the dom to change
-      this.$nextTick(() => this.initializeLazyLoad())
+      this.waitForDomAndInitializeLazyLoad()
     },
     allCustomGroups () {
-      this.$nextTick(() => this.initializeLazyLoad())
+      this.waitForDomAndInitializeLazyLoad()
+    },
+    showing (val) {
+      if (val) {
+        this.contentLoaded = true
+        this.waitForDomAndInitializeLazyLoad()
+      }
     }
   },
   mounted () {
     if (this.defaultGroup) {
       this.highlight(this.defaultGroup)
     }
-    this.initializeLazyLoad()
+    this.waitForDomAndInitializeLazyLoad()
   },
   destroyed () {
     this.destroyLazyLoad()
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index ed196066..b92bccd7 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,5 +1,7 @@
 <template>
-  <div class="emoji-picker panel panel-default panel-body">
+  <div
+    class="emoji-picker panel panel-default panel-body"
+  >
     <div class="heading">
       <span class="emoji-tabs">
         <span
@@ -45,7 +47,10 @@
         </span>
       </span>
     </div>
-    <div class="content">
+    <div
+      v-if="contentLoaded"
+      class="content"
+    >
       <div
         class="emoji-content"
         :class="{hidden: showingStickers}"
-- 
cgit v1.2.3-70-g09d2


From b77259a9a0c353ede8ff1d6bf5c13ae91ca7fc7c Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 8 Jan 2022 01:35:16 -0500
Subject: Use StillImage to render emojis in emoji picker

---
 src/components/emoji_picker/emoji_picker.js  | 15 +++++++++++++--
 src/components/emoji_picker/emoji_picker.vue |  5 +++--
 src/components/still-image/still-image.js    | 19 +++++++++++++++++--
 src/components/still-image/still-image.vue   |  5 +++--
 4 files changed, 36 insertions(+), 8 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index ea53a972..315364d5 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -118,8 +118,19 @@ const EmojiPicker = {
     },
     initializeLazyLoad () {
       this.destroyLazyLoad()
-      this.$lozad = lozad('img', {})
-      this.$lozad.observe()
+      this.$nextTick(() => {
+        this.$lozad = lozad('.still-image.emoji-picker-emoji', {
+          load: el => {
+            const vn = el.__vue__
+            if (!vn) {
+              return
+            }
+
+            vn.loadLazy()
+          }
+        })
+        this.$lozad.observe()
+      })
     },
     waitForDomAndInitializeLazyLoad () {
       this.$nextTick(() => this.initializeLazyLoad())
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index b92bccd7..19cc46b5 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -89,10 +89,11 @@
               @click.stop.prevent="onEmoji(emoji)"
             >
               <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
-              <img
+              <still-image
                 v-else
+                class="emoji-picker-emoji"
                 :data-src="emoji.imageUrl"
-              >
+              />
             </span>
             <span :ref="'group-end-' + group.id" />
           </div>
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index d7abbcb5..1806d33b 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -7,16 +7,23 @@ const StillImage = {
     'imageLoadHandler',
     'alt',
     'height',
-    'width'
+    'width',
+    'dataSrc'
   ],
   data () {
     return {
+      // for lazy loading, see loadLazy()
+      realSrc: this.src,
       stopGifs: this.$store.getters.mergedConfig.stopGifs
     }
   },
   computed: {
     animated () {
-      return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif'))
+      if (!this.realSrc) {
+        return false
+      }
+
+      return this.stopGifs && (this.mimetype === 'image/gif' || this.realSrc.endsWith('.gif'))
     },
     style () {
       const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
@@ -27,7 +34,15 @@ const StillImage = {
     }
   },
   methods: {
+    loadLazy () {
+      if (this.dataSrc) {
+        this.realSrc = this.dataSrc
+      }
+    },
     onLoad () {
+      if (!this.realSrc) {
+        return
+      }
       const image = this.$refs.src
       if (!image) return
       this.imageLoadHandler && this.imageLoadHandler(image)
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index ab3080c8..633fb229 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -11,10 +11,11 @@
     <!-- NOTE: key is required to force to re-render img tag when src is changed -->
     <img
       ref="src"
-      :key="src"
+      :key="realSrc"
       :alt="alt"
       :title="alt"
-      :src="src"
+      :data-src="dataSrc"
+      :src="realSrc"
       :referrerpolicy="referrerpolicy"
       @load="onLoad"
       @error="onError"
-- 
cgit v1.2.3-70-g09d2


From 38861fc6cc1e30c6ef3c429db222a303ed1c321d Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 8 Jan 2022 02:17:59 -0500
Subject: Scroll active tab header into view in emoji picker

---
 src/components/emoji_picker/emoji_picker.js  | 18 ++++++++++++++++++
 src/components/emoji_picker/emoji_picker.vue |  6 +++++-
 2 files changed, 23 insertions(+), 1 deletion(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 315364d5..26c767ac 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -87,8 +87,26 @@ const EmojiPicker = {
             this.activeGroup = group.id
           }
         })
+        this.scrollHeader()
       })
     },
+    scrollHeader () {
+      // Scroll the active tab's header into view
+      const headerRef = this.$refs['group-header-' + this.activeGroup][0]
+      const left = headerRef.offsetLeft
+      const right = left + headerRef.offsetWidth
+      const headerCont = this.$refs.header
+      const currentScroll = headerCont.scrollLeft
+      const currentScrollRight = currentScroll + headerCont.clientWidth
+      const setScroll = s => { headerCont.scrollLeft = s }
+
+      const margin = 7 // .emoji-tabs-item: padding
+      if (left - margin < currentScroll) {
+        setScroll(left - margin)
+      } else if (right + margin > currentScrollRight) {
+        setScroll(right + margin - headerCont.clientWidth)
+      }
+    },
     highlight (key) {
       const ref = this.$refs['group-' + key]
       const top = ref.offsetTop
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 19cc46b5..e8d42c24 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,9 +3,13 @@
     class="emoji-picker panel panel-default panel-body"
   >
     <div class="heading">
-      <span class="emoji-tabs">
+      <span
+        ref="header"
+        class="emoji-tabs"
+      >
         <span
           v-for="group in filteredEmojiGroups"
+          :ref="'group-header-' + group.id"
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
-- 
cgit v1.2.3-70-g09d2


From e01c76c7e90f354436e726456532f51288a7ab99 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Wed, 6 Apr 2022 21:29:50 -0400
Subject: Make emoji picker work with vue3

---
 src/components/emoji_picker/emoji_picker.js  | 21 +++++++++++++++------
 src/components/emoji_picker/emoji_picker.vue |  8 +++++---
 2 files changed, 20 insertions(+), 9 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 07f4b005..677ef5e4 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -84,7 +84,9 @@ const EmojiPicker = {
       keepOpen: false,
       customEmojiTimeout: null,
       // Lazy-load only after the first time `showing` becomes true.
-      contentLoaded: false
+      contentLoaded: false,
+      groupRefs: {},
+      emojiRefs: {}
     }
   },
   components: {
@@ -93,6 +95,12 @@ const EmojiPicker = {
     StillImage
   },
   methods: {
+    setGroupRef (name) {
+      return el => { this.groupRefs[name] = el }
+    },
+    setEmojiRef (name) {
+      return el => { this.emojiRefs[name] = el }
+    },
     onStickerUploaded (e) {
       this.$emit('sticker-uploaded', e)
     },
@@ -112,8 +120,8 @@ const EmojiPicker = {
       const top = target.scrollTop + 5
       this.$nextTick(() => {
         this.allEmojiGroups.forEach(group => {
-          const ref = this.$refs['group-' + group.id]
-          if (ref[0].offsetTop <= top) {
+          const ref = this.groupRefs['group-' + group.id]
+          if (ref && ref.offsetTop <= top) {
             this.activeGroup = group.id
           }
         })
@@ -122,7 +130,7 @@ const EmojiPicker = {
     },
     scrollHeader () {
       // Scroll the active tab's header into view
-      const headerRef = this.$refs['group-header-' + this.activeGroup][0]
+      const headerRef = this.groupRefs['group-header-' + this.activeGroup]
       const left = headerRef.offsetLeft
       const right = left + headerRef.offsetWidth
       const headerCont = this.$refs.header
@@ -138,7 +146,7 @@ const EmojiPicker = {
       }
     },
     highlight (key) {
-      const ref = this.$refs['group-' + key]
+      const ref = this.groupRefs['group-' + key]
       const top = ref.offsetTop
       this.setShowStickers(false)
       this.activeGroup = key
@@ -169,7 +177,8 @@ const EmojiPicker = {
       this.$nextTick(() => {
         this.$lozad = lozad('.still-image.emoji-picker-emoji', {
           load: el => {
-            const vn = el.__vue__
+            const name = el.getAttribute('data-emoji-name')
+            const vn = this.emojiRefs[name]
             if (!vn) {
               return
             }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index e8d42c24..a6a63411 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -9,7 +9,7 @@
       >
         <span
           v-for="group in filteredEmojiGroups"
-          :ref="'group-header-' + group.id"
+          :ref="setGroupRef('group-header-' + group.id)"
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
@@ -80,7 +80,7 @@
             class="emoji-group"
           >
             <h6
-              :ref="'group-' + group.id"
+              :ref="setGroupRef('group-' + group.id)"
               class="emoji-group-title"
             >
               {{ group.text }}
@@ -96,10 +96,12 @@
               <still-image
                 v-else
                 class="emoji-picker-emoji"
+                :ref="setEmojiRef(group.id + emoji.displayText)"
                 :data-src="emoji.imageUrl"
+                :data-emoji-name="group.id + emoji.displayText"
               />
             </span>
-            <span :ref="'group-end-' + group.id" />
+            <span :ref="setGroupRef('group-end-' + group.id)" />
           </div>
         </div>
         <div class="keep-open">
-- 
cgit v1.2.3-70-g09d2


From 0fd0d6c4c2c7791f889135727f8afef10a36472d Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 29 Apr 2022 22:40:06 -0400
Subject: Limit the width of unsupported multichar emojis

---
 src/components/emoji_picker/emoji_picker.scss | 8 ++++++--
 src/components/emoji_picker/emoji_picker.vue  | 7 +++++--
 2 files changed, 11 insertions(+), 4 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 222749d0..af01b3ec 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -198,18 +198,22 @@ $emoji-picker-emoji-size: 32px;
       height: $emoji-picker-emoji-size;
       box-sizing: border-box;
       display: flex;
-      font-size: $emoji-picker-emoji-size;
+      line-height: $emoji-picker-emoji-size;
       align-items: center;
       justify-content: center;
       margin: 4px;
 
       cursor: pointer;
 
-      img {
+      .emoji-picker-emoji.-custom {
         object-fit: contain;
         max-width: 100%;
         max-height: 100%;
       }
+      .emoji-picker-emoji.-unicode {
+        font-size: 24px;
+        overflow: hidden;
+      }
     }
 
   }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index a6a63411..e5a5958c 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -92,10 +92,13 @@
               class="emoji-item"
               @click.stop.prevent="onEmoji(emoji)"
             >
-              <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
+              <span
+                v-if="!emoji.imageUrl"
+                class="emoji-picker-emoji -unicode"
+              >{{ emoji.replacement }}</span>
               <still-image
                 v-else
-                class="emoji-picker-emoji"
+                class="emoji-picker-emoji -custom"
                 :ref="setEmojiRef(group.id + emoji.displayText)"
                 :data-src="emoji.imageUrl"
                 :data-emoji-name="group.id + emoji.displayText"
-- 
cgit v1.2.3-70-g09d2


From 6e2b87f5af922d27ee87b9fec64c5308832a41af Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 1 Aug 2022 11:30:54 -0400
Subject: Fix emoji picker lint

---
 src/components/emoji_picker/emoji_picker.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index e5a5958c..689138e6 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -98,8 +98,8 @@
               >{{ emoji.replacement }}</span>
               <still-image
                 v-else
-                class="emoji-picker-emoji -custom"
                 :ref="setEmojiRef(group.id + emoji.displayText)"
+                class="emoji-picker-emoji -custom"
                 :data-src="emoji.imageUrl"
                 :data-emoji-name="group.id + emoji.displayText"
               />
-- 
cgit v1.2.3-70-g09d2


From a73f9731f5ad78d1e6f1bd211ad2971d21fc1379 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Tue, 20 Sep 2022 20:44:52 -0400
Subject: Display localized unicode emoji names

---
 src/components/emoji_picker/emoji_picker.js  | 20 ++++++++++++++++++++
 src/components/emoji_picker/emoji_picker.vue |  2 +-
 src/i18n/languages.js                        |  3 ++-
 src/modules/instance.js                      | 18 +++++++++++++++---
 4 files changed, 38 insertions(+), 5 deletions(-)

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

diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index c2ae76f3..2ebead53 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,6 +1,7 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
 import StillImage from '../still-image/still-image.vue'
+import { ensureFinalFallback } from '../../i18n/languages.js'
 import lozad from 'lozad'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -285,6 +286,25 @@ const EmojiPicker = {
         this.waitForDomAndInitializeLazyLoad()
         this.filteredEmojiGroups = this.getFilteredEmojiGroups()
       }, 500)
+    },
+    languages () {
+      console.log('languages:', ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage))
+      return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+    },
+    maybeLocalizedEmojiName () {
+      return emoji => {
+        if (!emoji.annotations) {
+          return emoji.displayText
+        }
+
+        for (const lang of this.languages) {
+          if (emoji.annotations[lang]?.name) {
+            return emoji.annotations[lang].name
+          }
+        }
+
+        return emoji.displayText
+      }
     }
   }
 }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 689138e6..57bb0037 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -88,7 +88,7 @@
             <span
               v-for="emoji in group.emojis"
               :key="group.id + emoji.displayText"
-              :title="emoji.displayText"
+              :title="maybeLocalizedEmojiName(emoji)"
               class="emoji-item"
               @click.stop.prevent="onEmoji(emoji)"
             >
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
index 40cf04f2..250b3b1a 100644
--- a/src/i18n/languages.js
+++ b/src/i18n/languages.js
@@ -41,7 +41,8 @@ const langCodeToJsonName = (code) => specialJsonName[code] || code
 const langCodeToCldrName = (code) => code
 
 const ensureFinalFallback = codes => {
-  return codes.includes('en') ? codes : codes.concat(['en'])
+  const codeList = Array.isArray(codes) ? codes : [codes]
+  return codeList.includes('en') ? codeList : codeList.concat(['en'])
 }
 
 module.exports = {
diff --git a/src/modules/instance.js b/src/modules/instance.js
index c0c7cef0..5a72a6d3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -117,6 +117,18 @@ const loadAnnotations = (lang) => {
   )
 }
 
+const injectAnnotations = (emoji, annotations) => {
+  const availableLangs = Object.keys(annotations)
+
+  return {
+    ...emoji,
+    annotations: availableLangs.reduce((acc, cur) => {
+      acc[cur] = annotations[cur][emoji.replacement]
+      return acc
+    }, {})
+  }
+}
+
 const instance = {
   state: defaultState,
   mutations: {
@@ -164,13 +176,13 @@ const instance = {
     },
     standardEmojiList (state) {
       return SORTED_EMOJI_GROUP_IDS
-        .map(groupId => state.emoji[groupId] || [])
+        .map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)))
         .reduce((a, b) => a.concat(b), [])
     },
     standardEmojiGroupList (state) {
       return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
         id: groupId,
-        emojis: state.emoji[groupId] || []
+        emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
       }))
     },
     instanceDomain (state) {
@@ -218,7 +230,7 @@ const instance = {
     },
 
     loadUnicodeEmojiData ({ commit, state }, language) {
-      const langList = ensureFinalFallback(Array.isArray(language) ? language : [language])
+      const langList = ensureFinalFallback(language)
 
       return Promise.all(
         langList
-- 
cgit v1.2.3-70-g09d2


From 296a6fa4e3d14f0c8ca091870fd4f7e85bc34df8 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 9 Oct 2022 23:42:36 +0300
Subject: some shitty initial implementation of emoji picker with popover

---
 src/components/emoji_input/emoji_input.js     |  36 ++---
 src/components/emoji_input/emoji_input.vue    |   5 +-
 src/components/emoji_picker/emoji_picker.js   |  32 ++--
 src/components/emoji_picker/emoji_picker.scss |   8 +-
 src/components/emoji_picker/emoji_picker.vue  | 210 +++++++++++++-------------
 src/components/popover/popover.js             |   6 +
 6 files changed, 150 insertions(+), 147 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 5131e699..635251be 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -115,12 +115,12 @@ const EmojiInput = {
       caret: 0,
       focused: false,
       blurTimeout: null,
-      showPicker: false,
       temporarilyHideSuggestions: false,
       keepOpen: false,
       disableClickOutside: false,
       suggestions: [],
-      overlayStyle: {}
+      overlayStyle: {},
+      pickerShown: false
     }
   },
   components: {
@@ -142,7 +142,6 @@ const EmojiInput = {
       return this.focused &&
         this.suggestions &&
         this.suggestions.length > 0 &&
-        !this.showPicker &&
         !this.temporarilyHideSuggestions
     },
     textAtCaret () {
@@ -285,8 +284,8 @@ const EmojiInput = {
       if (pickerInput) pickerInput.focus()
     },
     triggerShowPicker () {
-      this.showPicker = true
       this.$nextTick(() => {
+        this.$refs.picker.showPicker()
         this.scrollIntoView()
         this.focusPickerInput()
       })
@@ -299,12 +298,16 @@ const EmojiInput = {
       }, 0)
     },
     togglePicker () {
+      console.log('piick')
       this.input.focus()
-      this.showPicker = !this.showPicker
-      if (this.showPicker) {
+      if (!this.pickerShown) {
+        console.log('pick')
         this.scrollIntoView()
+        this.$refs.picker.showPicker()
         this.$refs.picker.startEmojiLoad()
         this.$nextTick(this.focusPickerInput)
+      } else {
+        this.$refs.picker.hidePicker()
       }
     },
     replace (replacement) {
@@ -441,6 +444,12 @@ const EmojiInput = {
         }
       })
     },
+    onPickerShown () {
+      this.pickerShown = true
+    },
+    onPickerClosed () {
+      this.pickerShown = false
+    },
     onBlur (e) {
       // Clicking on any suggestion removes focus from autocomplete,
       // preventing click handler ever executing.
@@ -458,9 +467,6 @@ const EmojiInput = {
         this.blurTimeout = null
       }
 
-      if (!this.keepOpen) {
-        this.showPicker = false
-      }
       this.focused = true
       this.setCaret(e)
       this.temporarilyHideSuggestions = false
@@ -523,27 +529,15 @@ const EmojiInput = {
           this.input.focus()
         }
       }
-
-      this.showPicker = false
     },
     onInput (e) {
-      this.showPicker = false
       this.setCaret(e)
       this.$emit('update:modelValue', e.target.value)
     },
-    onClickInput (e) {
-      this.showPicker = false
-    },
-    onClickOutside (e) {
-      if (this.disableClickOutside) return
-      this.showPicker = false
-    },
     onStickerUploaded (e) {
-      this.showPicker = false
       this.$emit('sticker-uploaded', e)
     },
     onStickerUploadFailed (e) {
-      this.showPicker = false
       this.$emit('sticker-upload-Failed', e)
     },
     setCaret ({ target: { selectionStart } }) {
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 68498ef0..63bf856e 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -1,7 +1,6 @@
 <template>
   <div
     ref="root"
-    v-click-outside="onClickOutside"
     class="emoji-input"
     :class="{ 'with-picker': !hideEmojiButton }"
   >
@@ -24,13 +23,13 @@
       <EmojiPicker
         v-if="enableEmojiPicker"
         ref="picker"
-        :class="{ hide: !showPicker }"
-        :showing="showPicker"
         :enable-sticker-picker="enableStickerPicker"
         class="emoji-picker-panel"
         @emoji="insert"
         @sticker-uploaded="onStickerUploaded"
         @sticker-upload-failed="onStickerUploadFailed"
+        @show="onPickerShown"
+        @close="onPickerClosed"
       />
     </template>
     <Popover
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index fafc2af1..d70abf5b 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,5 +1,6 @@
 import { defineAsyncComponent } from 'vue'
 import Checkbox from '../checkbox/checkbox.vue'
+import Popover from 'src/components/popover/popover.vue'
 import StillImage from '../still-image/still-image.vue'
 import { ensureFinalFallback } from '../../i18n/languages.js'
 import lozad from 'lozad'
@@ -87,10 +88,6 @@ const EmojiPicker = {
       required: false,
       type: Boolean,
       default: false
-    },
-    showing: {
-      required: true,
-      type: Boolean
     }
   },
   data () {
@@ -111,15 +108,30 @@ const EmojiPicker = {
   components: {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
     Checkbox,
-    StillImage
+    StillImage,
+    Popover
   },
   methods: {
+    showPicker () {
+      console.log('pick')
+      this.$refs.popover.showPopover()
+      this.onShowing()
+    },
+    hidePicker () {
+      this.$refs.popover.hidePopover()
+    },
     setGroupRef (name) {
       return el => { this.groupRefs[name] = el }
     },
     setEmojiRef (name) {
       return el => { this.emojiRefs[name] = el }
     },
+    onPopoverShown () {
+      this.$emit('show')
+    },
+    onPopoverClosed () {
+      this.$emit('close')
+    },
     onStickerUploaded (e) {
       this.$emit('sticker-uploaded', e)
     },
@@ -251,16 +263,6 @@ const EmojiPicker = {
     allCustomGroups () {
       this.waitForDomAndInitializeLazyLoad()
       this.filteredEmojiGroups = this.getFilteredEmojiGroups()
-    },
-    showing (val) {
-      if (val) {
-        this.onShowing()
-      }
-    }
-  },
-  mounted () {
-    if (this.showing) {
-      this.onShowing()
     }
   },
   destroyed () {
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 016c46d7..53363ec1 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -6,14 +6,10 @@ $emoji-picker-header-picture-height: 32px;
 $emoji-picker-emoji-size: 32px;
 
 .emoji-picker {
+  width: 25em;
+  max-width: 100vw;
   display: flex;
   flex-direction: column;
-  position: absolute;
-  right: 0;
-  left: 0;
-  margin: 0 !important;
-  // TODO: actually use popover in emoji picker
-  z-index: var(--ZI_popovers);
   background-color: $fallback--bg;
   background-color: var(--popover, $fallback--bg);
   color: $fallback--link;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 57bb0037..ccd45bc1 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,129 +1,135 @@
 <template>
-  <div
-    class="emoji-picker panel panel-default panel-body"
+  <Popover
+    trigger="click"
+    popover-class="emoji-picker popover-default"
+    ref="popover"
+    @show="onPopoverShown"
+    @close="onPopoverClosed"
   >
-    <div class="heading">
-      <span
-        ref="header"
-        class="emoji-tabs"
-      >
+    <template #content>
+      <div class="heading">
         <span
-          v-for="group in filteredEmojiGroups"
-          :ref="setGroupRef('group-header-' + group.id)"
-          :key="group.id"
-          class="emoji-tabs-item"
-          :class="{
-            active: activeGroupView === group.id
-          }"
-          :title="group.text"
-          @click.prevent="highlight(group.id)"
+          ref="header"
+          class="emoji-tabs"
         >
           <span
-            v-if="group.image"
-            class="emoji-picker-header-image"
+            v-for="group in filteredEmojiGroups"
+            :ref="setGroupRef('group-header-' + group.id)"
+            :key="group.id"
+            class="emoji-tabs-item"
+            :class="{
+              active: activeGroupView === group.id
+            }"
+            :title="group.text"
+            @click.prevent="highlight(group.id)"
           >
-            <still-image
-              :alt="group.text"
-              :src="group.image"
+            <span
+              v-if="group.image"
+              class="emoji-picker-header-image"
+            >
+              <still-image
+                :alt="group.text"
+                :src="group.image"
+              />
+            </span>
+            <FAIcon
+              v-else
+              :icon="group.icon"
+              fixed-width
             />
           </span>
-          <FAIcon
-            v-else
-            :icon="group.icon"
-            fixed-width
-          />
         </span>
-      </span>
-      <span
-        v-if="stickerPickerEnabled"
-        class="additional-tabs"
-      >
         <span
-          class="stickers-tab-icon additional-tabs-item"
-          :class="{active: showingStickers}"
-          :title="$t('emoji.stickers')"
-          @click.prevent="toggleStickers"
+          v-if="stickerPickerEnabled"
+          class="additional-tabs"
         >
-          <FAIcon
-            icon="sticky-note"
-            fixed-width
-          />
+          <span
+            class="stickers-tab-icon additional-tabs-item"
+            :class="{active: showingStickers}"
+            :title="$t('emoji.stickers')"
+            @click.prevent="toggleStickers"
+          >
+            <FAIcon
+              icon="sticky-note"
+              fixed-width
+            />
+          </span>
         </span>
-      </span>
-    </div>
-    <div
-      v-if="contentLoaded"
-      class="content"
-    >
+      </div>
       <div
-        class="emoji-content"
-        :class="{hidden: showingStickers}"
+        v-if="contentLoaded"
+        class="content"
       >
-        <div class="emoji-search">
-          <input
-            v-model="keyword"
-            type="text"
-            class="form-control"
-            :placeholder="$t('emoji.search_emoji')"
-            @input="$event.target.composing = false"
-          >
-        </div>
         <div
-          ref="emoji-groups"
-          class="emoji-groups"
-          :class="groupsScrolledClass"
-          @scroll="onScroll"
+          class="emoji-content"
+          :class="{hidden: showingStickers}"
         >
+          <div class="emoji-search">
+            <input
+              v-model="keyword"
+              type="text"
+              class="form-control"
+              :placeholder="$t('emoji.search_emoji')"
+              @input="$event.target.composing = false"
+            >
+          </div>
           <div
-            v-for="group in filteredEmojiGroups"
-            :key="group.id"
-            class="emoji-group"
+            ref="emoji-groups"
+            class="emoji-groups"
+            :class="groupsScrolledClass"
+            @scroll="onScroll"
           >
-            <h6
-              :ref="setGroupRef('group-' + group.id)"
-              class="emoji-group-title"
-            >
-              {{ group.text }}
-            </h6>
-            <span
-              v-for="emoji in group.emojis"
-              :key="group.id + emoji.displayText"
-              :title="maybeLocalizedEmojiName(emoji)"
-              class="emoji-item"
-              @click.stop.prevent="onEmoji(emoji)"
+            <div
+              v-for="group in filteredEmojiGroups"
+              :key="group.id"
+              class="emoji-group"
             >
+              <h6
+                :ref="setGroupRef('group-' + group.id)"
+                class="emoji-group-title"
+              >
+                {{ group.text }}
+              </h6>
               <span
-                v-if="!emoji.imageUrl"
-                class="emoji-picker-emoji -unicode"
-              >{{ emoji.replacement }}</span>
-              <still-image
-                v-else
-                :ref="setEmojiRef(group.id + emoji.displayText)"
-                class="emoji-picker-emoji -custom"
-                :data-src="emoji.imageUrl"
-                :data-emoji-name="group.id + emoji.displayText"
-              />
-            </span>
-            <span :ref="setGroupRef('group-end-' + group.id)" />
+                v-for="emoji in group.emojis"
+                :key="group.id + emoji.displayText"
+                :title="maybeLocalizedEmojiName(emoji)"
+                class="emoji-item"
+                @click.stop.prevent="onEmoji(emoji)"
+              >
+                <span
+                  v-if="!emoji.imageUrl"
+                  class="emoji-picker-emoji -unicode"
+                >{{ emoji.replacement }}</span>
+                <still-image
+                  v-else
+                  :ref="setEmojiRef(group.id + emoji.displayText)"
+                  class="emoji-picker-emoji -custom"
+                  :data-src="emoji.imageUrl"
+                  :data-emoji-name="group.id + emoji.displayText"
+                />
+              </span>
+              <span :ref="setGroupRef('group-end-' + group.id)" />
+            </div>
+          </div>
+          <div class="keep-open">
+            <Checkbox v-model="keepOpen">
+              {{ $t('emoji.keep_open') }}
+            </Checkbox>
           </div>
         </div>
-        <div class="keep-open">
-          <Checkbox v-model="keepOpen">
-            {{ $t('emoji.keep_open') }}
-          </Checkbox>
+        <div
+          v-if="showingStickers"
+          class="stickers-content"
+        >
+          <sticker-picker
+            @uploaded="onStickerUploaded"
+            @upload-failed="onStickerUploadFailed"
+          />
         </div>
       </div>
-      <div
-        v-if="showingStickers"
-        class="stickers-content"
-      >
-        <sticker-picker
-          @uploaded="onStickerUploaded"
-          @upload-failed="onStickerUploadFailed"
-        />
-      </div>
-    </div>
-  </div>
+    </template>
+  </Popover>
 </template>
 
 <script src="./emoji_picker.js"></script>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 60c42f0f..3474d354 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -63,6 +63,7 @@ const Popover = {
       // used to avoid blinking if hovered onto popover
       graceTimeout: null,
       parentPopover: null,
+      disableClickOutside: false,
       childrenShown: new Set()
     }
   },
@@ -234,6 +235,10 @@ const Popover = {
     },
     showPopover () {
       if (this.disabled) return
+      this.disableClickOutside = true
+      setTimeout(() => {
+        this.disableClickOutside = false
+      }, 0)
       const wasHidden = this.hidden
       this.hidden = false
       this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
@@ -294,6 +299,7 @@ const Popover = {
       }
     },
     onClickOutside (e) {
+      if (this.disableClickOutside) return
       if (this.hidden) return
       if (this.$refs.content && this.$refs.content.contains(e.target)) return
       if (this.$el.contains(e.target)) return
-- 
cgit v1.2.3-70-g09d2


From 692342a77f1b91a88a30aadfd20994c293ad65e0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 10 Oct 2022 20:11:04 +0300
Subject: focus search input when opening emoji picker

---
 src/components/emoji_input/emoji_input.js    | 8 --------
 src/components/emoji_picker/emoji_picker.js  | 1 +
 src/components/emoji_picker/emoji_picker.vue | 1 +
 3 files changed, 2 insertions(+), 8 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index eb64e054..92c10f09 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -278,17 +278,10 @@ const EmojiInput = {
     }
   },
   methods: {
-    focusPickerInput () {
-      const pickerEl = this.$refs.picker.$el
-      if (!pickerEl) return
-      const pickerInput = pickerEl.querySelector('input')
-      if (pickerInput) pickerInput.focus()
-    },
     triggerShowPicker () {
       this.$nextTick(() => {
         this.$refs.picker.showPicker()
         this.scrollIntoView()
-        this.focusPickerInput()
       })
       // This temporarily disables "click outside" handler
       // since external trigger also means click originates
@@ -306,7 +299,6 @@ const EmojiInput = {
         this.scrollIntoView()
         this.$refs.picker.showPicker()
         this.$refs.picker.startEmojiLoad()
-        this.$nextTick(this.focusPickerInput)
       } else {
         this.$refs.picker.hidePicker()
       }
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index e907def9..d1372c4a 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -245,6 +245,7 @@ const EmojiPicker = {
       this.filteredEmojiGroups = this.getFilteredEmojiGroups()
       if (!oldContentLoaded) {
         this.$nextTick(() => {
+          this.$refs.search.focus()
           if (this.defaultGroup) {
             this.highlight(this.defaultGroup)
           }
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index ccd45bc1..a2c99c16 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -71,6 +71,7 @@
               class="form-control"
               :placeholder="$t('emoji.search_emoji')"
               @input="$event.target.composing = false"
+              ref="search"
             >
           </div>
           <div
-- 
cgit v1.2.3-70-g09d2


From 3c0e7882b10989424c3ec98413b555373efa1719 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 21 Nov 2022 22:17:33 +0200
Subject: lint

---
 src/components/emoji_input/emoji_input.vue                 | 13 ++++++++++---
 src/components/emoji_picker/emoji_picker.vue               |  4 ++--
 src/components/mobile_nav/mobile_nav.vue                   |  6 +++---
 src/components/notifications/notifications.vue             |  2 +-
 src/components/popover/popover.vue                         |  5 ++++-
 .../quick_filter_settings/quick_filter_settings.vue        |  2 +-
 src/components/quick_view_settings/quick_view_settings.vue |  2 +-
 src/components/search/search.js                            |  4 ++--
 src/components/timeline/timeline.vue                       | 14 ++++++++++----
 9 files changed, 34 insertions(+), 18 deletions(-)

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

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 63bf856e..c9bbc18f 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -6,9 +6,16 @@
   >
     <slot />
     <!-- TODO: make the 'x' disappear if at the end maybe? -->
-    <div class="hidden-overlay" :style="overlayStyle" ref="hiddenOverlay">
+    <div
+      ref="hiddenOverlay"
+      class="hidden-overlay"
+      :style="overlayStyle"
+    >
       <span>{{ preText }}</span>
-      <span class="caret" ref="hiddenOverlayCaret">x</span>
+      <span
+        ref="hiddenOverlayCaret"
+        class="caret"
+      >x</span>
       <span>{{ postText }}</span>
     </div>
     <template v-if="enableEmojiPicker">
@@ -33,9 +40,9 @@
       />
     </template>
     <Popover
+      ref="suggestorPopover"
       class="autocomplete-panel"
       placement="bottom"
-      ref="suggestorPopover"
     >
       <template #content>
         <div
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index a2c99c16..ff56d637 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,8 +1,8 @@
 <template>
   <Popover
+    ref="popover"
     trigger="click"
     popover-class="emoji-picker popover-default"
-    ref="popover"
     @show="onPopoverShown"
     @close="onPopoverClosed"
   >
@@ -66,12 +66,12 @@
         >
           <div class="emoji-search">
             <input
+              ref="search"
               v-model="keyword"
               type="text"
               class="form-control"
               :placeholder="$t('emoji.search_emoji')"
               @input="$event.target.composing = false"
-              ref="search"
             >
           </div>
           <div
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 6e732d1f..d642008b 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -11,7 +11,7 @@
         <button
           class="button-unstyled mobile-nav-button"
           :title="$t('nav.mobile_sidebar')"
-          :aria-expanaded="this.$refs.sideDrawer && !this.$refs.sideDrawer.closed"
+          :aria-expanaded="$refs.sideDrawer && !$refs.sideDrawer.closed"
           @click.stop.prevent="toggleMobileSidebar()"
         >
           <FAIcon
@@ -51,7 +51,7 @@
     >
       <div class="mobile-notifications-header">
         <span class="title">{{ $t('notifications.notifications') }}</span>
-        <span class="spacer"/>
+        <span class="spacer" />
         <button
           v-if="notificationsAtTop"
           class="button-unstyled mobile-nav-button"
@@ -79,8 +79,8 @@
       </div>
       <div
         id="mobile-notifications"
-        class="mobile-notifications"
         ref="mobileNotifications"
+        class="mobile-notifications"
         @scroll="onScroll"
       />
     </aside>
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 3d5878d4..633efca6 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -22,8 +22,8 @@
             >{{ unseenCount }}</span>
           </div>
           <div
-            class="rightside-button"
             v-if="showScrollTop"
+            class="rightside-button"
           >
             <button
               class="button-unstyled scroll-to-top-button"
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index c2cf2327..2869d736 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -12,7 +12,10 @@
     >
       <slot name="trigger" />
     </button>
-    <teleport :disabled="!teleport" to="#popovers">
+    <teleport
+      :disabled="!teleport"
+      to="#popovers"
+    >
       <transition name="fade">
         <div
           v-if="!hidden"
diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue
index 87fcd716..f2aa61ee 100644
--- a/src/components/quick_filter_settings/quick_filter_settings.vue
+++ b/src/components/quick_filter_settings/quick_filter_settings.vue
@@ -3,7 +3,7 @@
     trigger="click"
     class="QuickFilterSettings"
     :bound-to="{ x: 'container' }"
-    :triggerAttrs="{ title: $t('timeline.quick_filter_settings') }"
+    :trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
   >
     <template #content>
       <div class="dropdown-menu">
diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue
index d7c9bf3b..4bd81c5b 100644
--- a/src/components/quick_view_settings/quick_view_settings.vue
+++ b/src/components/quick_view_settings/quick_view_settings.vue
@@ -3,7 +3,7 @@
     trigger="click"
     class="QuickViewSettings"
     :bound-to="{ x: 'container' }"
-    :triggerAttrs="{ title: $t('timeline.quick_view_settings') }"
+    :trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
   >
     <template #content>
       <div class="dropdown-menu">
diff --git a/src/components/search/search.js b/src/components/search/search.js
index 8d4212cd..877d6f30 100644
--- a/src/components/search/search.js
+++ b/src/components/search/search.js
@@ -83,11 +83,11 @@ const Search = {
         this.lastStatusFetchCount = 0
       }
 
-      this.$store.dispatch('search', { q: query, resolve: true, offset: this.statusesOffset, 'type': searchType })
+      this.$store.dispatch('search', { q: query, resolve: true, offset: this.statusesOffset, type: searchType })
         .then(data => {
           this.loading = false
 
-          let oldLength = this.statuses.length
+          const oldLength = this.statuses.length
 
           // Always append to old results. If new results are empty, this doesn't change anything
           this.userIds = this.userIds.concat(map(data.accounts, 'id'))
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 877a0cc0..2279f21a 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -6,8 +6,8 @@
         :timeline-name="timelineName"
       />
       <div
-        class="rightside-button"
         v-if="showScrollTop && !embedded"
+        class="rightside-button"
       >
         <button
           class="button-unstyled scroll-to-top-button"
@@ -26,8 +26,8 @@
       </div>
       <template v-if="mobileLayout && !embedded">
         <div
-          class="rightside-button"
           v-if="showLoadButton"
+          class="rightside-button"
         >
           <button
             class="button-unstyled loadmore-button"
@@ -72,8 +72,14 @@
           {{ $t('timeline.up_to_date') }}
         </div>
       </template>
-      <QuickFilterSettings v-if="!embedded" class="rightside-button"/>
-      <QuickViewSettings v-if="!embedded" class="rightside-button"/>
+      <QuickFilterSettings
+        v-if="!embedded"
+        class="rightside-button"
+      />
+      <QuickViewSettings
+        v-if="!embedded"
+        class="rightside-button"
+      />
     </div>
     <div :class="classes.body">
       <div
-- 
cgit v1.2.3-70-g09d2

