From 383743615c06385aa1ac807da98a26076bcb815d Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Thu, 16 Jun 2022 12:26:28 +0100 Subject: [PATCH 1/4] reuse emoji picker for react-o-tron --- .../emoji_reaction_picker.js | 213 ++++++++++++++++++ .../emoji_reaction_picker.scss | 186 +++++++++++++++ .../emoji_reaction_picker.vue | 55 +++++ src/components/react_button/react_button.js | 41 +--- src/components/react_button/react_button.vue | 40 +--- 5 files changed, 462 insertions(+), 73 deletions(-) create mode 100644 src/components/emoji_reaction_picker/emoji_reaction_picker.js create mode 100644 src/components/emoji_reaction_picker/emoji_reaction_picker.scss create mode 100644 src/components/emoji_reaction_picker/emoji_reaction_picker.vue diff --git a/src/components/emoji_reaction_picker/emoji_reaction_picker.js b/src/components/emoji_reaction_picker/emoji_reaction_picker.js new file mode 100644 index 00000000..6b589079 --- /dev/null +++ b/src/components/emoji_reaction_picker/emoji_reaction_picker.js @@ -0,0 +1,213 @@ +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' + +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() + let 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 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 + } + }, + 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) + }, + 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 + }) + }, + 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-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 () { + this.showingStickers = !this.showingStickers + }, + setShowStickers (value) { + this.showingStickers = value + } + }, + 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 + }, + filteredEmoji () { + return filterByKeyword( + this.$store.state.instance.customEmoji || [], + this.keyword + ) + }, + customEmojiBuffer () { + return this.filteredEmoji.slice(0, this.customEmojiBufferSlice) + }, + emojis () { + const standardEmojis = this.$store.state.instance.emoji || [] + const customEmojis = this.customEmojiBuffer + + return [ + { + id: 'custom', + text: this.$t('emoji.custom'), + icon: 'smile-beam', + emojis: customEmojis + }, + { + id: 'standard', + text: this.$t('emoji.unicode'), + icon: 'box-open', + emojis: filterByKeyword(standardEmojis, this.keyword) + } + ] + }, + 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_reaction_picker/emoji_reaction_picker.scss b/src/components/emoji_reaction_picker/emoji_reaction_picker.scss new file mode 100644 index 00000000..2055e02e --- /dev/null +++ b/src/components/emoji_reaction_picker/emoji_reaction_picker.scss @@ -0,0 +1,186 @@ +@import '../../_variables.scss'; + +.emoji-picker { + display: flex; + flex-direction: column; + position: absolute; + right: 0; + left: 0; + margin: 0 !important; + z-index: 100; + background-color: $fallback--bg; + background-color: var(--popover, $fallback--bg); + color: $fallback--link; + color: var(--popoverText, $fallback--link); + --lightText: var(--popoverLightText, $fallback--faint); + --faint: var(--popoverFaintText, $fallback--faint); + --faintLink: var(--popoverFaintLink, $fallback--faint); + --lightText: var(--popoverLightText, $fallback--lightText); + --icon: var(--popoverIcon, $fallback--icon); + + .keep-open, + .too-many-emoji { + padding: 7px; + line-height: normal; + } + + .too-many-emoji { + display: flex; + flex-direction: column; + } + + .keep-open-label { + padding: 0 7px; + display: flex; + } + + .heading { + display: flex; + height: 32px; + padding: 10px 7px 5px; + } + + .content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0px; + } + + .emoji-tabs { + flex-grow: 1; + } + + .emoji-groups { + min-height: 200px; + } + + .additional-tabs { + border-left: 1px solid; + border-left-color: $fallback--icon; + border-left-color: var(--icon, $fallback--icon); + padding-left: 7px; + flex: 0 0 auto; + } + + .additional-tabs, + .emoji-tabs { + display: block; + min-width: 0; + flex-basis: auto; + flex-shrink: 1; + + &-item { + padding: 0 7px; + cursor: pointer; + font-size: 1.85em; + + &.disabled { + opacity: 0.5; + pointer-events: none; + } + + &.active { + border-bottom: 4px solid; + + svg { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + } + } + + .sticker-picker { + flex: 1 1 auto + } + + .stickers, + .emoji { + &-content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + + &.hidden { + opacity: 0; + pointer-events: none; + position: absolute; + } + } + } + + .emoji { + &-search { + padding: 5px; + flex: 0 0 auto; + + input { + width: 100%; + } + } + + &-groups { + flex: 1 1 1px; + position: relative; + overflow: auto; + user-select: none; + mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, + linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, + linear-gradient(to top, white, white); + transition: mask-size 150ms; + mask-size: 100% 20px, 100% 20px, auto; + // Autoprefixed seem to ignore this one, and also syntax is different + -webkit-mask-composite: xor; + mask-composite: exclude; + &.scrolled { + &-top { + mask-size: 100% 20px, 100% 0, auto; + } + &-bottom { + mask-size: 100% 0, 100% 20px, auto; + } + } + } + + &-group { + display: flex; + align-items: center; + flex-wrap: wrap; + padding-left: 5px; + justify-content: left; + + &-title { + font-size: 0.85em; + width: 100%; + margin: 0; + + &.disabled { + display: none; + } + } + } + + &-item { + width: 32px; + height: 32px; + box-sizing: border-box; + display: flex; + font-size: 32px; + align-items: center; + justify-content: center; + margin: 4px; + + cursor: pointer; + + img { + object-fit: contain; + max-width: 100%; + max-height: 100%; + } + } + + } + +} diff --git a/src/components/emoji_reaction_picker/emoji_reaction_picker.vue b/src/components/emoji_reaction_picker/emoji_reaction_picker.vue new file mode 100644 index 00000000..a24eeb7e --- /dev/null +++ b/src/components/emoji_reaction_picker/emoji_reaction_picker.vue @@ -0,0 +1,55 @@ + + + + diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 7531fbc8..9f415e11 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -1,4 +1,5 @@ import Popover from '../popover/popover.vue' +import EmojiReactionPicker from '../emoji_reaction_picker/emoji_reaction_picker.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' @@ -12,10 +13,12 @@ const ReactButton = { } }, components: { - Popover + Popover, + EmojiReactionPicker }, methods: { - addReaction (event, emoji, close) { + addReaction (event, close) { + const emoji = event.insertion const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji) if (existingReaction && existingReaction.me) { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) @@ -32,40 +35,6 @@ const ReactButton = { } }, computed: { - commonEmojis () { - return [ - { displayText: 'thumbsup', replacement: '👍' }, - { displayText: 'angry', replacement: '😠' }, - { displayText: 'eyes', replacement: '👀' }, - { displayText: 'joy', replacement: '😂' }, - { displayText: 'fire', replacement: '🔥' } - ] - }, - emojis () { - if (this.filterWord !== '') { - const filterWordLowercase = this.filterWord.toLowerCase() - let orderedEmojiList = [] - for (const emoji of [ - ...this.$store.state.instance.emoji, - ...this.$store.state.instance.customEmoji - ]) { - if (emoji.replacement === this.filterWord) return [emoji] - - const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) - if (indexOfFilterWord > -1) { - if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { - orderedEmojiList[indexOfFilterWord] = [] - } - orderedEmojiList[indexOfFilterWord].push(emoji) - } - } - return orderedEmojiList.flat() - } - return [ - ...this.$store.state.instance.emoji, - ...this.$store.state.instance.customEmoji - ] || [] - }, mergedConfig () { return this.$store.getters.mergedConfig } diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index e5103fb9..afb9df4e 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -9,43 +9,9 @@ @show="focusInput" >