Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85832597
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index d04649dcca..cab952f8ef 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,256 +1,301 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
import { trim } from 'lodash'
library.add(
faBoxOpen,
faStickyNote,
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const filterByKeyword = (list, keyword = '') => {
if (keyword === '') return list
const keywordLowercase = keyword.toLowerCase()
const orderedEmojiList = []
for (const emoji of list) {
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
if (indexOfKeyword > -1) {
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
orderedEmojiList[indexOfKeyword] = []
}
orderedEmojiList[indexOfKeyword].push(emoji)
}
}
return orderedEmojiList.flat()
}
const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
const EmojiPicker = {
props: {
enableStickerPicker: {
required: false,
type: Boolean,
default: false
}
},
data () {
return {
keyword: '',
activeGroup: 'custom',
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
- customEmojiLoadAllConfirmed: false
+ customEmojiLoadAllConfirmed: false,
+ groupLoadedCount: {}
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox
},
methods: {
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
- this.triggerLoadMore(target)
+ this.$nextTick(() => {
+ this.triggerLoadMore(target)
+ })
},
highlight (key) {
const ref = this.$refs['group-' + key]
const top = ref.offsetTop
this.setShowStickers(false)
this.activeGroup = key
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = top + 1
+ this.loadEmoji(key)
})
},
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
}
},
triggerLoadMore (target) {
- const ref = this.$refs[`group-end-${this.lastNonUnicodeGroupId}`][0]
- if (!ref) return
- const bottom = ref.offsetTop + ref.offsetHeight
+ Object.keys(this.allCustomGroups)
+ .map(groupId => {
+ const ref = this.$refs[`group-end-${groupId}`][0]
+ if (!ref) return undefined
- const scrollerBottom = target.scrollTop + target.clientHeight
- const scrollerTop = target.scrollTop
- const scrollerMax = target.scrollHeight
+ const bottom = ref.offsetTop + ref.offsetHeight
- // 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()
- }
+ const group = this.$refs[`group-${groupId}`][0]
+ const top = group.offsetTop
+
+ 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 < top + target.clientHeight / 2 && top < scrollerBottom
+ // Don't load when looking at unicode category or at the very bottom
+ const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
+ if (!bottomAboveViewport && (approachingBottom || atTop)) {
+ return groupId
+ }
+ return undefined
+ })
+ .filter(k => k)
+ .map(k => {
+ this.loadEmoji(k)
+ })
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
- this.emojisView.forEach(group => {
+ this.allEmojiGroups.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
+ loadEmoji (loadGroup) {
+ if (!this.allCustomGroups[loadGroup]) {
+ return
+ }
+
+ const allLoaded = this.loadedCount[loadGroup] >= this.allCustomGroups[loadGroup].emojis.length
if (allLoaded) {
return
}
- this.customEmojiBufferSlice += LOAD_EMOJI_BY
+ this.groupLoadedCount = {
+ ...this.groupLoadedCount,
+ [loadGroup]: this.loadedCount[loadGroup] + 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 () {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
this.showingStickers = value
+ },
+ limitedEmojis (list, groupId) {
+ return list.slice(0, this.loadedCount[groupId])
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
},
stickersAvailable () {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
allEmojis () {
return this.$store.state.instance.customEmoji || []
},
filteredEmoji () {
return filterByKeyword(
this.allEmojis,
trim(this.keyword)
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
groupedCustomEmojis () {
return this.customEmojiBuffer.reduce((res, emoji) => {
const pack = packOf(emoji)
if (!res[pack]) {
res[pack] = {
id: `custom-${pack}`,
text: pack,
/// FIXME
// icon: 'smile-beam',
image: emoji.imageUrl,
emojis: []
}
}
res[pack].emojis.push(emoji)
return res
}, {})
},
- allEmojiGroups () {
- return this.allEmojis
+ allCustomGroups () {
+ return this.filteredEmoji
.reduce((res, emoji) => {
const packName = packOf(emoji)
const packId = `custom-${packName}`
- if (res.filter(k => k.id === packId).length === 0) {
- res.push({
+ if (!res[packId]) {
+ res[packId] = ({
id: packId,
text: packName,
- image: emoji.imageUrl
+ image: emoji.imageUrl,
+ emojis: []
})
}
+ res[packId].emojis.push(emoji)
return res
- }, [])
+ }, {})
+ },
+ sensibleInitialAmountForAGroup () {
+ const groupCount = Object.keys(this.allCustomGroups).length
+ return Math.max(Math.floor(LOAD_EMOJI_BY / Math.max(groupCount, 1)), 1)
+ },
+ allEmojiGroups () {
+ const standardEmojis = this.$store.state.instance.emoji || []
+ return Object.entries(this.allCustomGroups)
+ .map(([_, v]) => v)
.concat({
id: 'standard',
text: this.$t('emoji.unicode'),
- icon: 'box-open'
+ icon: 'box-open',
+ emojis: filterByKeyword(standardEmojis, this.keyword)
})
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
// const customEmojis = this.customEmojiBuffer
return [
...Object
.keys(this.groupedCustomEmojis)
.map(k => this.groupedCustomEmojis[k]),
{
id: 'standard',
text: this.$t('emoji.unicode'),
icon: 'box-open',
emojis: filterByKeyword(standardEmojis, trim(this.keyword))
}
]
},
+ loadedCount () {
+ return Object.keys(this.allCustomGroups)
+ .reduce((res, groupId) => {
+ res[groupId] = this.groupLoadedCount[groupId] || this.sensibleInitialAmountForAGroup
+ return res
+ }, {})
+ },
lastNonUnicodeGroupId () {
return this.emojis[this.emojis.length - 2].id
},
emojisView () {
return this.emojis.filter(value => value.emojis.length > 0)
},
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0
}
}
}
export default EmojiPicker
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index fe60cb5ef6..277d5bf625 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,116 +1,116 @@
<template>
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span class="emoji-tabs">
<span
v-for="group in allEmojiGroups"
:key="group.id"
class="emoji-tabs-item"
:class="{
active: activeGroupView === group.id,
disabled: false
}"
:title="group.text"
@click.prevent="highlight(group.id)"
>
<span
v-if="group.image"
class="emoji-picker-header-image"
>
<img
:alt="group.text"
:src="group.image"
>
</span>
<FAIcon
v-else
:icon="group.icon"
fixed-width
/>
</span>
</span>
<span
v-if="stickerPickerEnabled"
class="additional-tabs"
>
<span
class="stickers-tab-icon additional-tabs-item"
:class="{active: showingStickers}"
:title="$t('emoji.stickers')"
@click.prevent="toggleStickers"
>
<FAIcon
icon="sticky-note"
fixed-width
/>
</span>
</span>
</div>
<div class="content">
<div
class="emoji-content"
:class="{hidden: showingStickers}"
>
<div class="emoji-search">
<input
v-model="keyword"
type="text"
class="form-control"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
</div>
<div
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="onScroll"
>
<div
- v-for="group in emojisView"
+ v-for="group in allEmojiGroups"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
- v-for="emoji in group.emojis"
+ v-for="emoji in limitedEmojis(group.emojis, group.id)"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
</div>
<div
v-if="showingStickers"
class="stickers-content"
>
<sticker-picker
@uploaded="onStickerUploaded"
@upload-failed="onStickerUploadFailed"
/>
</div>
</div>
</div>
</template>
<script src="./emoji_picker.js"></script>
<style lang="scss" src="./emoji_picker.scss"></style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 2, 8:42 AM (5 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803945
Default Alt Text
(13 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline