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

* from/develop/tusooa/grouped-emoji-picker: (34 commits)
  Fix emoji picker lint
  Fix emoji picker lint
  Tweak efficiency when changing filter keywords in emoji picker
  Use trimmed keyword for filtering emojis
  Limit the width of unsupported multichar emojis
  Make emoji picker work with vue3
  Make StillImage react to src changes
  Lint
  Add English translation for unicode emoji group names
  Add icons for unicode emoji groups
  Make emoji picker use grouped unicode emojis
  Generate grouped unicode emojis from unicode-emoji-json
  Scroll active tab header into view in emoji picker
  Clean up emoji picker css
  Use StillImage to render emojis in emoji picker
  Fix error on emoji picker first load
  Group emojis only by pack and remove pack: prefix
  Lint
  Lazy-load emoji picker in post form
  Fix sticker picker heading tab
  ...
This commit is contained in:
Henry Jameson 2022-08-18 11:50:40 +03:00
commit 14be662918
20 changed files with 413 additions and 1569 deletions

1
.gitignore vendored
View file

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

View file

@ -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)

View file

@ -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

27
build/update-emoji.js Normal file
View file

@ -0,0 +1,27 @@
module.exports = {
updateEmoji () {
const emojis = require('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.log('Updating emojis...')
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
console.log('Done.')
}
}

View file

@ -35,6 +35,7 @@
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"lozad": "^1.16.0",
"phoenix": "1.6.2", "phoenix": "1.6.2",
"punycode.js": "2.1.0", "punycode.js": "2.1.0",
"qrcode": "1.5.0", "qrcode": "1.5.0",
@ -117,6 +118,7 @@
"stylelint-config-standard": "20.0.0", "stylelint-config-standard": "20.0.0",
"stylelint-rscss": "0.4.0", "stylelint-rscss": "0.4.0",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"unicode-emoji-json": "^0.3.0",
"vue-style-loader": "4.1.3", "vue-style-loader": "4.1.3",
"webpack": "5.74.0", "webpack": "5.74.0",
"webpack-dev-middleware": "3.7.3", "webpack-dev-middleware": "3.7.3",

View file

@ -205,7 +205,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()

View file

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

View file

@ -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
* *

View file

@ -1,25 +1,50 @@
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 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',
// When to start loading new batch emoji, in pixels 'food-and-drink': 'ice-cream',
const LOAD_EMOJI_MARGIN = 64 'travel-and-places': 'bus',
activities: 'basketball-ball',
objects: 'lightbulb',
symbols: 'code',
flags: 'flag'
}
const filterByKeyword = (list, keyword = '') => { const filterByKeyword = (list, keyword = '') => {
if (keyword === '') return list if (keyword === '') return list
@ -44,6 +69,10 @@ const EmojiPicker = {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
showing: {
required: true,
type: Boolean
} }
}, },
data () { data () {
@ -53,16 +82,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 +116,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,72 +164,89 @@ 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)
},
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: 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 () {
@ -174,39 +258,33 @@ 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
}, },
{ allEmojiGroups () {
id: 'standard', return Object.entries(this.allCustomGroups)
text: this.$t('emoji.unicode'), .map(([_, v]) => v)
icon: 'box-open', .concat(this.unicodeEmojiGroups)
emojis: filterByKeyword(standardEmojis, trim(this.keyword))
}
]
},
emojisView () {
return this.emojis.filter(value => value.emojis.length > 0)
}, },
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)
} }
} }
} }

View file

@ -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,21 @@
--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%;
object-fit: contain;
}
}
.keep-open, .keep-open,
.too-many-emoji { .too-many-emoji {
padding: 7px; padding: 7px;
@ -37,7 +57,6 @@
.heading { .heading {
display: flex; display: flex;
height: 32px;
padding: 10px 7px 5px; padding: 10px 7px 5px;
} }
@ -50,6 +69,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 +80,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 +91,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 +194,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;
}
} }
} }

View file

@ -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 <span
v-for="group in emojis" ref="header"
class="emoji-tabs"
>
<span
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 }}
@ -74,13 +92,19 @@
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">

View file

@ -164,7 +164,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
@ -173,13 +173,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 || []

View file

@ -46,7 +46,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)
@ -59,7 +59,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

View file

@ -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
] ]
}) })

View file

@ -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')
}
} }
} }

View file

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

View file

@ -196,6 +196,17 @@
"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"
}, },

View file

@ -3,6 +3,18 @@ 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'
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 defaultState = { const defaultState = {
// Stuff from apiConfig // Stuff from apiConfig
name: 'Pleroma FE', name: 'Pleroma FE',
@ -64,7 +76,7 @@ const defaultState = {
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],
customEmojiFetched: false, customEmojiFetched: false,
emoji: [], emoji: {},
emojiFetched: false, emojiFetched: false,
pleromaBackend: true, pleromaBackend: true,
postFormats: [], postFormats: [],
@ -115,6 +127,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] || [])
.reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList (state) {
return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
id: groupId,
emojis: state.emoji[groupId] || []
}))
},
instanceDomain (state) { instanceDomain (state) {
return new URL(state.server).hostname return new URL(state.server).hostname
} }
@ -141,13 +188,14 @@ const instance = {
const res = await window.fetch('/static/emoji.json') const res = await window.fetch('/static/emoji.json')
if (res.ok) { if (res.ok) {
const values = await res.json() const values = await res.json()
const emoji = Object.keys(values).map((key) => { const emoji = Object.keys(values).reduce((res, groupId) => {
return { res[groupId] = values[groupId].map(e => ({
displayText: key, displayText: e.name,
imageUrl: false, imageUrl: false,
replacement: values[key] replacement: e.emoji
} }))
}).sort((a, b) => a.name > b.name ? 1 : -1) return res
}, {})
commit('setInstanceOption', { name: 'emoji', value: emoji }) commit('setInstanceOption', { name: 'emoji', value: emoji })
} else { } else {
throw (res) throw (res)
@ -164,6 +212,16 @@ const instance = {
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 +232,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)

File diff suppressed because it is too large Load diff

View file

@ -5924,6 +5924,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"
@ -8492,6 +8497,11 @@ unicode-canonical-property-names-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==
unicode-emoji-json@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/unicode-emoji-json/-/unicode-emoji-json-0.3.0.tgz#965e097ef8a367081c5036f81873015a95a5c1ad"
integrity sha512-yImheILedqhZtVEEenRELu9AnX347ZTA3bVMWAU5WMUv7pdU2hcfpwo2mKN8Rns9uHLmOZP90/4B4SPS5aF/iw==
unicode-match-property-ecmascript@^2.0.0: unicode-match-property-ecmascript@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3"