logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 03b61f0a9cb09a47d2d9bc89c0a08c62b70c12e2
parent aa9cae8c716789b9c0952914ecbb42c1d6762b98
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Thu, 22 Sep 2022 08:11:25 +0000

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

Group emojis into packs in emoji picker

See merge request pleroma/pleroma-fe!1408

Diffstat:

M.babelrc2+-
M.gitignore1+
Mbuild/build.js3+++
Mbuild/dev-server.js3+++
Abuild/update-emoji.js27+++++++++++++++++++++++++++
Mbuild/webpack.base.conf.js3++-
Mpackage.json2++
Msrc/components/emoji_input/emoji_input.js50+++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/components/emoji_input/emoji_input.vue3++-
Msrc/components/emoji_input/suggestor.js34+++++++++++++++++-----------------
Msrc/components/emoji_picker/emoji_picker.js302+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/components/emoji_picker/emoji_picker.scss52++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/components/emoji_picker/emoji_picker.vue52++++++++++++++++++++++++++++++++++++++--------------
Msrc/components/post_status_form/post_status_form.js6+++---
Msrc/components/react_button/react_button.js4++--
Msrc/components/settings_modal/tabs/profile_tab.js4++--
Msrc/components/still-image/still-image.js27+++++++++++++++++++++++++--
Msrc/components/still-image/still-image.vue5+++--
Msrc/i18n/en.json14+++++++++++++-
Asrc/i18n/languages.js53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/messages.js46+++++++++++++---------------------------------
Msrc/modules/config.js1+
Msrc/modules/instance.js149++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Dstatic/emoji.json1432-------------------------------------------------------------------------------
Myarn.lock10++++++++++
25 files changed, 655 insertions(+), 1630 deletions(-)

diff --git a/.babelrc b/.babelrc @@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], - "comments": false + "comments": true } diff --git a/.gitignore b/.gitignore @@ -7,3 +7,4 @@ test/e2e/reports selenium-debug.log .idea/ config/local.json +static/emoji.json diff --git a/build/build.js b/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) diff --git a/build/dev-server.js b/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 diff --git a/build/update-emoji.js b/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.') + } +} diff --git a/build/webpack.base.conf.js b/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: { diff --git a/package.json b/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", diff --git a/src/components/emoji_input/emoji_input.js b/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() diff --git 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" @@ -63,7 +64,7 @@ v-if="!suggestion.user" class="displayText" > - {{ suggestion.displayText }} + {{ maybeLocalizedEmojiName(suggestion) }} </span> <span class="detailText">{{ suggestion.detailText }}</span> </div> diff --git a/src/components/emoji_input/suggestor.js b/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 }) } diff --git a/src/components/emoji_picker/emoji_picker.js b/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 + } } } } diff --git a/src/components/emoji_picker/emoji_picker.scss b/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; + } } } diff --git a/src/components/emoji_picker/emoji_picker.vue b/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"> diff --git a/src/components/post_status_form/post_status_form.js b/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 || [] diff --git a/src/components/react_button/react_button.js b/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 diff --git a/src/components/settings_modal/tabs/profile_tab.js b/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 ] }) diff --git 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) @@ -42,6 +57,14 @@ const StillImage = { onError () { this.imageLoadError && this.imageLoadError() } + }, + watch: { + src () { + this.realSrc = this.src + }, + dataSrc () { + this.$el.removeAttribute('data-loaded') + } } } diff --git 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" diff --git a/src/i18n/en.json b/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." diff --git a/src/i18n/languages.js b/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 +} diff --git a/src/i18n/messages.js b/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 diff --git a/src/modules/config.js b/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': diff --git a/src/modules/instance.js b/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) diff --git a/static/emoji.json b/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": "๐Ÿ’ค" -} -\ No newline at end of file diff --git a/yarn.lock b/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"