Merge branch 'from/develop/tusooa/grouped-emoji-picker' into 'develop'
Group emojis into packs in emoji picker See merge request pleroma/pleroma-fe!1408
This commit is contained in:
commit
03b61f0a9c
2
.babelrc
2
.babelrc
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["@babel/preset-env"],
|
||||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||||
"comments": false
|
"comments": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,3 +7,4 @@ test/e2e/reports
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
.idea/
|
.idea/
|
||||||
config/local.json
|
config/local.json
|
||||||
|
static/emoji.json
|
||||||
|
|
|
@ -18,6 +18,9 @@ console.log(
|
||||||
var spinner = ora('building for production...')
|
var spinner = ora('building for production...')
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
|
var updateEmoji = require('./update-emoji').updateEmoji
|
||||||
|
updateEmoji()
|
||||||
|
|
||||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||||
rm('-rf', assetsPath)
|
rm('-rf', assetsPath)
|
||||||
mkdir('-p', assetsPath)
|
mkdir('-p', assetsPath)
|
||||||
|
|
|
@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||||
? require('./webpack.prod.conf')
|
? require('./webpack.prod.conf')
|
||||||
: require('./webpack.dev.conf')
|
: require('./webpack.dev.conf')
|
||||||
|
|
||||||
|
var updateEmoji = require('./update-emoji').updateEmoji
|
||||||
|
updateEmoji()
|
||||||
|
|
||||||
// default port where dev server listens for incoming traffic
|
// default port where dev server listens for incoming traffic
|
||||||
var port = process.env.PORT || config.dev.port
|
var port = process.env.PORT || config.dev.port
|
||||||
// Define HTTP proxies to your custom API backend
|
// Define HTTP proxies to your custom API backend
|
||||||
|
|
|
@ -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.')
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,8 @@ module.exports = {
|
||||||
output: {
|
output: {
|
||||||
path: config.build.assetsRoot,
|
path: config.build.assetsRoot,
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||||
filename: '[name].js'
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[name].js'
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
"@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",
|
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
|
||||||
"@vuelidate/core": "2.0.0-alpha.44",
|
"@vuelidate/core": "2.0.0-alpha.44",
|
||||||
"@vuelidate/validators": "2.0.0-alpha.31",
|
"@vuelidate/validators": "2.0.0-alpha.31",
|
||||||
|
@ -34,6 +35,7 @@
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
|
"lozad": "^1.16.0",
|
||||||
"parse-link-header": "2.0.0",
|
"parse-link-header": "2.0.0",
|
||||||
"phoenix": "1.6.2",
|
"phoenix": "1.6.2",
|
||||||
"punycode.js": "2.1.0",
|
"punycode.js": "2.1.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faSmileBeam
|
faSmileBeam
|
||||||
|
@ -143,6 +143,51 @@ const EmojiInput = {
|
||||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||||
return word
|
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 () {
|
mounted () {
|
||||||
|
@ -181,7 +226,7 @@ const EmojiInput = {
|
||||||
const firstchar = newWord.charAt(0)
|
const firstchar = newWord.charAt(0)
|
||||||
this.suggestions = []
|
this.suggestions = []
|
||||||
if (newWord === firstchar) return
|
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
|
// Async: cancel if textAtCaret has changed during wait
|
||||||
if (this.textAtCaret !== newWord) return
|
if (this.textAtCaret !== newWord) return
|
||||||
if (matchedSuggestions.length <= 0) return
|
if (matchedSuggestions.length <= 0) return
|
||||||
|
@ -207,7 +252,6 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
this.focusPickerInput()
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
:class="{ hide: !showPicker }"
|
:class="{ hide: !showPicker }"
|
||||||
|
:showing="showPicker"
|
||||||
:enable-sticker-picker="enableStickerPicker"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
class="emoji-picker-panel"
|
class="emoji-picker-panel"
|
||||||
@emoji="insert"
|
@emoji="insert"
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
v-if="!suggestion.user"
|
v-if="!suggestion.user"
|
||||||
class="displayText"
|
class="displayText"
|
||||||
>
|
>
|
||||||
{{ suggestion.displayText }}
|
{{ maybeLocalizedEmojiName(suggestion) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="detailText">{{ suggestion.detailText }}</span>
|
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* suggest - generates a suggestor function to be used by emoji-input
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
* data: object providing source information for specific types of suggestions:
|
* data: object providing source information for specific types of suggestions:
|
||||||
* data.emoji - optional, an array of all emoji available i.e.
|
* 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
|
* data.users - optional, an array of all known users
|
||||||
* updateUsersList - optional, a function to search and append to users
|
* updateUsersList - optional, a function to search and append to users
|
||||||
*
|
*
|
||||||
|
@ -13,10 +13,10 @@
|
||||||
export default data => {
|
export default data => {
|
||||||
const emojiCurry = suggestEmoji(data.emoji)
|
const emojiCurry = suggestEmoji(data.emoji)
|
||||||
const usersCurry = data.store && suggestUsers(data.store)
|
const usersCurry = data.store && suggestUsers(data.store)
|
||||||
return input => {
|
return (input, nameKeywordLocalizer) => {
|
||||||
const firstChar = input[0]
|
const firstChar = input[0]
|
||||||
if (firstChar === ':' && data.emoji) {
|
if (firstChar === ':' && data.emoji) {
|
||||||
return emojiCurry(input)
|
return emojiCurry(input, nameKeywordLocalizer)
|
||||||
}
|
}
|
||||||
if (firstChar === '@' && usersCurry) {
|
if (firstChar === '@' && usersCurry) {
|
||||||
return usersCurry(input)
|
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)
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
return emojis
|
return emojis
|
||||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||||
.sort((a, b) => {
|
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
|
||||||
let aScore = 0
|
.map(k => {
|
||||||
let bScore = 0
|
let score = 0
|
||||||
|
|
||||||
// An exact match always wins
|
// An exact match always wins
|
||||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
|
||||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
|
||||||
|
|
||||||
// Prioritize custom emoji a lot
|
// Prioritize custom emoji a lot
|
||||||
aScore += a.imageUrl ? 100 : 0
|
score += k.imageUrl ? 100 : 0
|
||||||
bScore += b.imageUrl ? 100 : 0
|
|
||||||
|
|
||||||
// Prioritize prefix matches somewhat
|
// Prioritize prefix matches somewhat
|
||||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
|
||||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
|
||||||
|
|
||||||
// Sort by length
|
// Sort by length
|
||||||
aScore -= a.displayText.length
|
score -= k.displayText.length
|
||||||
bScore -= b.displayText.length
|
|
||||||
|
|
||||||
|
k.score = score
|
||||||
|
return k
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
// Break ties alphabetically
|
// Break ties alphabetically
|
||||||
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
||||||
|
|
||||||
return bScore - aScore + alphabetically
|
return b.score - a.score + alphabetically
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,76 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam,
|
||||||
|
faSmile,
|
||||||
|
faUser,
|
||||||
|
faPaw,
|
||||||
|
faIceCream,
|
||||||
|
faBus,
|
||||||
|
faBasketballBall,
|
||||||
|
faLightbulb,
|
||||||
|
faCode,
|
||||||
|
faFlag
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { trim } from 'lodash'
|
import { debounce, trim } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam,
|
||||||
|
faSmile,
|
||||||
|
faUser,
|
||||||
|
faPaw,
|
||||||
|
faIceCream,
|
||||||
|
faBus,
|
||||||
|
faBasketballBall,
|
||||||
|
faLightbulb,
|
||||||
|
faCode,
|
||||||
|
faFlag
|
||||||
)
|
)
|
||||||
|
|
||||||
// At widest, approximately 20 emoji are visible in a row,
|
const UNICODE_EMOJI_GROUP_ICON = {
|
||||||
// loading 3 rows, could be overkill for narrow picker
|
'smileys-and-emotion': 'smile',
|
||||||
const LOAD_EMOJI_BY = 60
|
'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 maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
|
||||||
const LOAD_EMOJI_MARGIN = 64
|
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
|
if (keyword === '') return list
|
||||||
|
|
||||||
const keywordLowercase = keyword.toLowerCase()
|
const keywordLowercase = keyword.toLowerCase()
|
||||||
const orderedEmojiList = []
|
const orderedEmojiList = []
|
||||||
for (const emoji of list) {
|
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 (indexOfKeyword > -1) {
|
||||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||||
orderedEmojiList[indexOfKeyword] = []
|
orderedEmojiList[indexOfKeyword] = []
|
||||||
|
@ -44,6 +87,10 @@ const EmojiPicker = {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showing: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -53,16 +100,26 @@ const EmojiPicker = {
|
||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
groupsScrolledClass: 'scrolled-top',
|
||||||
keepOpen: false,
|
keepOpen: false,
|
||||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
|
||||||
customEmojiTimeout: null,
|
customEmojiTimeout: null,
|
||||||
customEmojiLoadAllConfirmed: false
|
// Lazy-load only after the first time `showing` becomes true.
|
||||||
|
contentLoaded: false,
|
||||||
|
groupRefs: {},
|
||||||
|
emojiRefs: {},
|
||||||
|
filteredEmojiGroups: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
StillImage
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setGroupRef (name) {
|
||||||
|
return el => { this.groupRefs[name] = el }
|
||||||
|
},
|
||||||
|
setEmojiRef (name) {
|
||||||
|
return el => { this.emojiRefs[name] = el }
|
||||||
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
|
@ -77,10 +134,38 @@ const EmojiPicker = {
|
||||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||||
this.updateScrolledClass(target)
|
this.updateScrolledClass(target)
|
||||||
this.scrolledGroup(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) {
|
highlight (key) {
|
||||||
const ref = this.$refs['group-' + key]
|
const ref = this.groupRefs['group-' + key]
|
||||||
const top = ref.offsetTop
|
const top = ref.offsetTop
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
|
@ -97,73 +182,90 @@ const EmojiPicker = {
|
||||||
this.groupsScrolledClass = 'scrolled-middle'
|
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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrolledGroup (target) {
|
|
||||||
const top = target.scrollTop + 5
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.emojisView.forEach(group => {
|
|
||||||
const ref = this.$refs['group-' + group.id]
|
|
||||||
if (ref.offsetTop <= top) {
|
|
||||||
this.activeGroup = group.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadEmoji () {
|
|
||||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
|
||||||
|
|
||||||
if (allLoaded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
toggleStickers () {
|
toggleStickers () {
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
},
|
},
|
||||||
setShowStickers (value) {
|
setShowStickers (value) {
|
||||||
this.showingStickers = value
|
this.showingStickers = value
|
||||||
|
},
|
||||||
|
filterByKeyword (list, keyword) {
|
||||||
|
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
|
||||||
|
},
|
||||||
|
initializeLazyLoad () {
|
||||||
|
this.destroyLazyLoad()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
waitForDomAndInitializeLazyLoad () {
|
||||||
|
this.$nextTick(() => this.initializeLazyLoad())
|
||||||
|
},
|
||||||
|
destroyLazyLoad () {
|
||||||
|
if (this.$lozad) {
|
||||||
|
if (this.$lozad.observer) {
|
||||||
|
this.$lozad.observer.disconnect()
|
||||||
|
}
|
||||||
|
if (this.$lozad.mutationObserver) {
|
||||||
|
this.$lozad.mutationObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFilteredEmojiGroups () {
|
||||||
|
return this.allEmojiGroups
|
||||||
|
.map(group => ({
|
||||||
|
...group,
|
||||||
|
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
|
||||||
|
}))
|
||||||
|
.filter(group => group.emojis.length > 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
keyword () {
|
keyword () {
|
||||||
this.customEmojiLoadAllConfirmed = false
|
|
||||||
this.onScroll()
|
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: {
|
computed: {
|
||||||
activeGroupView () {
|
activeGroupView () {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
|
@ -174,39 +276,55 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
filteredEmoji () {
|
allCustomGroups () {
|
||||||
return filterByKeyword(
|
return this.$store.getters.groupedCustomEmojis
|
||||||
this.$store.state.instance.customEmoji || [],
|
|
||||||
trim(this.keyword)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
defaultGroup () {
|
||||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
return Object.keys(this.allCustomGroups)[0]
|
||||||
},
|
},
|
||||||
emojis () {
|
unicodeEmojiGroups () {
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiGroupList.map(group => ({
|
||||||
const customEmojis = this.customEmojiBuffer
|
id: `standard-${group.id}`,
|
||||||
|
text: this.$t(`emoji.unicode_groups.${group.id}`),
|
||||||
return [
|
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
|
||||||
{
|
emojis: group.emojis
|
||||||
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))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
emojisView () {
|
allEmojiGroups () {
|
||||||
return this.emojis.filter(value => value.emojis.length > 0)
|
return Object.entries(this.allCustomGroups)
|
||||||
|
.map(([_, v]) => v)
|
||||||
|
.concat(this.unicodeEmojiGroups)
|
||||||
},
|
},
|
||||||
stickerPickerEnabled () {
|
stickerPickerEnabled () {
|
||||||
return (this.$store.state.instance.stickers || []).length !== 0
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
@import '../../_variables.scss';
|
@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 {
|
.emoji-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -19,6 +24,23 @@
|
||||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||||
--icon: var(--popoverIcon, $fallback--icon);
|
--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,
|
.keep-open,
|
||||||
.too-many-emoji {
|
.too-many-emoji {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
|
@ -37,7 +59,6 @@
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
|
||||||
padding: 10px 7px 5px;
|
padding: 10px 7px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +71,10 @@
|
||||||
|
|
||||||
.emoji-tabs {
|
.emoji-tabs {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-groups {
|
.emoji-groups {
|
||||||
|
@ -57,6 +82,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.additional-tabs {
|
.additional-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-left-color: $fallback--icon;
|
border-left-color: $fallback--icon;
|
||||||
border-left-color: var(--icon, $fallback--icon);
|
border-left-color: var(--icon, $fallback--icon);
|
||||||
|
@ -66,15 +93,20 @@
|
||||||
|
|
||||||
.additional-tabs,
|
.additional-tabs,
|
||||||
.emoji-tabs {
|
.emoji-tabs {
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
flex-shrink: 1;
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
padding: 0 7px;
|
padding: 0 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.85em;
|
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 {
|
&.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -164,22 +196,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 32px;
|
width: $emoji-picker-emoji-size;
|
||||||
height: 32px;
|
height: $emoji-picker-emoji-size;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 32px;
|
line-height: $emoji-picker-emoji-size;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
.emoji-picker-emoji.-custom {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
.emoji-picker-emoji.-unicode {
|
||||||
|
font-size: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="emoji-picker panel panel-default panel-body">
|
<div
|
||||||
|
class="emoji-picker panel panel-default panel-body"
|
||||||
|
>
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span class="emoji-tabs">
|
<span
|
||||||
|
ref="header"
|
||||||
|
class="emoji-tabs"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-for="group in emojis"
|
v-for="group in filteredEmojiGroups"
|
||||||
|
:ref="setGroupRef('group-header-' + group.id)"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="emoji-tabs-item"
|
class="emoji-tabs-item"
|
||||||
:class="{
|
:class="{
|
||||||
active: activeGroupView === group.id,
|
active: activeGroupView === group.id
|
||||||
disabled: group.emojis.length === 0
|
|
||||||
}"
|
}"
|
||||||
:title="group.text"
|
:title="group.text"
|
||||||
@click.prevent="highlight(group.id)"
|
@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
|
<FAIcon
|
||||||
|
v-else
|
||||||
:icon="group.icon"
|
:icon="group.icon"
|
||||||
fixed-width
|
fixed-width
|
||||||
/>
|
/>
|
||||||
|
@ -36,7 +51,10 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div
|
||||||
|
v-if="contentLoaded"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="emoji-content"
|
class="emoji-content"
|
||||||
:class="{hidden: showingStickers}"
|
:class="{hidden: showingStickers}"
|
||||||
|
@ -57,12 +75,12 @@
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="group in emojisView"
|
v-for="group in filteredEmojiGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="emoji-group"
|
class="emoji-group"
|
||||||
>
|
>
|
||||||
<h6
|
<h6
|
||||||
:ref="'group-' + group.id"
|
:ref="setGroupRef('group-' + group.id)"
|
||||||
class="emoji-group-title"
|
class="emoji-group-title"
|
||||||
>
|
>
|
||||||
{{ group.text }}
|
{{ group.text }}
|
||||||
|
@ -70,17 +88,23 @@
|
||||||
<span
|
<span
|
||||||
v-for="emoji in group.emojis"
|
v-for="emoji in group.emojis"
|
||||||
:key="group.id + emoji.displayText"
|
:key="group.id + emoji.displayText"
|
||||||
:title="emoji.displayText"
|
:title="maybeLocalizedEmojiName(emoji)"
|
||||||
class="emoji-item"
|
class="emoji-item"
|
||||||
@click.stop.prevent="onEmoji(emoji)"
|
@click.stop.prevent="onEmoji(emoji)"
|
||||||
>
|
>
|
||||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
<span
|
||||||
<img
|
v-if="!emoji.imageUrl"
|
||||||
|
class="emoji-picker-emoji -unicode"
|
||||||
|
>{{ emoji.replacement }}</span>
|
||||||
|
<still-image
|
||||||
v-else
|
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>
|
||||||
<span :ref="'group-end-' + group.id" />
|
<span :ref="setGroupRef('group-end-' + group.id)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="keep-open">
|
<div class="keep-open">
|
||||||
|
|
|
@ -189,7 +189,7 @@ const PostStatusForm = {
|
||||||
emojiUserSuggestor () {
|
emojiUserSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
],
|
],
|
||||||
store: this.$store
|
store: this.$store
|
||||||
|
@ -198,13 +198,13 @@ const PostStatusForm = {
|
||||||
emojiSuggestor () {
|
emojiSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
emoji () {
|
emoji () {
|
||||||
return this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiList || []
|
||||||
},
|
},
|
||||||
customEmoji () {
|
customEmoji () {
|
||||||
return this.$store.state.instance.customEmoji || []
|
return this.$store.state.instance.customEmoji || []
|
||||||
|
|
|
@ -59,7 +59,7 @@ const ReactButton = {
|
||||||
if (this.filterWord !== '') {
|
if (this.filterWord !== '') {
|
||||||
const filterWordLowercase = trim(this.filterWord.toLowerCase())
|
const filterWordLowercase = trim(this.filterWord.toLowerCase())
|
||||||
const orderedEmojiList = []
|
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]
|
if (emoji.replacement === this.filterWord) return [emoji]
|
||||||
|
|
||||||
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
|
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
|
||||||
|
@ -72,7 +72,7 @@ const ReactButton = {
|
||||||
}
|
}
|
||||||
return orderedEmojiList.flat()
|
return orderedEmojiList.flat()
|
||||||
}
|
}
|
||||||
return this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiList || []
|
||||||
},
|
},
|
||||||
mergedConfig () {
|
mergedConfig () {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
|
|
|
@ -64,7 +64,7 @@ const ProfileTab = {
|
||||||
emojiUserSuggestor () {
|
emojiUserSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
],
|
],
|
||||||
store: this.$store
|
store: this.$store
|
||||||
|
@ -73,7 +73,7 @@ const ProfileTab = {
|
||||||
emojiSuggestor () {
|
emojiSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,16 +7,23 @@ const StillImage = {
|
||||||
'imageLoadHandler',
|
'imageLoadHandler',
|
||||||
'alt',
|
'alt',
|
||||||
'height',
|
'height',
|
||||||
'width'
|
'width',
|
||||||
|
'dataSrc'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
// for lazy loading, see loadLazy()
|
||||||
|
realSrc: this.src,
|
||||||
stopGifs: this.$store.getters.mergedConfig.stopGifs
|
stopGifs: this.$store.getters.mergedConfig.stopGifs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
animated () {
|
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 () {
|
style () {
|
||||||
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
|
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
|
||||||
|
@ -27,7 +34,15 @@ const StillImage = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadLazy () {
|
||||||
|
if (this.dataSrc) {
|
||||||
|
this.realSrc = this.dataSrc
|
||||||
|
}
|
||||||
|
},
|
||||||
onLoad () {
|
onLoad () {
|
||||||
|
if (!this.realSrc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const image = this.$refs.src
|
const image = this.$refs.src
|
||||||
if (!image) return
|
if (!image) return
|
||||||
this.imageLoadHandler && this.imageLoadHandler(image)
|
this.imageLoadHandler && this.imageLoadHandler(image)
|
||||||
|
@ -42,6 +57,14 @@ const StillImage = {
|
||||||
onError () {
|
onError () {
|
||||||
this.imageLoadError && this.imageLoadError()
|
this.imageLoadError && this.imageLoadError()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src () {
|
||||||
|
this.realSrc = this.src
|
||||||
|
},
|
||||||
|
dataSrc () {
|
||||||
|
this.$el.removeAttribute('data-loaded')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,11 @@
|
||||||
<!-- NOTE: key is required to force to re-render img tag when src is changed -->
|
<!-- NOTE: key is required to force to re-render img tag when src is changed -->
|
||||||
<img
|
<img
|
||||||
ref="src"
|
ref="src"
|
||||||
:key="src"
|
:key="realSrc"
|
||||||
:alt="alt"
|
:alt="alt"
|
||||||
:title="alt"
|
:title="alt"
|
||||||
:src="src"
|
:data-src="dataSrc"
|
||||||
|
:src="realSrc"
|
||||||
:referrerpolicy="referrerpolicy"
|
:referrerpolicy="referrerpolicy"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
@error="onError"
|
@error="onError"
|
||||||
|
|
|
@ -199,8 +199,20 @@
|
||||||
"add_emoji": "Insert emoji",
|
"add_emoji": "Insert emoji",
|
||||||
"custom": "Custom emoji",
|
"custom": "Custom emoji",
|
||||||
"unicode": "Unicode 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_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": {
|
"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."
|
"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."
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
// 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.
|
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
|
||||||
|
|
||||||
const loaders = {
|
import { languages, langCodeToJsonName } from './languages.js'
|
||||||
ar: () => import('./ar.json'),
|
|
||||||
ca: () => import('./ca.json'),
|
const hasLanguageFile = (code) => languages.includes(code)
|
||||||
cs: () => import('./cs.json'),
|
|
||||||
de: () => import('./de.json'),
|
const loadLanguageFile = (code) => {
|
||||||
eo: () => import('./eo.json'),
|
return import(
|
||||||
es: () => import('./es.json'),
|
/* webpackInclude: /\.json$/ */
|
||||||
et: () => import('./et.json'),
|
/* webpackChunkName: "i18n/[request]" */
|
||||||
eu: () => import('./eu.json'),
|
`./${langCodeToJsonName(code)}.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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
languages: ['en', ...Object.keys(loaders)],
|
languages,
|
||||||
default: {
|
default: {
|
||||||
en: require('./en.json').default
|
en: require('./en.json').default
|
||||||
},
|
},
|
||||||
setLanguage: async (i18n, language) => {
|
setLanguage: async (i18n, language) => {
|
||||||
if (loaders[language]) {
|
if (hasLanguageFile(language)) {
|
||||||
const messages = await loaders[language]()
|
const messages = await loadLanguageFile(language)
|
||||||
i18n.setLocaleMessage(language, messages.default)
|
i18n.setLocaleMessage(language, messages.default)
|
||||||
}
|
}
|
||||||
i18n.locale = language
|
i18n.locale = language
|
||||||
|
|
|
@ -183,6 +183,7 @@ const config = {
|
||||||
break
|
break
|
||||||
case 'interfaceLanguage':
|
case 'interfaceLanguage':
|
||||||
messages.setLanguage(this.getters.i18n, value)
|
messages.setLanguage(this.getters.i18n, value)
|
||||||
|
dispatch('loadUnicodeEmojiData', value)
|
||||||
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
|
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
|
||||||
break
|
break
|
||||||
case 'thirdColumnMode':
|
case 'thirdColumnMode':
|
||||||
|
|
|
@ -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 { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
import apiService from '../services/api/api.service.js'
|
import apiService from '../services/api/api.service.js'
|
||||||
import { instanceDefaultProperties } from './config.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 = {
|
const defaultState = {
|
||||||
// Stuff from apiConfig
|
// Stuff from apiConfig
|
||||||
|
@ -64,8 +97,9 @@ const defaultState = {
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
customEmojiFetched: false,
|
customEmojiFetched: false,
|
||||||
emoji: [],
|
emoji: {},
|
||||||
emojiFetched: false,
|
emojiFetched: false,
|
||||||
|
unicodeEmojiAnnotations: {},
|
||||||
pleromaBackend: true,
|
pleromaBackend: true,
|
||||||
postFormats: [],
|
postFormats: [],
|
||||||
restrictedNicknames: [],
|
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 = {
|
const instance = {
|
||||||
state: defaultState,
|
state: defaultState,
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -107,6 +166,9 @@ const instance = {
|
||||||
},
|
},
|
||||||
setKnownDomains (state, domains) {
|
setKnownDomains (state, domains) {
|
||||||
state.knownDomains = domains
|
state.knownDomains = domains
|
||||||
|
},
|
||||||
|
setUnicodeEmojiAnnotations (state, { lang, annotations }) {
|
||||||
|
state.unicodeEmojiAnnotations[lang] = annotations
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -115,6 +177,41 @@ const instance = {
|
||||||
.map(key => [key, state[key]])
|
.map(key => [key, state[key]])
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
|
.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) {
|
instanceDomain (state) {
|
||||||
return new URL(state.server).hostname
|
return new URL(state.server).hostname
|
||||||
}
|
}
|
||||||
|
@ -138,32 +235,52 @@ const instance = {
|
||||||
},
|
},
|
||||||
async getStaticEmoji ({ commit }) {
|
async getStaticEmoji ({ commit }) {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/static/emoji.json')
|
const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
|
||||||
if (res.ok) {
|
|
||||||
const values = await res.json()
|
const emoji = Object.keys(values).reduce((res, groupId) => {
|
||||||
const emoji = Object.keys(values).map((key) => {
|
res[groupId] = values[groupId].map(e => ({
|
||||||
return {
|
displayText: e.slug,
|
||||||
displayText: key,
|
imageUrl: false,
|
||||||
imageUrl: false,
|
replacement: e.emoji
|
||||||
replacement: values[key]
|
}))
|
||||||
}
|
return res
|
||||||
}).sort((a, b) => a.name > b.name ? 1 : -1)
|
}, {})
|
||||||
commit('setInstanceOption', { name: 'emoji', value: emoji })
|
commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
|
||||||
} else {
|
|
||||||
throw (res)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Can't load static emoji")
|
console.warn("Can't load static emoji")
|
||||||
console.warn(e)
|
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 }) {
|
async getCustomEmoji ({ commit, state }) {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
const res = await window.fetch('/api/pleroma/emoji.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const result = await res.json()
|
const result = await res.json()
|
||||||
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
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 emoji = Object.entries(values).map(([key, value]) => {
|
||||||
const imageUrl = value.image_url
|
const imageUrl = value.image_url
|
||||||
return {
|
return {
|
||||||
|
@ -174,7 +291,7 @@ const instance = {
|
||||||
}
|
}
|
||||||
// Technically could use tags but those are kinda useless right now,
|
// Technically could use tags but those are kinda useless right now,
|
||||||
// should have been "pack" field, that would be more useful
|
// 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 })
|
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw (res)
|
||||||
|
|
1431
static/emoji.json
1431
static/emoji.json
File diff suppressed because it is too large
Load Diff
10
yarn.lock
10
yarn.lock
|
@ -1629,6 +1629,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
pointer-tracker "^2.0.3"
|
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":
|
"@nightwatch/chai@5.0.2":
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@nightwatch/chai/-/chai-5.0.2.tgz#86b20908fc090dffd5c9567c0392bc6a494cc2e6"
|
resolved "https://registry.yarnpkg.com/@nightwatch/chai/-/chai-5.0.2.tgz#86b20908fc090dffd5c9567c0392bc6a494cc2e6"
|
||||||
|
@ -5733,6 +5738,11 @@ lower-case@^2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.3"
|
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:
|
lru-cache@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||||
|
|
Loading…
Reference in New Issue