diff options
| author | HJ <30-hj@users.noreply.git.pleroma.social> | 2022-09-22 08:11:25 +0000 |
|---|---|---|
| committer | HJ <30-hj@users.noreply.git.pleroma.social> | 2022-09-22 08:11:25 +0000 |
| commit | 03b61f0a9cb09a47d2d9bc89c0a08c62b70c12e2 (patch) | |
| tree | 4454f5fbc5f2fff4756e410744dc36cba6e35f19 | |
| parent | aa9cae8c716789b9c0952914ecbb42c1d6762b98 (diff) | |
| parent | a7f836a64e334d6c46ed1e12c7bf9b2ff4a09c7e (diff) | |
Merge branch 'from/develop/tusooa/grouped-emoji-picker' into 'develop'
Group emojis into packs in emoji picker
See merge request pleroma/pleroma-fe!1408
25 files changed, 655 insertions, 1629 deletions
@@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], - "comments": false + "comments": true } @@ -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 index b3c9aad4..35969eb6 100644 --- 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 index 854efa0b..e51ba948 100644 --- 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 new file mode 100644 index 00000000..9f4b4e67 --- /dev/null +++ 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 index 78b75e3f..bf946922 100644 --- 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 index 50a6b55f..260df573 100644 --- 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 index b664d6b3..ffc0ffac 100644 --- 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 index 81b81913..43581dbf 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -19,6 +19,7 @@ v-if="enableEmojiPicker" ref="picker" :class="{ hide: !showPicker }" + :showing="showPicker" :enable-sticker-picker="enableStickerPicker" class="emoji-picker-panel" @emoji="insert" @@ -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 index 0ddb4d68..adaa879e 100644 --- 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 index f6920208..fafc2af1 100644 --- 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 index a2f17c51..016c46d7 100644 --- 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 index a7269120..57bb0037 100644 --- 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 index 77f73d04..5c536b74 100644 --- 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 index 5e052e1e..e65bfd93 100644 --- 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 index 376248ef..b86faef0 100644 --- 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 index d7abbcb5..200ef147 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -7,16 +7,23 @@ const StillImage = { 'imageLoadHandler', 'alt', 'height', - 'width' + 'width', + 'dataSrc' ], data () { return { + // for lazy loading, see loadLazy() + realSrc: this.src, stopGifs: this.$store.getters.mergedConfig.stopGifs } }, computed: { animated () { - return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) + if (!this.realSrc) { + return false + } + + return this.stopGifs && (this.mimetype === 'image/gif' || this.realSrc.endsWith('.gif')) }, style () { const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str @@ -27,7 +34,15 @@ const StillImage = { } }, methods: { + loadLazy () { + if (this.dataSrc) { + this.realSrc = this.dataSrc + } + }, onLoad () { + if (!this.realSrc) { + return + } const image = this.$refs.src if (!image) return this.imageLoadHandler && this.imageLoadHandler(image) @@ -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 index ab3080c8..633fb229 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -11,10 +11,11 @@ <!-- NOTE: key is required to force to re-render img tag when src is changed --> <img ref="src" - :key="src" + :key="realSrc" :alt="alt" :title="alt" - :src="src" + :data-src="dataSrc" + :src="realSrc" :referrerpolicy="referrerpolicy" @load="onLoad" @error="onError" diff --git a/src/i18n/en.json b/src/i18n/en.json index 22ec42a9..30b59e82 100644 --- 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 new file mode 100644 index 00000000..250b3b1a --- /dev/null +++ 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 index eae75c80..74a89ca8 100644 --- 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 index eeaac917..c966602e 100644 --- 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 index bfce6f38..b1bc9779 100644 --- 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 deleted file mode 100644 index 12b91b3f..00000000 --- a/static/emoji.json +++ /dev/null @@ -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 @@ -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" |
