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:
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"