Browse Source

Merge branch 'from/develop/tusooa/grouped-emoji-picker' into 'develop'

Group emojis into packs in emoji picker

See merge request pleroma/pleroma-fe!1408
HJ 2 years ago
parent
commit
03b61f0a9c

+ 1 - 1
.babelrc

@@ -1,5 +1,5 @@
 {
   "presets": ["@babel/preset-env"],
   "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
-  "comments": false
+  "comments": true
 }

+ 1 - 0
.gitignore

@@ -7,3 +7,4 @@ test/e2e/reports
 selenium-debug.log
 .idea/
 config/local.json
+static/emoji.json

+ 3 - 0
build/build.js

@@ -18,6 +18,9 @@ console.log(
 var spinner = ora('building for production...')
 spinner.start()
 
+var updateEmoji = require('./update-emoji').updateEmoji
+updateEmoji()
+
 var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
 rm('-rf', assetsPath)
 mkdir('-p', assetsPath)

+ 3 - 0
build/dev-server.js

@@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
   ? require('./webpack.prod.conf')
   : require('./webpack.dev.conf')
 
+var updateEmoji = require('./update-emoji').updateEmoji
+updateEmoji()
+
 // default port where dev server listens for incoming traffic
 var port = process.env.PORT || config.dev.port
 // Define HTTP proxies to your custom API backend

+ 27 - 0
build/update-emoji.js

@@ -0,0 +1,27 @@
+
+module.exports = {
+  updateEmoji () {
+    const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
+    const fs = require('fs')
+
+    Object.keys(emojis)
+      .map(k => {
+        emojis[k].map(e => {
+          delete e.unicode_version
+          delete e.emoji_version
+          delete e.skin_tone_support_unicode_version
+        })
+      })
+
+    const res = {}
+    Object.keys(emojis)
+      .map(k => {
+        const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
+        res[groupId] = emojis[k]
+      })
+
+    console.info('Updating emojis...')
+    fs.writeFileSync('static/emoji.json', JSON.stringify(res))
+    console.info('Done.')
+  }
+}

+ 2 - 1
build/webpack.base.conf.js

@@ -24,7 +24,8 @@ module.exports = {
   output: {
     path: config.build.assetsRoot,
     publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
-    filename: '[name].js'
+    filename: '[name].js',
+    chunkFilename: '[name].js'
   },
   optimization: {
     splitChunks: {

+ 2 - 0
package.json

@@ -23,6 +23,7 @@
     "@fortawesome/free-solid-svg-icons": "6.2.0",
     "@fortawesome/vue-fontawesome": "3.0.1",
     "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
+    "@kazvmoe-infra/unicode-emoji-json": "^0.4.0",
     "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
     "@vuelidate/core": "2.0.0-alpha.44",
     "@vuelidate/validators": "2.0.0-alpha.31",
@@ -34,6 +35,7 @@
     "escape-html": "1.0.3",
     "js-cookie": "3.0.1",
     "localforage": "1.10.0",
+    "lozad": "^1.16.0",
     "parse-link-header": "2.0.0",
     "phoenix": "1.6.2",
     "punycode.js": "2.1.0",

+ 47 - 3
src/components/emoji_input/emoji_input.js

@@ -3,7 +3,7 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue'
 import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
 import { take } from 'lodash'
 import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
-
+import { ensureFinalFallback } from '../../i18n/languages.js'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faSmileBeam
@@ -143,6 +143,51 @@ const EmojiInput = {
         const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
         return word
       }
+    },
+    languages () {
+      return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+    },
+    maybeLocalizedEmojiNamesAndKeywords () {
+      return emoji => {
+        const names = [emoji.displayText]
+        const keywords = []
+
+        if (emoji.displayTextI18n) {
+          names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
+        }
+
+        if (emoji.annotations) {
+          this.languages.forEach(lang => {
+            names.push(emoji.annotations[lang]?.name)
+
+            keywords.push(...(emoji.annotations[lang]?.keywords || []))
+          })
+        }
+
+        return {
+          names: names.filter(k => k),
+          keywords: keywords.filter(k => k)
+        }
+      }
+    },
+    maybeLocalizedEmojiName () {
+      return emoji => {
+        if (!emoji.annotations) {
+          return emoji.displayText
+        }
+
+        if (emoji.displayTextI18n) {
+          return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
+        }
+
+        for (const lang of this.languages) {
+          if (emoji.annotations[lang]?.name) {
+            return emoji.annotations[lang].name
+          }
+        }
+
+        return emoji.displayText
+      }
     }
   },
   mounted () {
@@ -181,7 +226,7 @@ const EmojiInput = {
       const firstchar = newWord.charAt(0)
       this.suggestions = []
       if (newWord === firstchar) return
-      const matchedSuggestions = await this.suggest(newWord)
+      const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
       // Async: cancel if textAtCaret has changed during wait
       if (this.textAtCaret !== newWord) return
       if (matchedSuggestions.length <= 0) return
@@ -207,7 +252,6 @@ const EmojiInput = {
     },
     triggerShowPicker () {
       this.showPicker = true
-      this.$refs.picker.startEmojiLoad()
       this.$nextTick(() => {
         this.scrollIntoView()
         this.focusPickerInput()

+ 2 - 1
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"
@@ -63,7 +64,7 @@
               v-if="!suggestion.user"
               class="displayText"
             >
-              {{ suggestion.displayText }}
+              {{ maybeLocalizedEmojiName(suggestion) }}
             </span>
             <span class="detailText">{{ suggestion.detailText }}</span>
           </div>

+ 17 - 17
src/components/emoji_input/suggestor.js

@@ -2,7 +2,7 @@
  * suggest - generates a suggestor function to be used by emoji-input
  * data: object providing source information for specific types of suggestions:
  * data.emoji - optional, an array of all emoji available i.e.
- *   (state.instance.emoji + state.instance.customEmoji)
+ *   (getters.standardEmojiList + state.instance.customEmoji)
  * data.users - optional, an array of all known users
  * updateUsersList - optional, a function to search and append to users
  *
@@ -13,10 +13,10 @@
 export default data => {
   const emojiCurry = suggestEmoji(data.emoji)
   const usersCurry = data.store && suggestUsers(data.store)
-  return input => {
+  return (input, nameKeywordLocalizer) => {
     const firstChar = input[0]
     if (firstChar === ':' && data.emoji) {
-      return emojiCurry(input)
+      return emojiCurry(input, nameKeywordLocalizer)
     }
     if (firstChar === '@' && usersCurry) {
       return usersCurry(input)
@@ -25,34 +25,34 @@ export default data => {
   }
 }
 
-export const suggestEmoji = emojis => input => {
+export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
   const noPrefix = input.toLowerCase().substr(1)
   return emojis
-    .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
-    .sort((a, b) => {
-      let aScore = 0
-      let bScore = 0
+    .map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
+    .filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
+    .map(k => {
+      let score = 0
 
       // An exact match always wins
-      aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
-      bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
+      score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
 
       // Prioritize custom emoji a lot
-      aScore += a.imageUrl ? 100 : 0
-      bScore += b.imageUrl ? 100 : 0
+      score += k.imageUrl ? 100 : 0
 
       // Prioritize prefix matches somewhat
-      aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
-      bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+      score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
 
       // Sort by length
-      aScore -= a.displayText.length
-      bScore -= b.displayText.length
+      score -= k.displayText.length
 
+      k.score = score
+      return k
+    })
+    .sort((a, b) => {
       // Break ties alphabetically
       const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
 
-      return bScore - aScore + alphabetically
+      return b.score - a.score + alphabetically
     })
 }
 

+ 210 - 92
src/components/emoji_picker/emoji_picker.js

@@ -1,33 +1,76 @@
 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 {
   faBoxOpen,
   faStickyNote,
-  faSmileBeam
+  faSmileBeam,
+  faSmile,
+  faUser,
+  faPaw,
+  faIceCream,
+  faBus,
+  faBasketballBall,
+  faLightbulb,
+  faCode,
+  faFlag
 } from '@fortawesome/free-solid-svg-icons'
-import { trim } from 'lodash'
+import { debounce, trim } from 'lodash'
 
 library.add(
   faBoxOpen,
   faStickyNote,
-  faSmileBeam
+  faSmileBeam,
+  faSmile,
+  faUser,
+  faPaw,
+  faIceCream,
+  faBus,
+  faBasketballBall,
+  faLightbulb,
+  faCode,
+  faFlag
 )
 
-// At widest, approximately 20 emoji are visible in a row,
-// loading 3 rows, could be overkill for narrow picker
-const LOAD_EMOJI_BY = 60
+const UNICODE_EMOJI_GROUP_ICON = {
+  'smileys-and-emotion': 'smile',
+  'people-and-body': 'user',
+  'animals-and-nature': 'paw',
+  'food-and-drink': 'ice-cream',
+  'travel-and-places': 'bus',
+  activities: 'basketball-ball',
+  objects: 'lightbulb',
+  symbols: 'code',
+  flags: 'flag'
+}
 
-// When to start loading new batch emoji, in pixels
-const LOAD_EMOJI_MARGIN = 64
+const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
+  const res = [emoji.displayText, nameLocalizer(emoji)]
+  if (emoji.annotations) {
+    languages.forEach(lang => {
+      const keywords = emoji.annotations[lang]?.keywords || []
+      const name = emoji.annotations[lang]?.name
+      res.push(...(keywords.concat([name]).filter(k => k)))
+    })
+  }
+  return res
+}
 
-const filterByKeyword = (list, keyword = '') => {
+const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
   if (keyword === '') return list
 
   const keywordLowercase = keyword.toLowerCase()
   const orderedEmojiList = []
   for (const emoji of list) {
-    const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
+    const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
+      .map(k => k.toLowerCase().indexOf(keywordLowercase))
+      .filter(k => k > -1)
+
+    const indexOfKeyword = indices.length ? Math.min(...indices) : -1
+
     if (indexOfKeyword > -1) {
       if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
         orderedEmojiList[indexOfKeyword] = []
@@ -44,6 +87,10 @@ const EmojiPicker = {
       required: false,
       type: Boolean,
       default: false
+    },
+    showing: {
+      required: true,
+      type: Boolean
     }
   },
   data () {
@@ -53,16 +100,26 @@ const EmojiPicker = {
       showingStickers: false,
       groupsScrolledClass: 'scrolled-top',
       keepOpen: false,
-      customEmojiBufferSlice: LOAD_EMOJI_BY,
       customEmojiTimeout: null,
-      customEmojiLoadAllConfirmed: false
+      // Lazy-load only after the first time `showing` becomes true.
+      contentLoaded: false,
+      groupRefs: {},
+      emojiRefs: {},
+      filteredEmojiGroups: []
     }
   },
   components: {
     StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
-    Checkbox
+    Checkbox,
+    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)
     },
@@ -77,10 +134,38 @@ const EmojiPicker = {
       const target = (e && e.target) || this.$refs['emoji-groups']
       this.updateScrolledClass(target)
       this.scrolledGroup(target)
-      this.triggerLoadMore(target)
+    },
+    scrolledGroup (target) {
+      const top = target.scrollTop + 5
+      this.$nextTick(() => {
+        this.allEmojiGroups.forEach(group => {
+          const ref = this.groupRefs['group-' + group.id]
+          if (ref && ref.offsetTop <= top) {
+            this.activeGroup = group.id
+          }
+        })
+        this.scrollHeader()
+      })
+    },
+    scrollHeader () {
+      // Scroll the active tab's header into view
+      const headerRef = this.groupRefs['group-header-' + this.activeGroup]
+      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 ref = this.groupRefs['group-' + key]
       const top = ref.offsetTop
       this.setShowStickers(false)
       this.activeGroup = key
@@ -97,73 +182,90 @@ const EmojiPicker = {
         this.groupsScrolledClass = 'scrolled-middle'
       }
     },
-    triggerLoadMore (target) {
-      const ref = this.$refs['group-end-custom']
-      if (!ref) return
-      const bottom = ref.offsetTop + ref.offsetHeight
-
-      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 < 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()
-      }
+    toggleStickers () {
+      this.showingStickers = !this.showingStickers
     },
-    scrolledGroup (target) {
-      const top = target.scrollTop + 5
+    setShowStickers (value) {
+      this.showingStickers = value
+    },
+    filterByKeyword (list, keyword) {
+      return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
+    },
+    initializeLazyLoad () {
+      this.destroyLazyLoad()
       this.$nextTick(() => {
-        this.emojisView.forEach(group => {
-          const ref = this.$refs['group-' + group.id]
-          if (ref.offsetTop <= top) {
-            this.activeGroup = group.id
+        this.$lozad = lozad('.still-image.emoji-picker-emoji', {
+          load: el => {
+            const name = el.getAttribute('data-emoji-name')
+            const vn = this.emojiRefs[name]
+            if (!vn) {
+              return
+            }
+
+            vn.loadLazy()
           }
         })
+        this.$lozad.observe()
       })
     },
-    loadEmoji () {
-      const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
-
-      if (allLoaded) {
-        return
-      }
-
-      this.customEmojiBufferSlice += LOAD_EMOJI_BY
+    waitForDomAndInitializeLazyLoad () {
+      this.$nextTick(() => this.initializeLazyLoad())
     },
-    startEmojiLoad (forceUpdate = false) {
-      if (!forceUpdate) {
-        this.keyword = ''
-      }
-      this.$nextTick(() => {
-        this.$refs['emoji-groups'].scrollTop = 0
-      })
-      const bufferSize = this.customEmojiBuffer.length
-      const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
-      if (bufferPrefilledAll && !forceUpdate) {
-        return
+    destroyLazyLoad () {
+      if (this.$lozad) {
+        if (this.$lozad.observer) {
+          this.$lozad.observer.disconnect()
+        }
+        if (this.$lozad.mutationObserver) {
+          this.$lozad.mutationObserver.disconnect()
+        }
       }
-      this.customEmojiBufferSlice = LOAD_EMOJI_BY
     },
-    toggleStickers () {
-      this.showingStickers = !this.showingStickers
+    onShowing () {
+      const oldContentLoaded = this.contentLoaded
+      this.contentLoaded = true
+      this.waitForDomAndInitializeLazyLoad()
+      this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+      if (!oldContentLoaded) {
+        this.$nextTick(() => {
+          if (this.defaultGroup) {
+            this.highlight(this.defaultGroup)
+          }
+        })
+      }
     },
-    setShowStickers (value) {
-      this.showingStickers = value
+    getFilteredEmojiGroups () {
+      return this.allEmojiGroups
+        .map(group => ({
+          ...group,
+          emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
+        }))
+        .filter(group => group.emojis.length > 0)
     }
   },
   watch: {
     keyword () {
-      this.customEmojiLoadAllConfirmed = false
       this.onScroll()
-      this.startEmojiLoad(true)
+      this.debouncedHandleKeywordChange()
+    },
+    allCustomGroups () {
+      this.waitForDomAndInitializeLazyLoad()
+      this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+    },
+    showing (val) {
+      if (val) {
+        this.onShowing()
+      }
     }
   },
+  mounted () {
+    if (this.showing) {
+      this.onShowing()
+    }
+  },
+  destroyed () {
+    this.destroyLazyLoad()
+  },
   computed: {
     activeGroupView () {
       return this.showingStickers ? '' : this.activeGroup
@@ -174,39 +276,55 @@ const EmojiPicker = {
       }
       return 0
     },
-    filteredEmoji () {
-      return filterByKeyword(
-        this.$store.state.instance.customEmoji || [],
-        trim(this.keyword)
-      )
+    allCustomGroups () {
+      return this.$store.getters.groupedCustomEmojis
     },
-    customEmojiBuffer () {
-      return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
+    defaultGroup () {
+      return Object.keys(this.allCustomGroups)[0]
     },
-    emojis () {
-      const standardEmojis = this.$store.state.instance.emoji || []
-      const customEmojis = this.customEmojiBuffer
-
-      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))
-        }
-      ]
+    unicodeEmojiGroups () {
+      return this.$store.getters.standardEmojiGroupList.map(group => ({
+        id: `standard-${group.id}`,
+        text: this.$t(`emoji.unicode_groups.${group.id}`),
+        icon: UNICODE_EMOJI_GROUP_ICON[group.id],
+        emojis: group.emojis
+      }))
     },
-    emojisView () {
-      return this.emojis.filter(value => value.emojis.length > 0)
+    allEmojiGroups () {
+      return Object.entries(this.allCustomGroups)
+        .map(([_, v]) => v)
+        .concat(this.unicodeEmojiGroups)
     },
     stickerPickerEnabled () {
       return (this.$store.state.instance.stickers || []).length !== 0
+    },
+    debouncedHandleKeywordChange () {
+      return debounce(() => {
+        this.waitForDomAndInitializeLazyLoad()
+        this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+      }, 500)
+    },
+    languages () {
+      return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+    },
+    maybeLocalizedEmojiName () {
+      return emoji => {
+        if (!emoji.annotations) {
+          return emoji.displayText
+        }
+
+        if (emoji.displayTextI18n) {
+          return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
+        }
+
+        for (const lang of this.languages) {
+          if (emoji.annotations[lang]?.name) {
+            return emoji.annotations[lang].name
+          }
+        }
+
+        return emoji.displayText
+      }
     }
   }
 }

+ 44 - 8
src/components/emoji_picker/emoji_picker.scss

@@ -1,5 +1,10 @@
 @import '../../_variables.scss';
 
+$emoji-picker-header-height: 36px;
+$emoji-picker-header-picture-width: 32px;
+$emoji-picker-header-picture-height: 32px;
+$emoji-picker-emoji-size: 32px;
+
 .emoji-picker {
   display: flex;
   flex-direction: column;
@@ -19,6 +24,23 @@
   --lightText: var(--popoverLightText, $fallback--lightText);
   --icon: var(--popoverIcon, $fallback--icon);
 
+  &-header-image {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: $emoji-picker-header-picture-width;
+    max-width: $emoji-picker-header-picture-width;
+    height: $emoji-picker-header-picture-height;
+    max-height: $emoji-picker-header-picture-height;
+    .still-image {
+      max-width: 100%;
+      max-height: 100%;
+      height: 100%;
+      width: 100%;
+      object-fit: contain;
+    }
+  }
+
   .keep-open,
   .too-many-emoji {
     padding: 7px;
@@ -37,7 +59,6 @@
 
   .heading {
     display: flex;
-    height: 32px;
     padding: 10px 7px 5px;
   }
 
@@ -50,6 +71,10 @@
 
   .emoji-tabs {
     flex-grow: 1;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    overflow-x: auto;
   }
 
   .emoji-groups {
@@ -57,6 +82,8 @@
   }
 
   .additional-tabs {
+    display: flex;
+    flex: 1;
     border-left: 1px solid;
     border-left-color: $fallback--icon;
     border-left-color: var(--icon, $fallback--icon);
@@ -66,15 +93,20 @@
 
   .additional-tabs,
   .emoji-tabs {
-    display: block;
-    min-width: 0;
     flex-basis: auto;
-    flex-shrink: 1;
+    display: flex;
+    align-content: center;
 
     &-item {
       padding: 0 7px;
       cursor: pointer;
       font-size: 1.85em;
+      width: $emoji-picker-header-picture-width;
+      max-width: $emoji-picker-header-picture-width;
+      height: $emoji-picker-header-picture-height;
+      max-height: $emoji-picker-header-picture-height;
+      display: flex;
+      align-items: center;
 
       &.disabled {
         opacity: 0.5;
@@ -164,22 +196,26 @@
     }
 
     &-item {
-      width: 32px;
-      height: 32px;
+      width: $emoji-picker-emoji-size;
+      height: $emoji-picker-emoji-size;
       box-sizing: border-box;
       display: flex;
-      font-size: 32px;
+      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;
+      }
     }
 
   }

+ 38 - 14
src/components/emoji_picker/emoji_picker.vue

@@ -1,19 +1,34 @@
 <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
+        ref="header"
+        class="emoji-tabs"
+      >
         <span
-          v-for="group in emojis"
+          v-for="group in filteredEmojiGroups"
+          :ref="setGroupRef('group-header-' + group.id)"
           :key="group.id"
           class="emoji-tabs-item"
           :class="{
-            active: activeGroupView === group.id,
-            disabled: group.emojis.length === 0
+            active: activeGroupView === group.id
           }"
           :title="group.text"
           @click.prevent="highlight(group.id)"
         >
+          <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
           />
@@ -36,7 +51,10 @@
         </span>
       </span>
     </div>
-    <div class="content">
+    <div
+      v-if="contentLoaded"
+      class="content"
+    >
       <div
         class="emoji-content"
         :class="{hidden: showingStickers}"
@@ -57,12 +75,12 @@
           @scroll="onScroll"
         >
           <div
-            v-for="group in emojisView"
+            v-for="group in filteredEmojiGroups"
             :key="group.id"
             class="emoji-group"
           >
             <h6
-              :ref="'group-' + group.id"
+              :ref="setGroupRef('group-' + group.id)"
               class="emoji-group-title"
             >
               {{ group.text }}
@@ -70,17 +88,23 @@
             <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)"
             >
-              <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
-              <img
+              <span
+                v-if="!emoji.imageUrl"
+                class="emoji-picker-emoji -unicode"
+              >{{ emoji.replacement }}</span>
+              <still-image
                 v-else
-                :src="emoji.imageUrl"
-              >
+                :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="'group-end-' + group.id" />
+            <span :ref="setGroupRef('group-end-' + group.id)" />
           </div>
         </div>
         <div class="keep-open">

+ 3 - 3
src/components/post_status_form/post_status_form.js

@@ -189,7 +189,7 @@ const PostStatusForm = {
     emojiUserSuggestor () {
       return suggestor({
         emoji: [
-          ...this.$store.state.instance.emoji,
+          ...this.$store.getters.standardEmojiList,
           ...this.$store.state.instance.customEmoji
         ],
         store: this.$store
@@ -198,13 +198,13 @@ const PostStatusForm = {
     emojiSuggestor () {
       return suggestor({
         emoji: [
-          ...this.$store.state.instance.emoji,
+          ...this.$store.getters.standardEmojiList,
           ...this.$store.state.instance.customEmoji
         ]
       })
     },
     emoji () {
-      return this.$store.state.instance.emoji || []
+      return this.$store.getters.standardEmojiList || []
     },
     customEmoji () {
       return this.$store.state.instance.customEmoji || []

+ 2 - 2
src/components/react_button/react_button.js

@@ -59,7 +59,7 @@ const ReactButton = {
       if (this.filterWord !== '') {
         const filterWordLowercase = trim(this.filterWord.toLowerCase())
         const orderedEmojiList = []
-        for (const emoji of this.$store.state.instance.emoji) {
+        for (const emoji of this.$store.getters.standardEmojiList) {
           if (emoji.replacement === this.filterWord) return [emoji]
 
           const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
@@ -72,7 +72,7 @@ const ReactButton = {
         }
         return orderedEmojiList.flat()
       }
-      return this.$store.state.instance.emoji || []
+      return this.$store.getters.standardEmojiList || []
     },
     mergedConfig () {
       return this.$store.getters.mergedConfig

+ 2 - 2
src/components/settings_modal/tabs/profile_tab.js

@@ -64,7 +64,7 @@ const ProfileTab = {
     emojiUserSuggestor () {
       return suggestor({
         emoji: [
-          ...this.$store.state.instance.emoji,
+          ...this.$store.getters.standardEmojiList,
           ...this.$store.state.instance.customEmoji
         ],
         store: this.$store
@@ -73,7 +73,7 @@ const ProfileTab = {
     emojiSuggestor () {
       return suggestor({
         emoji: [
-          ...this.$store.state.instance.emoji,
+          ...this.$store.getters.standardEmojiList,
           ...this.$store.state.instance.customEmoji
         ]
       })

+ 25 - 2
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)
@@ -42,6 +57,14 @@ const StillImage = {
     onError () {
       this.imageLoadError && this.imageLoadError()
     }
+  },
+  watch: {
+    src () {
+      this.realSrc = this.src
+    },
+    dataSrc () {
+      this.$el.removeAttribute('data-loaded')
+    }
   }
 }
 

+ 3 - 2
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"

+ 13 - 1
src/i18n/en.json

@@ -199,8 +199,20 @@
     "add_emoji": "Insert emoji",
     "custom": "Custom emoji",
     "unicode": "Unicode emoji",
+    "unicode_groups": {
+      "activities": "Activities",
+      "animals-and-nature": "Animals & Nature",
+      "flags": "Flags",
+      "food-and-drink": "Food & Drink",
+      "objects": "Objects",
+      "people-and-body": "People & Body",
+      "smileys-and-emotion": "Smileys & Emotion",
+      "symbols": "Symbols",
+      "travel-and-places": "Travel & Places"
+    },
     "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
-    "load_all": "Loading all {emojiAmount} emoji"
+    "load_all": "Loading all {emojiAmount} emoji",
+    "regional_indicator": "Regional indicator {letter}"
   },
   "errors": {
     "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."

+ 53 - 0
src/i18n/languages.js

@@ -0,0 +1,53 @@
+
+const languages = [
+  'ar',
+  'ca',
+  'cs',
+  'de',
+  'eo',
+  'en',
+  'es',
+  'et',
+  'eu',
+  'fi',
+  'fr',
+  'ga',
+  'he',
+  'hu',
+  'it',
+  'ja',
+  'ja_easy',
+  'ko',
+  'nb',
+  'nl',
+  'oc',
+  'pl',
+  'pt',
+  'ro',
+  'ru',
+  'sk',
+  'te',
+  'uk',
+  'zh',
+  'zh_Hant'
+]
+
+const specialJsonName = {
+  ja: 'ja_pedantic'
+}
+
+const langCodeToJsonName = (code) => specialJsonName[code] || code
+
+const langCodeToCldrName = (code) => code
+
+const ensureFinalFallback = codes => {
+  const codeList = Array.isArray(codes) ? codes : [codes]
+  return codeList.includes('en') ? codeList : codeList.concat(['en'])
+}
+
+module.exports = {
+  languages,
+  langCodeToJsonName,
+  langCodeToCldrName,
+  ensureFinalFallback
+}

+ 13 - 33
src/i18n/messages.js

@@ -7,46 +7,26 @@
 // sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
 // There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
 
-const loaders = {
-  ar: () => import('./ar.json'),
-  ca: () => import('./ca.json'),
-  cs: () => import('./cs.json'),
-  de: () => import('./de.json'),
-  eo: () => import('./eo.json'),
-  es: () => import('./es.json'),
-  et: () => import('./et.json'),
-  eu: () => import('./eu.json'),
-  fi: () => import('./fi.json'),
-  fr: () => import('./fr.json'),
-  ga: () => import('./ga.json'),
-  he: () => import('./he.json'),
-  hu: () => import('./hu.json'),
-  it: () => import('./it.json'),
-  ja: () => import('./ja_pedantic.json'),
-  ja_easy: () => import('./ja_easy.json'),
-  ko: () => import('./ko.json'),
-  nb: () => import('./nb.json'),
-  nl: () => import('./nl.json'),
-  oc: () => import('./oc.json'),
-  pl: () => import('./pl.json'),
-  pt: () => import('./pt.json'),
-  ro: () => import('./ro.json'),
-  ru: () => import('./ru.json'),
-  sk: () => import('./sk.json'),
-  te: () => import('./te.json'),
-  uk: () => import('./uk.json'),
-  zh: () => import('./zh.json'),
-  zh_Hant: () => import('./zh_Hant.json')
+import { languages, langCodeToJsonName } from './languages.js'
+
+const hasLanguageFile = (code) => languages.includes(code)
+
+const loadLanguageFile = (code) => {
+  return import(
+    /* webpackInclude: /\.json$/ */
+    /* webpackChunkName: "i18n/[request]" */
+    `./${langCodeToJsonName(code)}.json`
+  )
 }
 
 const messages = {
-  languages: ['en', ...Object.keys(loaders)],
+  languages,
   default: {
     en: require('./en.json').default
   },
   setLanguage: async (i18n, language) => {
-    if (loaders[language]) {
-      const messages = await loaders[language]()
+    if (hasLanguageFile(language)) {
+      const messages = await loadLanguageFile(language)
       i18n.setLocaleMessage(language, messages.default)
     }
     i18n.locale = language

+ 1 - 0
src/modules/config.js

@@ -183,6 +183,7 @@ const config = {
           break
         case 'interfaceLanguage':
           messages.setLanguage(this.getters.i18n, value)
+          dispatch('loadUnicodeEmojiData', value)
           Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
           break
         case 'thirdColumnMode':

+ 133 - 16
src/modules/instance.js

@@ -2,6 +2,39 @@ import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
 import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
 import apiService from '../services/api/api.service.js'
 import { instanceDefaultProperties } from './config.js'
+import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
+
+const SORTED_EMOJI_GROUP_IDS = [
+  'smileys-and-emotion',
+  'people-and-body',
+  'animals-and-nature',
+  'food-and-drink',
+  'travel-and-places',
+  'activities',
+  'objects',
+  'symbols',
+  'flags'
+]
+
+const REGIONAL_INDICATORS = (() => {
+  const start = 0x1F1E6
+  const end = 0x1F1FF
+  const A = 'A'.codePointAt(0)
+  const res = new Array(end - start + 1)
+  for (let i = start; i <= end; ++i) {
+    const letter = String.fromCodePoint(A + i - start)
+    res[i - start] = {
+      replacement: String.fromCodePoint(i),
+      imageUrl: false,
+      displayText: 'regional_indicator_' + letter,
+      displayTextI18n: {
+        key: 'emoji.regional_indicator',
+        args: { letter }
+      }
+    }
+  }
+  return res
+})()
 
 const defaultState = {
   // Stuff from apiConfig
@@ -64,8 +97,9 @@ const defaultState = {
   // Nasty stuff
   customEmoji: [],
   customEmojiFetched: false,
-  emoji: [],
+  emoji: {},
   emojiFetched: false,
+  unicodeEmojiAnnotations: {},
   pleromaBackend: true,
   postFormats: [],
   restrictedNicknames: [],
@@ -97,6 +131,31 @@ const defaultState = {
   }
 }
 
+const loadAnnotations = (lang) => {
+  return import(
+    /* webpackChunkName: "emoji-annotations/[request]" */
+    `@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json`
+  )
+    .then(k => k.default)
+}
+
+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 injectRegionalIndicators = groups => {
+  groups.symbols.push(...REGIONAL_INDICATORS)
+  return groups
+}
+
 const instance = {
   state: defaultState,
   mutations: {
@@ -107,6 +166,9 @@ const instance = {
     },
     setKnownDomains (state, domains) {
       state.knownDomains = domains
+    },
+    setUnicodeEmojiAnnotations (state, { lang, annotations }) {
+      state.unicodeEmojiAnnotations[lang] = annotations
     }
   },
   getters: {
@@ -115,6 +177,41 @@ const instance = {
         .map(key => [key, state[key]])
         .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
     },
+    groupedCustomEmojis (state) {
+      const packsOf = emoji => {
+        return emoji.tags
+          .filter(k => k.startsWith('pack:'))
+          .map(k => k.slice(5)) // remove 'pack:' prefix
+      }
+
+      return state.customEmoji
+        .reduce((res, emoji) => {
+          packsOf(emoji).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
+        }, {})
+    },
+    standardEmojiList (state) {
+      return SORTED_EMOJI_GROUP_IDS
+        .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] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
+      }))
+    },
     instanceDomain (state) {
       return new URL(state.server).hostname
     }
@@ -138,32 +235,52 @@ const instance = {
     },
     async getStaticEmoji ({ commit }) {
       try {
-        const res = await window.fetch('/static/emoji.json')
-        if (res.ok) {
-          const values = await res.json()
-          const emoji = Object.keys(values).map((key) => {
-            return {
-              displayText: key,
-              imageUrl: false,
-              replacement: values[key]
-            }
-          }).sort((a, b) => a.name > b.name ? 1 : -1)
-          commit('setInstanceOption', { name: 'emoji', value: emoji })
-        } else {
-          throw (res)
-        }
+        const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
+
+        const emoji = Object.keys(values).reduce((res, groupId) => {
+          res[groupId] = values[groupId].map(e => ({
+            displayText: e.slug,
+            imageUrl: false,
+            replacement: e.emoji
+          }))
+          return res
+        }, {})
+        commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
       } catch (e) {
         console.warn("Can't load static emoji")
         console.warn(e)
       }
     },
 
+    loadUnicodeEmojiData ({ commit, state }, language) {
+      const langList = ensureFinalFallback(language)
+
+      return Promise.all(
+        langList
+          .map(async lang => {
+            if (!state.unicodeEmojiAnnotations[lang]) {
+              const annotations = await loadAnnotations(lang)
+              commit('setUnicodeEmojiAnnotations', { lang, annotations })
+            }
+          }))
+    },
+
     async getCustomEmoji ({ commit, state }) {
       try {
         const res = await window.fetch('/api/pleroma/emoji.json')
         if (res.ok) {
           const result = await res.json()
           const values = Array.isArray(result) ? Object.assign({}, ...result) : result
+          const caseInsensitiveStrCmp = (a, b) => {
+            const la = a.toLowerCase()
+            const lb = b.toLowerCase()
+            return la > lb ? 1 : (la < lb ? -1 : 0)
+          }
+          const byPackThenByName = (a, b) => {
+            const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
+            return caseInsensitiveStrCmp(packOf(a), packOf(b)) || caseInsensitiveStrCmp(a.displayText, b.displayText)
+          }
+
           const emoji = Object.entries(values).map(([key, value]) => {
             const imageUrl = value.image_url
             return {
@@ -174,7 +291,7 @@ const instance = {
             }
             // Technically could use tags but those are kinda useless right now,
             // should have been "pack" field, that would be more useful
-          }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : -1)
+          }).sort(byPackThenByName)
           commit('setInstanceOption', { name: 'customEmoji', value: emoji })
         } else {
           throw (res)

+ 0 - 1431
static/emoji.json

@@ -1,1431 +0,0 @@
-{
-  "100": "๐Ÿ’ฏ",
-  "1234": "๐Ÿ”ข",
-  "1st_place_medal": "๐Ÿฅ‡",
-  "2nd_place_medal": "๐Ÿฅˆ",
-  "3rd_place_medal": "๐Ÿฅ‰",
-  "8ball": "๐ŸŽฑ",
-  "a_button_blood_type": "๐Ÿ…ฐ",
-  "ab": "๐Ÿ†Ž",
-  "abacus": "๐Ÿงฎ",
-  "abc": "๐Ÿ”ค",
-  "abcd": "๐Ÿ”ก",
-  "accept": "๐Ÿ‰‘",
-  "adhesive_bandage": "๐Ÿฉน",
-  "admission_tickets": "๐ŸŽŸ",
-  "adult": "๐Ÿง‘",
-  "aerial_tramway": "๐Ÿšก",
-  "airplane": "โœˆ",
-  "airplane_arriving": "๐Ÿ›ฌ",
-  "airplane_departure": "๐Ÿ›ซ",
-  "alarm_clock": "โฐ",
-  "alembic": "โš—๏ธ",
-  "alien": "๐Ÿ‘ฝ",
-  "ambulance": "๐Ÿš‘",
-  "amphora": "๐Ÿบ",
-  "anchor": "โš“",
-  "angel": "๐Ÿ‘ผ",
-  "anger": "๐Ÿ’ข",
-  "anger_right": "๐Ÿ—ฏ",
-  "angry": "๐Ÿ˜ ",
-  "anguished": "๐Ÿ˜ง",
-  "ant": "๐Ÿœ",
-  "apple": "๐ŸŽ",
-  "aquarius": "โ™’",
-  "aries": "โ™ˆ",
-  "arrow_backward": "โ—€๏ธ",
-  "arrow_double_down": "โฌ",
-  "arrow_double_up": "โซ",
-  "arrow_down": "โฌ‡๏ธ",
-  "arrow_down_small": "๐Ÿ”ฝ",
-  "arrow_forward": "โ–ถ๏ธ",
-  "arrow_heading_down": "โคต๏ธ",
-  "arrow_heading_up": "โคด๏ธ",
-  "arrow_left": "โฌ…๏ธ",
-  "arrow_lower_left": "โ†™๏ธ",
-  "arrow_lower_right": "โ†˜๏ธ",
-  "arrow_right": "โžก",
-  "arrow_right_hook": "โ†ช๏ธ",
-  "arrow_up": "โฌ†๏ธ",
-  "arrow_up_down": "โ†•",
-  "arrow_up_small": "๐Ÿ”ผ",
-  "arrow_upper_left": "โ†–",
-  "arrow_upper_right": "โ†—๏ธ",
-  "arrows_clockwise": "๐Ÿ”ƒ",
-  "arrows_counterclockwise": "๐Ÿ”„",
-  "art": "๐ŸŽจ",
-  "articulated_lorry": "๐Ÿš›",
-  "artist_palette": "๐ŸŽจ",
-  "asterisk": "*โƒฃ",
-  "astonished": "๐Ÿ˜ฒ",
-  "athletic_shoe": "๐Ÿ‘Ÿ",
-  "atm": "๐Ÿง",
-  "atom": "โš›",
-  "atom_symbol": "โš›๏ธ",
-  "auto_rickshaw": "๐Ÿ›บ",
-  "automobile": "๐Ÿš—",
-  "avocado": "๐Ÿฅ‘",
-  "axe": "๐Ÿช“",
-  "b_button_blood_type": "๐Ÿ…ฑ",
-  "baby": "๐Ÿ‘ถ",
-  "baby_bottle": "๐Ÿผ",
-  "baby_chick": "๐Ÿค",
-  "baby_symbol": "๐Ÿšผ",
-  "back": "๐Ÿ”™",
-  "bacon": "๐Ÿฅ“",
-  "badger": "๐Ÿฆก",
-  "badminton": "๐Ÿธ",
-  "bagel": "๐Ÿฅฏ",
-  "baggage_claim": "๐Ÿ›„",
-  "baguette_bread": "๐Ÿฅ–",
-  "balance_scale": "โš–๏ธ",
-  "bald": "๐Ÿฆฒ",
-  "ballet_shoes": "๐Ÿฉฐ",
-  "balloon": "๐ŸŽˆ",
-  "ballot_box": "๐Ÿ—ณ",
-  "ballot_box_with_check": "โ˜‘๏ธ",
-  "bamboo": "๐ŸŽ",
-  "banana": "๐ŸŒ",
-  "bangbang": "โ€ผ๏ธ",
-  "banjo": "๐Ÿช•",
-  "bank": "๐Ÿฆ",
-  "bar_chart": "๐Ÿ“Š",
-  "barber": "๐Ÿ’ˆ",
-  "baseball": "โšพ",
-  "basket": "๐Ÿงบ",
-  "basketball": "๐Ÿ€",
-  "basketballer": "โ›น",
-  "bat": "๐Ÿฆ‡",
-  "bath": "๐Ÿ›€",
-  "bathtub": "๐Ÿ›",
-  "battery": "๐Ÿ”‹",
-  "beach_umbrella": "โ›ฑ",
-  "beach_with_umbrella": "๐Ÿ–",
-  "bear": "๐Ÿป",
-  "beard": "๐Ÿง”",
-  "bearded_person": "๐Ÿง”",
-  "bed": "๐Ÿ›",
-  "bee": "๐Ÿ",
-  "beer": "๐Ÿบ",
-  "beers": "๐Ÿป",
-  "beetle": "๐Ÿž",
-  "beginner": "๐Ÿ”ฐ",
-  "bell": "๐Ÿ””",
-  "bellhop_bell": "๐Ÿ›Ž",
-  "bento": "๐Ÿฑ",
-  "beverage_box": "๐Ÿงƒ",
-  "bicyclist": "๐Ÿšด",
-  "bike": "๐Ÿšฒ",
-  "bikini": "๐Ÿ‘™",
-  "billed_cap": "๐Ÿงข",
-  "biohazard": "โ˜ฃ๏ธ",
-  "bird": "๐Ÿฆ",
-  "birthday": "๐ŸŽ‚",
-  "black_circle": "โšซ",
-  "black_heart": "๐Ÿ–ค",
-  "black_joker": "๐Ÿƒ",
-  "black_large_square": "โฌ›",
-  "black_medium_small_square": "โ—พ",
-  "black_medium_square": "โ—ผ",
-  "black_nib": "โœ’๏ธ",
-  "black_small_square": "โ–ช",
-  "black_square_button": "๐Ÿ”ฒ",
-  "blond_haired_person": "๐Ÿ‘ฑ",
-  "blossom": "๐ŸŒผ",
-  "blowfish": "๐Ÿก",
-  "blue_book": "๐Ÿ“˜",
-  "blue_car": "๐Ÿš™",
-  "blue_circle": "๐Ÿ”ต",
-  "blue_heart": "๐Ÿ’™",
-  "blue_square": "๐ŸŸฆ",
-  "blush": "๐Ÿ˜Š",
-  "boar": "๐Ÿ—",
-  "bomb": "๐Ÿ’ฃ",
-  "bone": "๐Ÿฆด",
-  "book": "๐Ÿ“–",
-  "bookmark": "๐Ÿ”–",
-  "bookmark_tabs": "๐Ÿ“‘",
-  "books": "๐Ÿ“š",
-  "boom": "๐Ÿ’ฅ",
-  "boot": "๐Ÿ‘ข",
-  "bouquet": "๐Ÿ’",
-  "bow": "๐Ÿ™‡",
-  "bow_and_arrow": "๐Ÿน",
-  "bowl_with_spoon": "๐Ÿฅฃ",
-  "bowling": "๐ŸŽณ",
-  "boxing_glove": "๐ŸฅŠ",
-  "boy": "๐Ÿ‘ฆ",
-  "brain": "๐Ÿง ",
-  "bread": "๐Ÿž",
-  "breast_feeding": "๐Ÿคฑ",
-  "breastfeeding": "๐Ÿคฑ",
-  "brick": "๐Ÿงฑ",
-  "bride_with_veil": "๐Ÿ‘ฐ",
-  "bridge_at_night": "๐ŸŒ‰",
-  "briefcase": "๐Ÿ’ผ",
-  "briefs": "๐Ÿฉฒ",
-  "broccoli": "๐Ÿฅฆ",
-  "broken_heart": "๐Ÿ’”",
-  "broom": "๐Ÿงน",
-  "brown_circle": "๐ŸŸค",
-  "brown_heart": "๐ŸคŽ",
-  "bug": "๐Ÿ›",
-  "building_construction": "๐Ÿ—",
-  "bulb": "๐Ÿ’ก",
-  "bullettrain_front": "๐Ÿš…",
-  "bullettrain_side": "๐Ÿš„",
-  "burrito": "๐ŸŒฏ",
-  "bus": "๐ŸšŒ",
-  "busstop": "๐Ÿš",
-  "bust_in_silhouette": "๐Ÿ‘ค",
-  "busts_in_silhouette": "๐Ÿ‘ฅ",
-  "butter": "๐Ÿงˆ",
-  "butterfly": "๐Ÿฆ‹",
-  "cactus": "๐ŸŒต",
-  "cake": "๐Ÿฐ",
-  "calendar": "๐Ÿ“†",
-  "call_me": "๐Ÿค™",
-  "call_me_hand": "๐Ÿค™",
-  "calling": "๐Ÿ“ฒ",
-  "camel": "๐Ÿซ",
-  "camera": "๐Ÿ“ท",
-  "camera_with_flash": "๐Ÿ“ธ",
-  "camping": "๐Ÿ•",
-  "cancer": "โ™‹",
-  "candle": "๐Ÿ•ฏ",
-  "candy": "๐Ÿฌ",
-  "canned_food": "๐Ÿฅซ",
-  "canoe": "๐Ÿ›ถ",
-  "capital_abcd": "๐Ÿ” ",
-  "capricorn": "โ™‘",
-  "card_file_box": "๐Ÿ—ƒ",
-  "card_index": "๐Ÿ“‡",
-  "card_index_dividers": "๐Ÿ—‚",
-  "carousel_horse": "๐ŸŽ ",
-  "carrot": "๐Ÿฅ•",
-  "cat": "๐Ÿฑ",
-  "cat2": "๐Ÿˆ",
-  "cd": "๐Ÿ’ฟ",
-  "chains": "โ›“๏ธ",
-  "chair": "๐Ÿช‘",
-  "champagne": "๐Ÿพ",
-  "champagne_glass": "๐Ÿฅ‚",
-  "chart": "๐Ÿ’น",
-  "chart_with_downwards_trend": "๐Ÿ“‰",
-  "chart_with_upwards_trend": "๐Ÿ“ˆ",
-  "check_box_with_check": "โ˜‘",
-  "check_mark": "โœ”",
-  "checkered_flag": "๐Ÿ",
-  "cheese": "๐Ÿง€",
-  "cheese_wedge": "๐Ÿง€",
-  "cherries": "๐Ÿ’",
-  "cherry_blossom": "๐ŸŒธ",
-  "chess_pawn": "โ™Ÿ",
-  "chestnut": "๐ŸŒฐ",
-  "chicken": "๐Ÿ”",
-  "child": "๐Ÿง’",
-  "children_crossing": "๐Ÿšธ",
-  "chipmunk": "๐Ÿฟ",
-  "chocolate_bar": "๐Ÿซ",
-  "chopsticks": "๐Ÿฅข",
-  "christmas_tree": "๐ŸŽ„",
-  "church": "โ›ช",
-  "cinema": "๐ŸŽฆ",
-  "circled_m": "โ“‚",
-  "circus_tent": "๐ŸŽช",
-  "city_dusk": "๐ŸŒ†",
-  "city_sunset": "๐ŸŒ‡",
-  "cityscape": "๐Ÿ™",
-  "cityscape_at_dusk": "๐ŸŒ†",
-  "cl": "๐Ÿ†‘",
-  "clap": "๐Ÿ‘",
-  "clapper": "๐ŸŽฌ",
-  "classical_building": "๐Ÿ›",
-  "clinking_glasses": "๐Ÿฅ‚",
-  "clipboard": "๐Ÿ“‹",
-  "clock1": "๐Ÿ•",
-  "clock10": "๐Ÿ•™",
-  "clock1030": "๐Ÿ•ฅ",
-  "clock11": "๐Ÿ•š",
-  "clock1130": "๐Ÿ•ฆ",
-  "clock12": "๐Ÿ•›",
-  "clock1230": "๐Ÿ•ง",
-  "clock130": "๐Ÿ•œ",
-  "clock2": "๐Ÿ•‘",
-  "clock230": "๐Ÿ•",
-  "clock3": "๐Ÿ•’",
-  "clock330": "๐Ÿ•ž",
-  "clock4": "๐Ÿ•“",
-  "clock430": "๐Ÿ•Ÿ",
-  "clock5": "๐Ÿ•”",
-  "clock530": "๐Ÿ• ",
-  "clock6": "๐Ÿ••",
-  "clock630": "๐Ÿ•ก",
-  "clock7": "๐Ÿ•–",
-  "clock730": "๐Ÿ•ข",
-  "clock8": "๐Ÿ•—",
-  "clock830": "๐Ÿ•ฃ",
-  "clock9": "๐Ÿ•˜",
-  "clock930": "๐Ÿ•ค",
-  "closed_book": "๐Ÿ“•",
-  "closed_lock_with_key": "๐Ÿ”",
-  "closed_umbrella": "๐ŸŒ‚",
-  "cloud": "โ˜๏ธ",
-  "cloud_with_lightning": "๐ŸŒฉ",
-  "cloud_with_lightning_and_rain": "โ›ˆ๏ธ",
-  "cloud_with_rain": "๐ŸŒง",
-  "cloud_with_snow": "๐ŸŒจ",
-  "clown": "๐Ÿคก",
-  "clown_face": "๐Ÿคก",
-  "club_suit": "โ™ฃ๏ธ",
-  "clubs": "โ™ฃ",
-  "coat": "๐Ÿงฅ",
-  "cocktail": "๐Ÿธ",
-  "coconut": "๐Ÿฅฅ",
-  "coffee": "โ˜•",
-  "coffin": "โšฐ๏ธ",
-  "cold_face": "๐Ÿฅถ",
-  "cold_sweat": "๐Ÿ˜ฐ",
-  "comet": "โ˜„๏ธ",
-  "compass": "๐Ÿงญ",
-  "compression": "๐Ÿ—œ",
-  "computer": "๐Ÿ’ป",
-  "computer_mouse": "๐Ÿ–ฑ",
-  "confetti_ball": "๐ŸŽŠ",
-  "confounded": "๐Ÿ˜–",
-  "confused": "๐Ÿ˜•",
-  "congratulations": "ใŠ—",
-  "construction": "๐Ÿšง",
-  "construction_worker": "๐Ÿ‘ท",
-  "control_knobs": "๐ŸŽ›",
-  "convenience_store": "๐Ÿช",
-  "cookie": "๐Ÿช",
-  "cooking": "๐Ÿณ",
-  "cool": "๐Ÿ†’",
-  "cop": "๐Ÿ‘ฎ",
-  "copyright": "ยฉ",
-  "corn": "๐ŸŒฝ",
-  "couch_and_lamp": "๐Ÿ›‹",
-  "couple": "๐Ÿ‘ซ",
-  "couple_with_heart": "๐Ÿ’‘",
-  "couplekiss": "๐Ÿ’",
-  "cow": "๐Ÿฎ",
-  "cow2": "๐Ÿ„",
-  "cowboy": "๐Ÿค ",
-  "cowboy_hat_face": "๐Ÿค ",
-  "crab": "๐Ÿฆ€",
-  "crayon": "๐Ÿ–",
-  "crazy_face": "๐Ÿคช",
-  "credit_card": "๐Ÿ’ณ",
-  "crescent_moon": "๐ŸŒ™",
-  "cricket": "๐Ÿฆ—",
-  "cricket_game": "๐Ÿ",
-  "crocodile": "๐ŸŠ",
-  "croissant": "๐Ÿฅ",
-  "cross": "โœ๏ธ",
-  "crossed_fingers": "๐Ÿคž",
-  "crossed_flags": "๐ŸŽŒ",
-  "crossed_swords": "โš”๏ธ",
-  "crown": "๐Ÿ‘‘",
-  "cry": "๐Ÿ˜ข",
-  "crying_cat_face": "๐Ÿ˜ฟ",
-  "crystal_ball": "๐Ÿ”ฎ",
-  "cucumber": "๐Ÿฅ’",
-  "cup_with_straw": "๐Ÿฅค",
-  "cupcake": "๐Ÿง",
-  "cupid": "๐Ÿ’˜",
-  "curling_stone": "๐ŸฅŒ",
-  "curly_hair": "๐Ÿฆฑ",
-  "curly_loop": "โžฐ",
-  "currency_exchange": "๐Ÿ’ฑ",
-  "curry": "๐Ÿ›",
-  "custard": "๐Ÿฎ",
-  "customs": "๐Ÿ›ƒ",
-  "cut_of_meat": "๐Ÿฅฉ",
-  "cyclone": "๐ŸŒ€",
-  "dagger": "๐Ÿ—ก",
-  "dancer": "๐Ÿ’ƒ",
-  "dancers": "๐Ÿ‘ฏ",
-  "dango": "๐Ÿก",
-  "dark_skin_tone": "๐Ÿฟ",
-  "dark_sunglasses": "๐Ÿ•ถ",
-  "dart": "๐ŸŽฏ",
-  "dash": "๐Ÿ’จ",
-  "date": "๐Ÿ“…",
-  "deaf_person": "๐Ÿง",
-  "deciduous_tree": "๐ŸŒณ",
-  "deer": "๐ŸฆŒ",
-  "department_store": "๐Ÿฌ",
-  "derelict_house": "๐Ÿš",
-  "desert": "๐Ÿœ",
-  "desert_island": "๐Ÿ",
-  "desktop_computer": "๐Ÿ–ฅ",
-  "detective": "๐Ÿ•ต",
-  "diamond_shape_with_a_dot_inside": "๐Ÿ’ ",
-  "diamond_suit": "โ™ฆ๏ธ",
-  "diamonds": "โ™ฆ",
-  "disappointed": "๐Ÿ˜ž",
-  "disappointed_relieved": "๐Ÿ˜ฅ",
-  "diving_mask": "๐Ÿคฟ",
-  "diya_lamp": "๐Ÿช”",
-  "dizzy": "๐Ÿ’ซ",
-  "dizzy_face": "๐Ÿ˜ต",
-  "dna": "๐Ÿงฌ",
-  "do_not_litter": "๐Ÿšฏ",
-  "dog": "๐Ÿถ",
-  "dog2": "๐Ÿ•",
-  "dollar": "๐Ÿ’ต",
-  "dolls": "๐ŸŽŽ",
-  "dolphin": "๐Ÿฌ",
-  "door": "๐Ÿšช",
-  "double_exclamation_mark": "โ€ผ",
-  "doughnut": "๐Ÿฉ",
-  "dove": "๐Ÿ•Š",
-  "down_arrow": "โฌ‡",
-  "downleft_arrow": "โ†™",
-  "downright_arrow": "โ†˜",
-  "dragon": "๐Ÿ‰",
-  "dragon_face": "๐Ÿฒ",
-  "dress": "๐Ÿ‘—",
-  "dromedary_camel": "๐Ÿช",
-  "drooling_face": "๐Ÿคค",
-  "drop_of_blood": "๐Ÿฉธ",
-  "droplet": "๐Ÿ’ง",
-  "drum": "๐Ÿฅ",
-  "duck": "๐Ÿฆ†",
-  "dumpling": "๐ŸฅŸ",
-  "dvd": "๐Ÿ“€",
-  "e-mail": "๐Ÿ“ง",
-  "eagle": "๐Ÿฆ…",
-  "ear": "๐Ÿ‘‚",
-  "ear_of_rice": "๐ŸŒพ",
-  "ear_with_hearing_aid": "๐Ÿฆป",
-  "earth_africa": "๐ŸŒ",
-  "earth_americas": "๐ŸŒŽ",
-  "earth_asia": "๐ŸŒ",
-  "egg": "๐Ÿฅš",
-  "eggplant": "๐Ÿ†",
-  "eight": "8โƒฃ",
-  "eight_pointed_black_star": "โœด๏ธ",
-  "eight_spoked_asterisk": "โœณ๏ธ",
-  "eightpointed_star": "โœด",
-  "eightspoked_asterisk": "โœณ",
-  "eject_button": "โ",
-  "electric_plug": "๐Ÿ”Œ",
-  "elephant": "๐Ÿ˜",
-  "elf": "๐Ÿง",
-  "end": "๐Ÿ”š",
-  "envelope": "โœ‰",
-  "envelope_with_arrow": "๐Ÿ“ฉ",
-  "euro": "๐Ÿ’ถ",
-  "european_castle": "๐Ÿฐ",
-  "european_post_office": "๐Ÿค",
-  "evergreen_tree": "๐ŸŒฒ",
-  "exclamation": "โ—",
-  "exclamation_question_mark": "โ‰",
-  "exploding_head": "๐Ÿคฏ",
-  "expressionless": "๐Ÿ˜‘",
-  "eye": "๐Ÿ‘",
-  "eyeglasses": "๐Ÿ‘“",
-  "eyes": "๐Ÿ‘€",
-  "face_vomiting": "๐Ÿคฎ",
-  "face_with_hand_over_mouth": "๐Ÿคญ",
-  "face_with_headbandage": "๐Ÿค•",
-  "face_with_monocle": "๐Ÿง",
-  "face_with_raised_eyebrow": "๐Ÿคจ",
-  "face_with_symbols_on_mouth": "๐Ÿคฌ",
-  "face_with_symbols_over_mouth": "๐Ÿคฌ",
-  "face_with_thermometer": "๐Ÿค’",
-  "factory": "๐Ÿญ",
-  "fairy": "๐Ÿงš",
-  "falafel": "๐Ÿง†",
-  "fallen_leaf": "๐Ÿ‚",
-  "family": "๐Ÿ‘ช",
-  "fast_forward": "โฉ",
-  "fax": "๐Ÿ“ ",
-  "fearful": "๐Ÿ˜จ",
-  "feet": "๐Ÿพ",
-  "female_sign": "โ™€",
-  "ferris_wheel": "๐ŸŽก",
-  "ferry": "โ›ด๏ธ",
-  "field_hockey": "๐Ÿ‘",
-  "file_cabinet": "๐Ÿ—„",
-  "file_folder": "๐Ÿ“",
-  "film_frames": "๐ŸŽž",
-  "film_projector": "๐Ÿ“ฝ",
-  "fingers_crossed": "๐Ÿคž",
-  "fire": "๐Ÿ”ฅ",
-  "fire_engine": "๐Ÿš’",
-  "fire_extinguisher": "๐Ÿงฏ",
-  "firecracker": "๐Ÿงจ",
-  "fireworks": "๐ŸŽ†",
-  "first_place": "๐Ÿฅ‡",
-  "first_quarter_moon": "๐ŸŒ“",
-  "first_quarter_moon_with_face": "๐ŸŒ›",
-  "fish": "๐ŸŸ",
-  "fish_cake": "๐Ÿฅ",
-  "fishing_pole_and_fish": "๐ŸŽฃ",
-  "fist": "โœŠ",
-  "five": "5โƒฃ",
-  "flag_black": "๐Ÿด",
-  "flag_white": "๐Ÿณ",
-  "flags": "๐ŸŽ",
-  "flamingo": "๐Ÿฆฉ",
-  "flashlight": "๐Ÿ”ฆ",
-  "flat_shoe": "๐Ÿฅฟ",
-  "fleur-de-lis": "โšœ",
-  "fleurde-lis": "โšœ๏ธ",
-  "floppy_disk": "๐Ÿ’พ",
-  "flower_playing_cards": "๐ŸŽด",
-  "flushed": "๐Ÿ˜ณ",
-  "flying_disc": "๐Ÿฅ",
-  "flying_saucer": "๐Ÿ›ธ",
-  "fog": "๐ŸŒซ",
-  "foggy": "๐ŸŒ",
-  "foot": "๐Ÿฆถ",
-  "football": "๐Ÿˆ",
-  "footprints": "๐Ÿ‘ฃ",
-  "fork_and_knife": "๐Ÿด",
-  "fork_and_knife_with_plate": "๐Ÿฝ",
-  "fortune_cookie": "๐Ÿฅ ",
-  "fountain": "โ›ฒ",
-  "fountain_pen": "๐Ÿ–‹",
-  "four": "4โƒฃ",
-  "four_leaf_clover": "๐Ÿ€",
-  "fox": "๐ŸฆŠ",
-  "framed_picture": "๐Ÿ–ผ",
-  "free": "๐Ÿ†“",
-  "french_bread": "๐Ÿฅ–",
-  "fried_shrimp": "๐Ÿค",
-  "fries": "๐ŸŸ",
-  "frog": "๐Ÿธ",
-  "frowning": "๐Ÿ˜ฆ",
-  "frowning_face": "โ˜น๏ธ",
-  "fuelpump": "โ›ฝ",
-  "full_moon": "๐ŸŒ•",
-  "full_moon_with_face": "๐ŸŒ",
-  "funeral_urn": "โšฑ๏ธ",
-  "game_die": "๐ŸŽฒ",
-  "garlic": "๐Ÿง„",
-  "gear": "โš™๏ธ",
-  "gem": "๐Ÿ’Ž",
-  "gemini": "โ™Š",
-  "genie": "๐Ÿงž",
-  "ghost": "๐Ÿ‘ป",
-  "gift": "๐ŸŽ",
-  "gift_heart": "๐Ÿ’",
-  "giraffe": "๐Ÿฆ’",
-  "girl": "๐Ÿ‘ง",
-  "glass_of_milk": "๐Ÿฅ›",
-  "globe_with_meridians": "๐ŸŒ",
-  "gloves": "๐Ÿงค",
-  "goal": "๐Ÿฅ…",
-  "goal_net": "๐Ÿฅ…",
-  "goat": "๐Ÿ",
-  "goggles": "๐Ÿฅฝ",
-  "golf": "โ›ณ",
-  "golfer": "๐ŸŒ",
-  "gorilla": "๐Ÿฆ",
-  "grapes": "๐Ÿ‡",
-  "green_apple": "๐Ÿ",
-  "green_book": "๐Ÿ“—",
-  "green_circle": "๐ŸŸข",
-  "green_heart": "๐Ÿ’š",
-  "green_salad": "๐Ÿฅ—",
-  "green_square": "๐ŸŸฉ",
-  "grey_exclamation": "โ•",
-  "grey_question": "โ”",
-  "grimacing": "๐Ÿ˜ฌ",
-  "grin": "๐Ÿ˜",
-  "grinning": "๐Ÿ˜€",
-  "guard": "๐Ÿ’‚",
-  "guardsman": "๐Ÿ’‚",
-  "guide_dog": "๐Ÿฆฎ",
-  "guitar": "๐ŸŽธ",
-  "gun": "๐Ÿ”ซ",
-  "haircut": "๐Ÿ’‡",
-  "hamburger": "๐Ÿ”",
-  "hammer": "๐Ÿ”จ",
-  "hammer_and_pick": "โš’๏ธ",
-  "hammer_and_wrench": "๐Ÿ› ",
-  "hamster": "๐Ÿน",
-  "hand_with_fingers_splayed": "๐Ÿ–",
-  "handbag": "๐Ÿ‘œ",
-  "handshake": "๐Ÿค",
-  "hash": "#โƒฃ",
-  "hatched_chick": "๐Ÿฅ",
-  "hatching_chick": "๐Ÿฃ",
-  "head_bandage": "๐Ÿค•",
-  "headphones": "๐ŸŽง",
-  "hear_no_evil": "๐Ÿ™‰",
-  "heart": "โค๏ธ",
-  "heart_decoration": "๐Ÿ’Ÿ",
-  "heart_exclamation": "โฃ",
-  "heart_eyes": "๐Ÿ˜",
-  "heart_eyes_cat": "๐Ÿ˜ป",
-  "heart_suit": "โ™ฅ๏ธ",
-  "heartbeat": "๐Ÿ’“",
-  "heartpulse": "๐Ÿ’—",
-  "hearts": "โ™ฅ",
-  "heavy_check_mark": "โœ”๏ธ",
-  "heavy_division_sign": "โž—",
-  "heavy_dollar_sign": "๐Ÿ’ฒ",
-  "heavy_minus_sign": "โž–",
-  "heavy_multiplication_x": "โœ–๏ธ",
-  "heavy_plus_sign": "โž•",
-  "hedgehog": "๐Ÿฆ”",
-  "helicopter": "๐Ÿš",
-  "herb": "๐ŸŒฟ",
-  "hibiscus": "๐ŸŒบ",
-  "high_brightness": "๐Ÿ”†",
-  "high_heel": "๐Ÿ‘ ",
-  "hiking_boot": "๐Ÿฅพ",
-  "hindu_temple": "๐Ÿ›•",
-  "hippopotamus": "๐Ÿฆ›",
-  "hockey": "๐Ÿ’",
-  "hole": "๐Ÿ•ณ",
-  "honey_pot": "๐Ÿฏ",
-  "horse": "๐Ÿด",
-  "horse_racing": "๐Ÿ‡",
-  "hospital": "๐Ÿฅ",
-  "hot_face": "๐Ÿฅต",
-  "hot_pepper": "๐ŸŒถ",
-  "hot_springs": "โ™จ",
-  "hotdog": "๐ŸŒญ",
-  "hotel": "๐Ÿจ",
-  "hotsprings": "โ™จ๏ธ",
-  "hourglass": "โŒ›",
-  "hourglass_flowing_sand": "โณ",
-  "house": "๐Ÿ ",
-  "house_with_garden": "๐Ÿก",
-  "houses": "๐Ÿ˜",
-  "hugging": "๐Ÿค—",
-  "hundred_points": "๐Ÿ’ฏ",
-  "hushed": "๐Ÿ˜ฏ",
-  "ice": "๐ŸงŠ",
-  "ice_cream": "๐Ÿจ",
-  "ice_hockey": "๐Ÿ’",
-  "ice_skate": "โ›ธ๏ธ",
-  "icecream": "๐Ÿฆ",
-  "id": "๐Ÿ†”",
-  "ideograph_advantage": "๐Ÿ‰",
-  "imp": "๐Ÿ‘ฟ",
-  "inbox_tray": "๐Ÿ“ฅ",
-  "incoming_envelope": "๐Ÿ“จ",
-  "index_pointing_up": "โ˜",
-  "infinity": "โ™พ",
-  "information": "โ„น๏ธ",
-  "information_desk_person": "๐Ÿ’",
-  "information_source": "โ„น",
-  "innocent": "๐Ÿ˜‡",
-  "input_numbers": "๐Ÿ”ข",
-  "interrobang": "โ‰๏ธ",
-  "iphone": "๐Ÿ“ฑ",
-  "izakaya_lantern": "๐Ÿฎ",
-  "jack_o_lantern": "๐ŸŽƒ",
-  "japan": "๐Ÿ—พ",
-  "japanese_castle": "๐Ÿฏ",
-  "japanese_congratulations_button": "ใŠ—๏ธ",
-  "japanese_free_of_charge_button": "๐Ÿˆš",
-  "japanese_goblin": "๐Ÿ‘บ",
-  "japanese_ogre": "๐Ÿ‘น",
-  "japanese_reserved_button": "๐Ÿˆฏ",
-  "japanese_secret_button": "ใŠ™๏ธ",
-  "japanese_service_charge_button": "๐Ÿˆ‚",
-  "jeans": "๐Ÿ‘–",
-  "joy": "๐Ÿ˜‚",
-  "joy_cat": "๐Ÿ˜น",
-  "joystick": "๐Ÿ•น",
-  "kaaba": "๐Ÿ•‹",
-  "kangaroo": "๐Ÿฆ˜",
-  "key": "๐Ÿ”‘",
-  "keyboard": "โŒจ๏ธ",
-  "keycap_ten": "๐Ÿ”Ÿ",
-  "kick_scooter": "๐Ÿ›ด",
-  "kimono": "๐Ÿ‘˜",
-  "kiss": "๐Ÿ’‹",
-  "kissing": "๐Ÿ˜—",
-  "kissing_cat": "๐Ÿ˜ฝ",
-  "kissing_closed_eyes": "๐Ÿ˜š",
-  "kissing_heart": "๐Ÿ˜˜",
-  "kissing_smiling_eyes": "๐Ÿ˜™",
-  "kitchen_knife": "๐Ÿ”ช",
-  "kite": "๐Ÿช",
-  "kiwi": "๐Ÿฅ",
-  "kiwi_fruit": "๐Ÿฅ",
-  "knife": "๐Ÿ”ช",
-  "koala": "๐Ÿจ",
-  "koko": "๐Ÿˆ",
-  "lab_coat": "๐Ÿฅผ",
-  "label": "๐Ÿท",
-  "lacrosse": "๐Ÿฅ",
-  "large_blue_diamond": "๐Ÿ”ท",
-  "large_orange_diamond": "๐Ÿ”ถ",
-  "last_quarter_moon": "๐ŸŒ—",
-  "last_quarter_moon_with_face": "๐ŸŒœ",
-  "last_track_button": "โฎ๏ธ",
-  "latin_cross": "โœ",
-  "laughing": "๐Ÿ˜†",
-  "leafy_green": "๐Ÿฅฌ",
-  "leaves": "๐Ÿƒ",
-  "ledger": "๐Ÿ“’",
-  "left_arrow": "โฌ…",
-  "left_arrow_curving_right": "โ†ช",
-  "left_facing_fist": "๐Ÿค›",
-  "left_luggage": "๐Ÿ›…",
-  "left_right_arrow": "โ†”",
-  "leftfacing_fist": "๐Ÿค›",
-  "leftright_arrow": "โ†”๏ธ",
-  "leftwards_arrow_with_hook": "โ†ฉ๏ธ",
-  "leg": "๐Ÿฆต",
-  "lemon": "๐Ÿ‹",
-  "leo": "โ™Œ",
-  "leopard": "๐Ÿ†",
-  "level_slider": "๐ŸŽš",
-  "libra": "โ™Ž",
-  "light_rail": "๐Ÿšˆ",
-  "light_skin_tone": "๐Ÿป",
-  "link": "๐Ÿ”—",
-  "linked_paperclips": "๐Ÿ–‡",
-  "lion_face": "๐Ÿฆ",
-  "lips": "๐Ÿ‘„",
-  "lipstick": "๐Ÿ’„",
-  "lizard": "๐ŸฆŽ",
-  "llama": "๐Ÿฆ™",
-  "lobster": "๐Ÿฆž",
-  "lock": "๐Ÿ”’",
-  "lock_with_ink_pen": "๐Ÿ”",
-  "lollipop": "๐Ÿญ",
-  "loop": "โžฟ",
-  "lotion_bottle": "๐Ÿงด",
-  "loud_sound": "๐Ÿ”Š",
-  "loudspeaker": "๐Ÿ“ข",
-  "love_hotel": "๐Ÿฉ",
-  "love_letter": "๐Ÿ’Œ",
-  "love_you_gesture": "๐ŸคŸ",
-  "loveyou_gesture": "๐ŸคŸ",
-  "low_brightness": "๐Ÿ”…",
-  "luggage": "๐Ÿงณ",
-  "lying_face": "๐Ÿคฅ",
-  "m": "โ“‚๏ธ",
-  "mag": "๐Ÿ”",
-  "mag_right": "๐Ÿ”Ž",
-  "mage": "๐Ÿง™",
-  "magnet": "๐Ÿงฒ",
-  "mahjong": "๐Ÿ€„",
-  "mailbox": "๐Ÿ“ซ",
-  "mailbox_closed": "๐Ÿ“ช",
-  "mailbox_with_mail": "๐Ÿ“ฌ",
-  "mailbox_with_no_mail": "๐Ÿ“ญ",
-  "male_sign": "โ™‚",
-  "man": "๐Ÿ‘จ",
-  "man_dancing": "๐Ÿ•บ",
-  "man_in_suit": "๐Ÿ•ด",
-  "man_in_tuxedo": "๐Ÿคต",
-  "man_with_chinese_cap": "๐Ÿ‘ฒ",
-  "man_with_gua_pi_mao": "๐Ÿ‘ฒ",
-  "man_with_turban": "๐Ÿ‘ณ",
-  "mango": "๐Ÿฅญ",
-  "mans_shoe": "๐Ÿ‘ž",
-  "mantelpiece_clock": "๐Ÿ•ฐ",
-  "manual_wheelchair": "๐Ÿฆฝ",
-  "maple_leaf": "๐Ÿ",
-  "martial_arts_uniform": "๐Ÿฅ‹",
-  "mask": "๐Ÿ˜ท",
-  "massage": "๐Ÿ’†",
-  "mate": "๐Ÿง‰",
-  "meat_on_bone": "๐Ÿ–",
-  "mechanical_arm": "๐Ÿฆพ",
-  "mechanical_leg": "๐Ÿฆฟ",
-  "medal": "๐Ÿ…",
-  "medical_symbol": "โš•",
-  "medium_skin_tone": "๐Ÿฝ",
-  "mediumdark_skin_tone": "๐Ÿพ",
-  "mediumlight_skin_tone": "๐Ÿผ",
-  "mega": "๐Ÿ“ฃ",
-  "melon": "๐Ÿˆ",
-  "memo": "๐Ÿ“",
-  "menorah": "๐Ÿ•Ž",
-  "mens": "๐Ÿšน",
-  "merperson": "๐Ÿงœ",
-  "metal": "๐Ÿค˜",
-  "metro": "๐Ÿš‡",
-  "microbe": "๐Ÿฆ ",
-  "microphone": "๐ŸŽค",
-  "microscope": "๐Ÿ”ฌ",
-  "middle_finger": "๐Ÿ–•",
-  "military_medal": "๐ŸŽ–",
-  "milk": "๐Ÿฅ›",
-  "milky_way": "๐ŸŒŒ",
-  "minibus": "๐Ÿš",
-  "minidisc": "๐Ÿ’ฝ",
-  "mobile_phone_off": "๐Ÿ“ด",
-  "money_mouth": "๐Ÿค‘",
-  "money_with_wings": "๐Ÿ’ธ",
-  "moneybag": "๐Ÿ’ฐ",
-  "moneymouth_face": "๐Ÿค‘",
-  "monkey": "๐Ÿ’",
-  "monkey_face": "๐Ÿต",
-  "monorail": "๐Ÿš",
-  "moon_cake": "๐Ÿฅฎ",
-  "mortar_board": "๐ŸŽ“",
-  "mosque": "๐Ÿ•Œ",
-  "mosquito": "๐ŸฆŸ",
-  "motor_boat": "๐Ÿ›ฅ",
-  "motor_scooter": "๐Ÿ›ต",
-  "motorcycle": "๐Ÿ",
-  "motorized_wheelchair": "๐Ÿฆผ",
-  "motorway": "๐Ÿ›ฃ",
-  "mount_fuji": "๐Ÿ—ป",
-  "mountain": "โ›ฐ๏ธ",
-  "mountain_bicyclist": "๐Ÿšต",
-  "mountain_cableway": "๐Ÿš ",
-  "mountain_railway": "๐Ÿšž",
-  "mouse": "๐Ÿญ",
-  "mouse2": "๐Ÿ",
-  "movie_camera": "๐ŸŽฅ",
-  "moyai": "๐Ÿ—ฟ",
-  "mrs_claus": "๐Ÿคถ",
-  "multiplication_sign": "โœ–",
-  "muscle": "๐Ÿ’ช",
-  "mushroom": "๐Ÿ„",
-  "musical_keyboard": "๐ŸŽน",
-  "musical_note": "๐ŸŽต",
-  "musical_score": "๐ŸŽผ",
-  "mute": "๐Ÿ”‡",
-  "nail_care": "๐Ÿ’…",
-  "name_badge": "๐Ÿ“›",
-  "national_park": "๐Ÿž",
-  "nauseated_face": "๐Ÿคข",
-  "nazar_amulet": "๐Ÿงฟ",
-  "necktie": "๐Ÿ‘”",
-  "negative_squared_cross_mark": "โŽ",
-  "nerd": "๐Ÿค“",
-  "neutral_face": "๐Ÿ˜",
-  "new": "๐Ÿ†•",
-  "new_moon": "๐ŸŒ‘",
-  "new_moon_with_face": "๐ŸŒš",
-  "newspaper": "๐Ÿ“ฐ",
-  "next_track_button": "โญ๏ธ",
-  "ng": "๐Ÿ†–",
-  "night_with_stars": "๐ŸŒƒ",
-  "nine": "9โƒฃ",
-  "no_bell": "๐Ÿ”•",
-  "no_bicycles": "๐Ÿšณ",
-  "no_entry": "โ›”",
-  "no_entry_sign": "๐Ÿšซ",
-  "no_good": "๐Ÿ™…",
-  "no_mobile_phones": "๐Ÿ“ต",
-  "no_mouth": "๐Ÿ˜ถ",
-  "no_pedestrians": "๐Ÿšท",
-  "no_smoking": "๐Ÿšญ",
-  "non-potable_water": "๐Ÿšฑ",
-  "nose": "๐Ÿ‘ƒ",
-  "notebook": "๐Ÿ““",
-  "notebook_with_decorative_cover": "๐Ÿ“”",
-  "notes": "๐ŸŽถ",
-  "nut_and_bolt": "๐Ÿ”ฉ",
-  "o": "โญ•",
-  "o_button_blood_type": "๐Ÿ…พ",
-  "ocean": "๐ŸŒŠ",
-  "octagonal_sign": "๐Ÿ›‘",
-  "octopus": "๐Ÿ™",
-  "oden": "๐Ÿข",
-  "office": "๐Ÿข",
-  "oil_drum": "๐Ÿ›ข",
-  "ok": "๐Ÿ†—",
-  "ok_hand": "๐Ÿ‘Œ",
-  "ok_woman": "๐Ÿ™†",
-  "old_key": "๐Ÿ—",
-  "older_adult": "๐Ÿง“",
-  "older_man": "๐Ÿ‘ด",
-  "older_person": "๐Ÿง“",
-  "older_woman": "๐Ÿ‘ต",
-  "om_symbol": "๐Ÿ•‰",
-  "on": "๐Ÿ”›",
-  "oncoming_automobile": "๐Ÿš˜",
-  "oncoming_bus": "๐Ÿš",
-  "oncoming_fist": "๐Ÿ‘Š",
-  "oncoming_police_car": "๐Ÿš”",
-  "oncoming_taxi": "๐Ÿš–",
-  "one": "1โƒฃ",
-  "onepiece_swimsuit": "๐Ÿฉฑ",
-  "onion": "๐Ÿง…",
-  "open_file_folder": "๐Ÿ“‚",
-  "open_hands": "๐Ÿ‘",
-  "open_mouth": "๐Ÿ˜ฎ",
-  "ophiuchus": "โ›Ž",
-  "orange_book": "๐Ÿ“™",
-  "orange_circle": "๐ŸŸ ",
-  "orange_heart": "๐Ÿงก",
-  "orange_square": "๐ŸŸง",
-  "orangutan": "๐Ÿฆง",
-  "orthodox_cross": "โ˜ฆ๏ธ",
-  "otter": "๐Ÿฆฆ",
-  "outbox_tray": "๐Ÿ“ค",
-  "owl": "๐Ÿฆ‰",
-  "ox": "๐Ÿ‚",
-  "oyster": "๐Ÿฆช",
-  "p_button": "๐Ÿ…ฟ",
-  "package": "๐Ÿ“ฆ",
-  "page_facing_up": "๐Ÿ“„",
-  "page_with_curl": "๐Ÿ“ƒ",
-  "pager": "๐Ÿ“Ÿ",
-  "paintbrush": "๐Ÿ–Œ",
-  "palm_tree": "๐ŸŒด",
-  "palms_up_together": "๐Ÿคฒ",
-  "pancakes": "๐Ÿฅž",
-  "panda_face": "๐Ÿผ",
-  "paperclip": "๐Ÿ“Ž",
-  "parachute": "๐Ÿช‚",
-  "parrot": "๐Ÿฆœ",
-  "part_alternation_mark": "ใ€ฝ",
-  "partly_sunny": "โ›…",
-  "partying_face": "๐Ÿฅณ",
-  "passenger_ship": "๐Ÿ›ณ",
-  "passport_control": "๐Ÿ›‚",
-  "pause_button": "โธ๏ธ",
-  "peace": "โ˜ฎ",
-  "peace_symbol": "โ˜ฎ๏ธ",
-  "peach": "๐Ÿ‘",
-  "peacock": "๐Ÿฆš",
-  "peanuts": "๐Ÿฅœ",
-  "pear": "๐Ÿ",
-  "pen": "๐Ÿ–Š",
-  "pencil": "๐Ÿ“",
-  "pencil2": "โœ",
-  "penguin": "๐Ÿง",
-  "pensive": "๐Ÿ˜”",
-  "people_with_bunny_ears_partying": "๐Ÿ‘ฏ",
-  "people_wrestling": "๐Ÿคผ",
-  "performing_arts": "๐ŸŽญ",
-  "persevere": "๐Ÿ˜ฃ",
-  "person": "๐Ÿง‘",
-  "person_biking": "๐Ÿšด",
-  "person_bouncing_ball": "โ›น๏ธ",
-  "person_bowing": "๐Ÿ™‡",
-  "person_cartwheeling": "๐Ÿคธ",
-  "person_climbing": "๐Ÿง—",
-  "person_doing_cartwheel": "๐Ÿคธ",
-  "person_facepalming": "๐Ÿคฆ",
-  "person_fencing": "๐Ÿคบ",
-  "person_frowning": "๐Ÿ™",
-  "person_gesturing_no": "๐Ÿ™…",
-  "person_gesturing_ok": "๐Ÿ™†",
-  "person_getting_haircut": "๐Ÿ’‡",
-  "person_getting_massage": "๐Ÿ’†",
-  "person_in_lotus_position": "๐Ÿง˜",
-  "person_in_steamy_room": "๐Ÿง–",
-  "person_juggling": "๐Ÿคน",
-  "person_kneeling": "๐ŸงŽ",
-  "person_mountain_biking": "๐Ÿšต",
-  "person_playing_handball": "๐Ÿคพ",
-  "person_playing_water_polo": "๐Ÿคฝ",
-  "person_pouting": "๐Ÿ™Ž",
-  "person_raising_hand": "๐Ÿ™‹",
-  "person_rowing_boat": "๐Ÿšฃ",
-  "person_running": "๐Ÿƒ",
-  "person_shrugging": "๐Ÿคท",
-  "person_standing": "๐Ÿง",
-  "person_surfing": "๐Ÿ„",
-  "person_swimming": "๐ŸŠ",
-  "person_tipping_hand": "๐Ÿ’",
-  "person_walking": "๐Ÿšถ",
-  "person_wearing_turban": "๐Ÿ‘ณ",
-  "person_with_blond_hair": "๐Ÿ‘ฑ",
-  "person_with_pouting_face": "๐Ÿ™Ž",
-  "petri_dish": "๐Ÿงซ",
-  "pick": "โ›๏ธ",
-  "pie": "๐Ÿฅง",
-  "pig": "๐Ÿท",
-  "pig2": "๐Ÿ–",
-  "pig_nose": "๐Ÿฝ",
-  "pill": "๐Ÿ’Š",
-  "pinching_hand": "๐Ÿค",
-  "pineapple": "๐Ÿ",
-  "ping_pong": "๐Ÿ“",
-  "pisces": "โ™“",
-  "pizza": "๐Ÿ•",
-  "place_of_worship": "๐Ÿ›",
-  "play_button": "โ–ถ",
-  "play_or_pause_button": "โฏ๏ธ",
-  "play_pause": "โฏ",
-  "pleading_face": "๐Ÿฅบ",
-  "point_down": "๐Ÿ‘‡",
-  "point_left": "๐Ÿ‘ˆ",
-  "point_right": "๐Ÿ‘‰",
-  "point_up": "โ˜๏ธ",
-  "point_up_2": "๐Ÿ‘†",
-  "police_car": "๐Ÿš“",
-  "police_officer": "๐Ÿ‘ฎ",
-  "poodle": "๐Ÿฉ",
-  "poop": "๐Ÿ’ฉ",
-  "popcorn": "๐Ÿฟ",
-  "post_office": "๐Ÿฃ",
-  "postal_horn": "๐Ÿ“ฏ",
-  "postbox": "๐Ÿ“ฎ",
-  "potable_water": "๐Ÿšฐ",
-  "potato": "๐Ÿฅ”",
-  "pouch": "๐Ÿ‘",
-  "poultry_leg": "๐Ÿ—",
-  "pound": "๐Ÿ’ท",
-  "pouting_cat": "๐Ÿ˜พ",
-  "pray": "๐Ÿ™",
-  "prayer_beads": "๐Ÿ“ฟ",
-  "pregnant_woman": "๐Ÿคฐ",
-  "pretzel": "๐Ÿฅจ",
-  "prince": "๐Ÿคด",
-  "princess": "๐Ÿ‘ธ",
-  "printer": "๐Ÿ–จ",
-  "probing_cane": "๐Ÿฆฏ",
-  "punch": "๐Ÿ‘Š",
-  "purple_circle": "๐ŸŸฃ",
-  "purple_heart": "๐Ÿ’œ",
-  "purse": "๐Ÿ‘›",
-  "pushpin": "๐Ÿ“Œ",
-  "put_litter_in_its_place": "๐Ÿšฎ",
-  "puzzle_piece": "๐Ÿงฉ",
-  "question": "โ“",
-  "rabbit": "๐Ÿฐ",
-  "rabbit2": "๐Ÿ‡",
-  "raccoon": "๐Ÿฆ",
-  "racehorse": "๐ŸŽ",
-  "racing_car": "๐ŸŽ",
-  "radio": "๐Ÿ“ป",
-  "radio_button": "๐Ÿ”˜",
-  "radioactive": "โ˜ข๏ธ",
-  "rage": "๐Ÿ˜ก",
-  "railway_car": "๐Ÿšƒ",
-  "railway_track": "๐Ÿ›ค",
-  "rainbow": "๐ŸŒˆ",
-  "raised_back_of_hand": "๐Ÿคš",
-  "raised_hand": "โœ‹",
-  "raised_hands": "๐Ÿ™Œ",
-  "raising_hand": "๐Ÿ™‹",
-  "ram": "๐Ÿ",
-  "ramen": "๐Ÿœ",
-  "rat": "๐Ÿ€",
-  "razor": "๐Ÿช’",
-  "receipt": "๐Ÿงพ",
-  "record_button": "โบ๏ธ",
-  "recycle": "โ™ป",
-  "recycling_symbol": "โ™ป๏ธ",
-  "red_car": "๐Ÿš—",
-  "red_circle": "๐Ÿ”ด",
-  "red_envelope": "๐Ÿงง",
-  "red_hair": "๐Ÿฆฐ",
-  "red_heart": "โค",
-  "red_square": "๐ŸŸฅ",
-  "regional_indicator_a": "๐Ÿ‡ฆ",
-  "regional_indicator_b": "๐Ÿ‡ง",
-  "regional_indicator_c": "๐Ÿ‡จ",
-  "regional_indicator_d": "๐Ÿ‡ฉ",
-  "regional_indicator_e": "๐Ÿ‡ช",
-  "regional_indicator_f": "๐Ÿ‡ซ",
-  "regional_indicator_g": "๐Ÿ‡ฌ",
-  "regional_indicator_h": "๐Ÿ‡ญ",
-  "regional_indicator_i": "๐Ÿ‡ฎ",
-  "regional_indicator_j": "๐Ÿ‡ฏ",
-  "regional_indicator_k": "๐Ÿ‡ฐ",
-  "regional_indicator_l": "๐Ÿ‡ฑ",
-  "regional_indicator_m": "๐Ÿ‡ฒ",
-  "regional_indicator_n": "๐Ÿ‡ณ",
-  "regional_indicator_o": "๐Ÿ‡ด",
-  "regional_indicator_p": "๐Ÿ‡ต",
-  "regional_indicator_q": "๐Ÿ‡ถ",
-  "regional_indicator_r": "๐Ÿ‡ท",
-  "regional_indicator_s": "๐Ÿ‡ธ",
-  "regional_indicator_t": "๐Ÿ‡น",
-  "regional_indicator_u": "๐Ÿ‡บ",
-  "regional_indicator_v": "๐Ÿ‡ป",
-  "regional_indicator_w": "๐Ÿ‡ผ",
-  "regional_indicator_x": "๐Ÿ‡ฝ",
-  "regional_indicator_y": "๐Ÿ‡พ",
-  "regional_indicator_z": "๐Ÿ‡ฟ",
-  "registered": "ยฎ",
-  "relieved": "๐Ÿ˜Œ",
-  "reminder_ribbon": "๐ŸŽ—",
-  "repeat": "๐Ÿ”",
-  "repeat_one": "๐Ÿ”‚",
-  "rescue_workerโ€™s_helmet": "โ›‘๏ธ",
-  "restroom": "๐Ÿšป",
-  "reverse_button": "โ—€",
-  "revolving_hearts": "๐Ÿ’ž",
-  "rewind": "โช",
-  "rhino": "๐Ÿฆ",
-  "rhinoceros": "๐Ÿฆ",
-  "ribbon": "๐ŸŽ€",
-  "rice": "๐Ÿš",
-  "rice_ball": "๐Ÿ™",
-  "rice_cracker": "๐Ÿ˜",
-  "rice_scene": "๐ŸŽ‘",
-  "right_arrow": "โžก๏ธ",
-  "right_arrow_curving_down": "โคต",
-  "right_arrow_curving_left": "โ†ฉ",
-  "right_arrow_curving_up": "โคด",
-  "right_facing_fist": "๐Ÿคœ",
-  "rightfacing_fist": "๐Ÿคœ",
-  "ring": "๐Ÿ’",
-  "ringed_planet": "๐Ÿช",
-  "robot": "๐Ÿค–",
-  "rocket": "๐Ÿš€",
-  "rofl": "๐Ÿคฃ",
-  "roll_of_paper": "๐Ÿงป",
-  "rolledup_newspaper": "๐Ÿ—ž",
-  "roller_coaster": "๐ŸŽข",
-  "rolling_eyes": "๐Ÿ™„",
-  "rolling_on_the_floor_laughing": "๐Ÿคฃ",
-  "rooster": "๐Ÿ“",
-  "rose": "๐ŸŒน",
-  "rosette": "๐Ÿต",
-  "rotating_light": "๐Ÿšจ",
-  "round_pushpin": "๐Ÿ“",
-  "rowboat": "๐Ÿšฃ",
-  "rugby_football": "๐Ÿ‰",
-  "runner": "๐Ÿƒ",
-  "running_shirt_with_sash": "๐ŸŽฝ",
-  "safety_pin": "๐Ÿงท",
-  "safety_vest": "๐Ÿฆบ",
-  "sagittarius": "โ™",
-  "sailboat": "โ›ต",
-  "sake": "๐Ÿถ",
-  "salad": "๐Ÿฅ—",
-  "salt": "๐Ÿง‚",
-  "sandal": "๐Ÿ‘ก",
-  "sandwich": "๐Ÿฅช",
-  "santa": "๐ŸŽ…",
-  "sari": "๐Ÿฅป",
-  "satellite": "๐Ÿ“ก",
-  "sauropod": "๐Ÿฆ•",
-  "saxophone": "๐ŸŽท",
-  "scales": "โš–",
-  "scarf": "๐Ÿงฃ",
-  "school": "๐Ÿซ",
-  "school_satchel": "๐ŸŽ’",
-  "scissors": "โœ‚",
-  "scooter": "๐Ÿ›ด",
-  "scorpion": "๐Ÿฆ‚",
-  "scorpius": "โ™",
-  "scream": "๐Ÿ˜ฑ",
-  "scream_cat": "๐Ÿ™€",
-  "scroll": "๐Ÿ“œ",
-  "seat": "๐Ÿ’บ",
-  "second_place": "๐Ÿฅˆ",
-  "secret": "ใŠ™",
-  "see_no_evil": "๐Ÿ™ˆ",
-  "seedling": "๐ŸŒฑ",
-  "selfie": "๐Ÿคณ",
-  "seven": "7โƒฃ",
-  "shallow_pan_of_food": "๐Ÿฅ˜",
-  "shamrock": "โ˜˜๏ธ",
-  "shark": "๐Ÿฆˆ",
-  "shaved_ice": "๐Ÿง",
-  "sheep": "๐Ÿ‘",
-  "shell": "๐Ÿš",
-  "shield": "๐Ÿ›ก",
-  "shinto_shrine": "โ›ฉ๏ธ",
-  "ship": "๐Ÿšข",
-  "shirt": "๐Ÿ‘•",
-  "shopping_bags": "๐Ÿ›",
-  "shopping_cart": "๐Ÿ›’",
-  "shorts": "๐Ÿฉณ",
-  "shower": "๐Ÿšฟ",
-  "shrimp": "๐Ÿฆ",
-  "shushing_face": "๐Ÿคซ",
-  "sign_of_the_horns": "๐Ÿค˜",
-  "signal_strength": "๐Ÿ“ถ",
-  "six": "6โƒฃ",
-  "six_pointed_star": "๐Ÿ”ฏ",
-  "skateboard": "๐Ÿ›น",
-  "ski": "๐ŸŽฟ",
-  "skier": "โ›ท๏ธ",
-  "skull": "๐Ÿ’€",
-  "skull_and_crossbones": "โ˜ ๏ธ",
-  "skull_crossbones": "โ˜ ",
-  "skunk": "๐Ÿฆจ",
-  "sled": "๐Ÿ›ท",
-  "sleeping": "๐Ÿ˜ด",
-  "sleeping_accommodation": "๐Ÿ›Œ",
-  "sleepy": "๐Ÿ˜ช",
-  "slight_frown": "๐Ÿ™",
-  "slight_smile": "๐Ÿ™‚",
-  "slightly_frowning_face": "๐Ÿ™",
-  "slot_machine": "๐ŸŽฐ",
-  "sloth": "๐Ÿฆฅ",
-  "small_airplane": "๐Ÿ›ฉ",
-  "small_blue_diamond": "๐Ÿ”น",
-  "small_orange_diamond": "๐Ÿ”ธ",
-  "small_red_triangle": "๐Ÿ”บ",
-  "small_red_triangle_down": "๐Ÿ”ป",
-  "smile": "๐Ÿ˜„",
-  "smile_cat": "๐Ÿ˜ธ",
-  "smiley": "๐Ÿ˜ƒ",
-  "smiley_cat": "๐Ÿ˜บ",
-  "smiling": "โ˜บ๏ธ",
-  "smiling_face": "โ˜บ",
-  "smiling_face_with_hearts": "๐Ÿฅฐ",
-  "smiling_imp": "๐Ÿ˜ˆ",
-  "smirk": "๐Ÿ˜",
-  "smirk_cat": "๐Ÿ˜ผ",
-  "smoking": "๐Ÿšฌ",
-  "snail": "๐ŸŒ",
-  "snake": "๐Ÿ",
-  "sneezing_face": "๐Ÿคง",
-  "snowboarder": "๐Ÿ‚",
-  "snowcapped_mountain": "๐Ÿ”",
-  "snowflake": "โ„",
-  "snowman": "โ›„",
-  "soap": "๐Ÿงผ",
-  "sob": "๐Ÿ˜ญ",
-  "soccer": "โšฝ",
-  "socks": "๐Ÿงฆ",
-  "softball": "๐ŸฅŽ",
-  "soon": "๐Ÿ”œ",
-  "sos": "๐Ÿ†˜",
-  "sound": "๐Ÿ”‰",
-  "space_invader": "๐Ÿ‘พ",
-  "spade_suit": "โ™ ๏ธ",
-  "spades": "โ™ ",
-  "spaghetti": "๐Ÿ",
-  "sparkle": "โ‡",
-  "sparkler": "๐ŸŽ‡",
-  "sparkles": "โœจ",
-  "sparkling_heart": "๐Ÿ’–",
-  "speak_no_evil": "๐Ÿ™Š",
-  "speaker": "๐Ÿ”ˆ",
-  "speaking_head": "๐Ÿ—ฃ",
-  "speech_balloon": "๐Ÿ’ฌ",
-  "speech_left": "๐Ÿ—จ",
-  "speedboat": "๐Ÿšค",
-  "spider": "๐Ÿ•ท",
-  "spider_web": "๐Ÿ•ธ",
-  "spiral_calendar": "๐Ÿ—“",
-  "spiral_notepad": "๐Ÿ—’",
-  "sponge": "๐Ÿงฝ",
-  "spoon": "๐Ÿฅ„",
-  "squid": "๐Ÿฆ‘",
-  "stadium": "๐ŸŸ",
-  "star": "โญ",
-  "star2": "๐ŸŒŸ",
-  "star_and_crescent": "โ˜ช๏ธ",
-  "star_of_david": "โœก",
-  "star_struck": "๐Ÿคฉ",
-  "stars": "๐ŸŒ ",
-  "starstruck": "๐Ÿคฉ",
-  "station": "๐Ÿš‰",
-  "statue_of_liberty": "๐Ÿ—ฝ",
-  "steam_locomotive": "๐Ÿš‚",
-  "stethoscope": "๐Ÿฉบ",
-  "stew": "๐Ÿฒ",
-  "stop_button": "โน๏ธ",
-  "stopwatch": "โฑ๏ธ",
-  "straight_ruler": "๐Ÿ“",
-  "strawberry": "๐Ÿ“",
-  "stuck_out_tongue": "๐Ÿ˜›",
-  "stuck_out_tongue_closed_eyes": "๐Ÿ˜",
-  "stuck_out_tongue_winking_eye": "๐Ÿ˜œ",
-  "studio_microphone": "๐ŸŽ™",
-  "stuffed_flatbread": "๐Ÿฅ™",
-  "sun": "โ˜€",
-  "sun_behind_large_cloud": "๐ŸŒฅ",
-  "sun_behind_rain_cloud": "๐ŸŒฆ",
-  "sun_behind_small_cloud": "๐ŸŒค",
-  "sun_with_face": "๐ŸŒž",
-  "sunflower": "๐ŸŒป",
-  "sunglasses": "๐Ÿ˜Ž",
-  "sunny": "โ˜€๏ธ",
-  "sunrise": "๐ŸŒ…",
-  "sunrise_over_mountains": "๐ŸŒ„",
-  "superhero": "๐Ÿฆธ",
-  "supervillain": "๐Ÿฆน",
-  "surfer": "๐Ÿ„",
-  "sushi": "๐Ÿฃ",
-  "suspension_railway": "๐ŸšŸ",
-  "swan": "๐Ÿฆข",
-  "sweat": "๐Ÿ˜“",
-  "sweat_drops": "๐Ÿ’ฆ",
-  "sweat_smile": "๐Ÿ˜…",
-  "sweet_potato": "๐Ÿ ",
-  "swimmer": "๐ŸŠ",
-  "symbols": "๐Ÿ”ฃ",
-  "synagogue": "๐Ÿ•",
-  "syringe": "๐Ÿ’‰",
-  "t_rex": "๐Ÿฆ–",
-  "taco": "๐ŸŒฎ",
-  "tada": "๐ŸŽ‰",
-  "takeout_box": "๐Ÿฅก",
-  "tanabata_tree": "๐ŸŽ‹",
-  "tangerine": "๐ŸŠ",
-  "taurus": "โ™‰",
-  "taxi": "๐Ÿš•",
-  "tea": "๐Ÿต",
-  "teddy_bear": "๐Ÿงธ",
-  "telephone": "โ˜Ž",
-  "telephone_receiver": "๐Ÿ“ž",
-  "telescope": "๐Ÿ”ญ",
-  "tennis": "๐ŸŽพ",
-  "tent": "โ›บ",
-  "test_tube": "๐Ÿงช",
-  "thermometer": "๐ŸŒก",
-  "thermometer_face": "๐Ÿค’",
-  "thinking": "๐Ÿค”",
-  "third_place": "๐Ÿฅ‰",
-  "thought_balloon": "๐Ÿ’ญ",
-  "thread": "๐Ÿงต",
-  "three": "3โƒฃ",
-  "thumbsdown": "๐Ÿ‘Ž",
-  "thumbsup": "๐Ÿ‘",
-  "ticket": "๐ŸŽซ",
-  "tiger": "๐Ÿฏ",
-  "tiger2": "๐Ÿ…",
-  "timer_clock": "โฒ๏ธ",
-  "tired_face": "๐Ÿ˜ซ",
-  "tm": "โ„ข",
-  "toilet": "๐Ÿšฝ",
-  "tokyo_tower": "๐Ÿ—ผ",
-  "tomato": "๐Ÿ…",
-  "tone1": "๐Ÿป",
-  "tone2": "๐Ÿผ",
-  "tone3": "๐Ÿฝ",
-  "tone4": "๐Ÿพ",
-  "tone5": "๐Ÿฟ",
-  "tongue": "๐Ÿ‘…",
-  "toolbox": "๐Ÿงฐ",
-  "tooth": "๐Ÿฆท",
-  "top": "๐Ÿ”",
-  "tophat": "๐ŸŽฉ",
-  "tornado": "๐ŸŒช",
-  "track_next": "โญ",
-  "track_previous": "โฎ",
-  "trackball": "๐Ÿ–ฒ",
-  "tractor": "๐Ÿšœ",
-  "trade_mark": "โ„ข๏ธ",
-  "traffic_light": "๐Ÿšฅ",
-  "train": "๐Ÿš‹",
-  "train2": "๐Ÿš†",
-  "tram": "๐ŸšŠ",
-  "trex": "๐Ÿฆ–",
-  "triangular_flag_on_post": "๐Ÿšฉ",
-  "triangular_ruler": "๐Ÿ“",
-  "trident": "๐Ÿ”ฑ",
-  "triumph": "๐Ÿ˜ค",
-  "trolleybus": "๐ŸšŽ",
-  "trophy": "๐Ÿ†",
-  "tropical_drink": "๐Ÿน",
-  "tropical_fish": "๐Ÿ ",
-  "truck": "๐Ÿšš",
-  "trumpet": "๐ŸŽบ",
-  "tulip": "๐ŸŒท",
-  "tumbler_glass": "๐Ÿฅƒ",
-  "turkey": "๐Ÿฆƒ",
-  "turtle": "๐Ÿข",
-  "tv": "๐Ÿ“บ",
-  "twisted_rightwards_arrows": "๐Ÿ”€",
-  "two": "2โƒฃ",
-  "two_hearts": "๐Ÿ’•",
-  "two_men_holding_hands": "๐Ÿ‘ฌ",
-  "two_women_holding_hands": "๐Ÿ‘ญ",
-  "u5272": "๐Ÿˆน",
-  "u5408": "๐Ÿˆด",
-  "u55b6": "๐Ÿˆบ",
-  "u6307": "๐Ÿˆฏ",
-  "u6708": "๐Ÿˆท",
-  "u6709": "๐Ÿˆถ",
-  "u6e80": "๐Ÿˆต",
-  "u7121": "๐Ÿˆš",
-  "u7533": "๐Ÿˆธ",
-  "u7981": "๐Ÿˆฒ",
-  "u7a7a": "๐Ÿˆณ",
-  "umbrella": "โ˜”",
-  "umbrella_on_ground": "โ›ฑ๏ธ",
-  "unamused": "๐Ÿ˜’",
-  "underage": "๐Ÿ”ž",
-  "unicorn": "๐Ÿฆ„",
-  "unlock": "๐Ÿ”“",
-  "up": "๐Ÿ†™",
-  "up_arrow": "โฌ†",
-  "updown_arrow": "โ†•๏ธ",
-  "upleft_arrow": "โ†–๏ธ",
-  "upright_arrow": "โ†—",
-  "upside_down": "๐Ÿ™ƒ",
-  "v": "โœŒ๏ธ",
-  "vampire": "๐Ÿง›",
-  "vertical_traffic_light": "๐Ÿšฆ",
-  "vhs": "๐Ÿ“ผ",
-  "vibration_mode": "๐Ÿ“ณ",
-  "victory_hand": "โœŒ",
-  "video_camera": "๐Ÿ“น",
-  "video_game": "๐ŸŽฎ",
-  "violin": "๐ŸŽป",
-  "virgo": "โ™",
-  "volcano": "๐ŸŒ‹",
-  "volleyball": "๐Ÿ",
-  "vs": "๐Ÿ†š",
-  "vulcan": "๐Ÿ––",
-  "vulcan_salute": "๐Ÿ––",
-  "waffle": "๐Ÿง‡",
-  "walking": "๐Ÿšถ",
-  "waning_crescent_moon": "๐ŸŒ˜",
-  "waning_gibbous_moon": "๐ŸŒ–",
-  "warning": "โš ",
-  "wastebasket": "๐Ÿ—‘",
-  "watch": "โŒš",
-  "water_buffalo": "๐Ÿƒ",
-  "watermelon": "๐Ÿ‰",
-  "wave": "๐Ÿ‘‹",
-  "wavy_dash": "ใ€ฐ๏ธ",
-  "waxing_crescent_moon": "๐ŸŒ’",
-  "waxing_gibbous_moon": "๐ŸŒ”",
-  "wc": "๐Ÿšพ",
-  "weary": "๐Ÿ˜ฉ",
-  "wedding": "๐Ÿ’’",
-  "weightlifter": "๐Ÿ‹",
-  "whale": "๐Ÿณ",
-  "whale2": "๐Ÿ‹",
-  "wheel_of_dharma": "โ˜ธ๏ธ",
-  "wheelchair": "โ™ฟ",
-  "white_check_mark": "โœ…",
-  "white_circle": "โšช",
-  "white_flower": "๐Ÿ’ฎ",
-  "white_hair": "๐Ÿฆณ",
-  "white_heart": "๐Ÿค",
-  "white_large_square": "โฌœ",
-  "white_medium_small_square": "โ—ฝ",
-  "white_medium_square": "โ—ป๏ธ",
-  "white_small_square": "โ–ซ๏ธ",
-  "white_square_button": "๐Ÿ”ณ",
-  "wilted_flower": "๐Ÿฅ€",
-  "wilted_rose": "๐Ÿฅ€",
-  "wind_blowing_face": "๐ŸŒฌ",
-  "wind_chime": "๐ŸŽ",
-  "wine_glass": "๐Ÿท",
-  "wink": "๐Ÿ˜‰",
-  "wolf": "๐Ÿบ",
-  "woman": "๐Ÿ‘ฉ",
-  "woman_with_headscarf": "๐Ÿง•",
-  "womans_clothes": "๐Ÿ‘š",
-  "womans_hat": "๐Ÿ‘’",
-  "womens": "๐Ÿšบ",
-  "woozy_face": "๐Ÿฅด",
-  "world_map": "๐Ÿ—บ",
-  "worried": "๐Ÿ˜Ÿ",
-  "wrench": "๐Ÿ”ง",
-  "writing_hand": "โœ๏ธ",
-  "x": "โŒ",
-  "yarn": "๐Ÿงถ",
-  "yawning_face": "๐Ÿฅฑ",
-  "yellow_circle": "๐ŸŸก",
-  "yellow_heart": "๐Ÿ’›",
-  "yellow_square": "๐ŸŸจ",
-  "yen": "๐Ÿ’ด",
-  "yin_yang": "โ˜ฏ๏ธ",
-  "yoyo": "๐Ÿช€",
-  "yum": "๐Ÿ˜‹",
-  "zany_face": "๐Ÿคช",
-  "zap": "โšก",
-  "zebra": "๐Ÿฆ“",
-  "zero": "0โƒฃ",
-  "zipper_mouth": "๐Ÿค",
-  "zombie": "๐ŸงŸ",
-  "zzz": "๐Ÿ’ค"
-}

+ 10 - 0
yarn.lock

@@ -1629,6 +1629,11 @@
   dependencies:
     pointer-tracker "^2.0.3"
 
+"@kazvmoe-infra/unicode-emoji-json@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@kazvmoe-infra/unicode-emoji-json/-/unicode-emoji-json-0.4.0.tgz#555bab2f8d11db74820ef0a2fbe2805b17c22587"
+  integrity sha512-22OffREdHzD0U6A/W4RaFPV8NR73za6euibtAxNxO/fu5A6TwxRO2lAdbDWKJH9COv/vYs8zqfEiSalXH2nXJA==
+
 "@nightwatch/chai@5.0.2":
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/@nightwatch/chai/-/chai-5.0.2.tgz#86b20908fc090dffd5c9567c0392bc6a494cc2e6"
@@ -5733,6 +5738,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"