logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: 2b68134ab01266913b89b79ea6c3e9575278ecb2
parent: 5679dcdd18750a1fc9ac1d4eeea3fd3b642a2151
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Fri,  8 Nov 2019 22:01:42 +0000

Merge branch 'emoji-optimizations' into 'develop'

Emoji fixes, optimizations and improvements

Closes #690, #686, #682, #674, and #678

See merge request pleroma/pleroma-fe!969

Diffstat:

Msrc/App.scss12++++++++++++
Msrc/_variables.scss1+
Msrc/boot/after_store.js55+------------------------------------------------------
Msrc/components/emoji_input/emoji_input.js21++++++++++++++++++---
Msrc/components/emoji_input/emoji_input.vue5+++++
Msrc/components/emoji_picker/emoji_picker.js97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/components/emoji_picker/emoji_picker.scss28+++++++++++++++++++++-------
Msrc/components/emoji_picker/emoji_picker.vue3++-
Msrc/components/post_status_form/post_status_form.js32+++++++++++++++++++-------------
Msrc/components/post_status_form/post_status_form.vue7+++++--
Msrc/components/style_switcher/style_switcher.js3+++
Msrc/components/style_switcher/style_switcher.vue7+++++++
Msrc/i18n/en.json5++++-
Msrc/modules/instance.js61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/modules/users.js2++
Msrc/services/style_setter/style_setter.js5+++++
16 files changed, 250 insertions(+), 94 deletions(-)

diff --git a/src/App.scss b/src/App.scss @@ -661,6 +661,18 @@ nav { color: var(--alertErrorPanelText, $fallback--text); } } + + &.warning { + background-color: $fallback--alertWarning; + background-color: var(--alertWarning, $fallback--alertWarning); + color: $fallback--text; + color: var(--alertWarningText, $fallback--text); + + .panel-heading & { + color: $fallback--text; + color: var(--alertWarningPanelText, $fallback--text); + } + } } .faint { diff --git a/src/_variables.scss b/src/_variables.scss @@ -17,6 +17,7 @@ $fallback--cGreen: #0fa00f; $fallback--cOrange: orange; $fallback--alertError: rgba(211,16,20,.5); +$fallback--alertWarning: rgba(111,111,20,.5); $fallback--panelRadius: 10px; $fallback--checkboxRadius: 2px; diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -173,58 +173,6 @@ const getStickers = async ({ store }) => { } } -const getStaticEmoji = async ({ store }) => { - try { - const res = await window.fetch('/static/emoji.json') - if (res.ok) { - const values = await res.json() - const emoji = Object.keys(values).map((key) => { - return { - displayText: key, - imageUrl: false, - replacement: values[key] - } - }).sort((a, b) => a.displayText - b.displayText) - store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) - } else { - throw (res) - } - } catch (e) { - console.warn("Can't load static emoji") - console.warn(e) - } -} - -// This is also used to indicate if we have a 'pleroma backend' or not. -// Somewhat weird, should probably be somewhere else. -const getCustomEmoji = async ({ store }) => { - try { - const res = await window.fetch('/api/pleroma/emoji.json') - if (res.ok) { - const result = await res.json() - const values = Array.isArray(result) ? Object.assign({}, ...result) : result - const emoji = Object.entries(values).map(([key, value]) => { - const imageUrl = value.image_url - return { - displayText: key, - imageUrl: imageUrl ? store.state.instance.server + imageUrl : value, - tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'], - replacement: `:${key}: ` - } - // Technically could use tags but those are kinda useless right now, should have been "pack" field, that would be more useful - }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : 0) - store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) - } else { - throw (res) - } - } catch (e) { - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) - console.warn("Can't load custom emojis, maybe not a Pleroma instance?") - console.warn(e) - } -} - const getAppSecret = async ({ store }) => { const { state, commit } = store const { oauth, instance } = state @@ -259,6 +207,7 @@ const getNodeInfo = async ({ store }) => { const software = data.software store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' }) const frontendVersion = window.___pleromafe_commit_hash store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) @@ -315,8 +264,6 @@ const afterStoreSetup = async ({ store, i18n }) => { getTOS({ store }), getInstancePanel({ store }), getStickers({ store }), - getStaticEmoji({ store }), - getCustomEmoji({ store }), getNodeInfo({ store }) ]) diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -165,6 +165,7 @@ const EmojiInput = { methods: { triggerShowPicker () { this.showPicker = true + this.$refs.picker.startEmojiLoad() this.$nextTick(() => { this.scrollIntoView() }) @@ -181,6 +182,7 @@ const EmojiInput = { this.showPicker = !this.showPicker if (this.showPicker) { this.scrollIntoView() + this.$refs.picker.startEmojiLoad() } }, replace (replacement) { @@ -306,6 +308,16 @@ const EmojiInput = { } else { scrollerRef.scrollTop = targetScroll } + + this.$nextTick(() => { + const { offsetHeight } = this.input.elm + const { picker } = this.$refs + const pickerBottom = picker.$el.getBoundingClientRect().bottom + if (pickerBottom > window.innerHeight) { + picker.$el.style.top = 'auto' + picker.$el.style.bottom = offsetHeight + 'px' + } + }) }, onTransition (e) { this.resize() @@ -419,11 +431,14 @@ const EmojiInput = { this.caret = selectionStart }, resize () { - const { panel } = this.$refs + const { panel, picker } = this.$refs if (!panel) return const { offsetHeight, offsetTop } = this.input.elm - this.$refs.panel.style.top = (offsetTop + offsetHeight) + 'px' - this.$refs.picker.$el.style.top = (offsetTop + offsetHeight) + 'px' + const offsetBottom = offsetTop + offsetHeight + + panel.style.top = offsetBottom + 'px' + picker.$el.style.top = offsetBottom + 'px' + picker.$el.style.bottom = 'auto' } } } diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -2,6 +2,7 @@ <div v-click-outside="onClickOutside" class="emoji-input" + :class="{ 'with-picker': !hideEmojiButton }" > <slot /> <template v-if="enableEmojiPicker"> @@ -63,6 +64,10 @@ flex-direction: column; position: relative; + &.with-picker input { + padding-right: 30px; + } + .emoji-picker-icon { position: absolute; top: 0; diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js @@ -1,5 +1,12 @@ import Checkbox from '../checkbox/checkbox.vue' +// 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 = '') => { return list.filter(x => x.displayText.includes(keyword)) } @@ -18,7 +25,10 @@ const EmojiPicker = { activeGroup: 'custom', showingStickers: false, groupsScrolledClass: 'scrolled-top', - keepOpen: false + keepOpen: false, + customEmojiBufferSlice: LOAD_EMOJI_BY, + customEmojiTimeout: null, + customEmojiLoadAllConfirmed: false } }, components: { @@ -26,10 +36,22 @@ const EmojiPicker = { 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[0].offsetTop @@ -39,9 +61,7 @@ const EmojiPicker = { this.$refs['emoji-groups'].scrollTop = top + 1 }) }, - scrolledGroup (e) { - const target = (e && e.target) || this.$refs['emoji-groups'] - const top = target.scrollTop + 5 + updateScrolledClass (target) { if (target.scrollTop <= 5) { this.groupsScrolledClass = 'scrolled-top' } else if (target.scrollTop >= target.scrollTopMax - 5) { @@ -49,6 +69,28 @@ const EmojiPicker = { } else { this.groupsScrolledClass = 'scrolled-middle' } + }, + triggerLoadMore (target) { + const ref = this.$refs['group-end-custom'][0] + 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] @@ -58,22 +100,41 @@ const EmojiPicker = { }) }) }, + 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 - }, - onStickerUploaded (e) { - this.$emit('sticker-uploaded', e) - }, - onStickerUploadFailed (e) { - this.$emit('sticker-upload-failed', e) } }, watch: { keyword () { - this.scrolledGroup() + this.customEmojiLoadAllConfirmed = false + this.onScroll() + this.startEmojiLoad(true) } }, computed: { @@ -86,15 +147,25 @@ const EmojiPicker = { } 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.$store.state.instance.customEmoji || [] + const customEmojis = this.customEmojiBuffer + return [ { id: 'custom', text: this.$t('emoji.custom'), icon: 'icon-smile', - emojis: filterByKeyword(customEmojis, this.keyword) + emojis: customEmojis }, { id: 'standard', diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss @@ -6,15 +6,25 @@ position: absolute; right: 0; left: 0; - height: 320px; margin: 0 !important; z-index: 1; - .keep-open { + .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; @@ -24,7 +34,7 @@ .content { display: flex; flex-direction: column; - flex: 1 1 0; + flex: 1 1 auto; min-height: 0px; } @@ -32,12 +42,16 @@ 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 0; + flex: 0 0 auto; } .additional-tabs, @@ -68,7 +82,7 @@ } .sticker-picker { - flex: 1 1 0 + flex: 1 1 auto } .stickers, @@ -76,7 +90,7 @@ &-content { display: flex; flex-direction: column; - flex: 1 1 0; + flex: 1 1 auto; min-height: 0; &.hidden { @@ -90,7 +104,7 @@ .emoji { &-search { padding: 5px; - flex: 0 0 0; + flex: 0 0 auto; input { width: 100%; diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue @@ -47,7 +47,7 @@ ref="emoji-groups" class="emoji-groups" :class="groupsScrolledClass" - @scroll="scrolledGroup" + @scroll="onScroll" > <div v-for="group in emojisView" @@ -73,6 +73,7 @@ :src="emoji.imageUrl" > </span> + <span :ref="'group-end-' + group.id" /> </div> </div> <div class="keep-open"> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -276,11 +276,15 @@ const PostStatusForm = { return } - const rootRef = this.$refs['root'] + const formRef = this.$refs['form'] + const bottomRef = this.$refs['bottom'] /* Scroller is either `window` (replies in TL), sidebar (main post form, * replies in notifs) or mobile post form. Note that getting and setting * scroll is different for `Window` and `Element`s */ + const bottomBottomPaddingStr = window.getComputedStyle(bottomRef)['padding-bottom'] + const bottomBottomPadding = Number(bottomBottomPaddingStr.substring(0, bottomBottomPaddingStr.length - 2)) + const scrollerRef = this.$el.closest('.sidebar-scroller') || this.$el.closest('.post-form-modal-view') || window @@ -292,9 +296,6 @@ const PostStatusForm = { const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2)) const vertPadding = topPadding + bottomPadding - const oldHeightStr = target.style.height || '' - const oldHeight = Number(oldHeightStr.substring(0, oldHeightStr.length - 2)) - /* Explanation: * * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight @@ -306,7 +307,7 @@ const PostStatusForm = { * SHRINK the textarea when there's extra space. To workaround that we set * height to 'auto' which makes textarea tiny again, so that scrollHeight * will match text height again. HOWEVER, shrinking textarea can screw with - * the scroll since there might be not enough padding around root to even + * the scroll since there might be not enough padding around form-bottom to even * warrant a scroll, so it will jump to 0 and refuse to move anywhere, * so we check current scroll position before shrinking and then restore it * with needed delta. @@ -327,16 +328,21 @@ const PostStatusForm = { target.style.height = `${newHeight}px` // END content size update - // We check where the bottom border of root element is, this uses findOffset + // We check where the bottom border of form-bottom element is, this uses findOffset // to find offset relative to scrollable container (scroller) - const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top - - const textareaSizeChangeDelta = newHeight - oldHeight || 0 - const isBottomObstructed = scrollerBottomBorder < rootBottomBorder - const rootChangeDelta = rootBottomBorder - scrollerBottomBorder - const totalDelta = textareaSizeChangeDelta + - (isBottomObstructed ? rootChangeDelta : 0) + const bottomBottomBorder = bottomRef.offsetHeight + findOffset(bottomRef, scrollerRef).top + bottomBottomPadding + const isBottomObstructed = scrollerBottomBorder < bottomBottomBorder + const isFormBiggerThanScroller = scrollerHeight < formRef.offsetHeight + const bottomChangeDelta = bottomBottomBorder - scrollerBottomBorder + // The intention is basically this; + // Keep form-bottom always visible so that submit button is in view EXCEPT + // if form element bigger than scroller and caret isn't at the end, so that + // if you scroll up and edit middle of text you won't get scrolled back to bottom + const shouldScrollToBottom = isBottomObstructed && + !(isFormBiggerThanScroller && + this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length) + const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0 const targetScroll = currentScroll + totalDelta if (scrollerRef === window) { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -1,6 +1,6 @@ <template> <div - ref="root" + ref="form" class="post-status-form" > <form @@ -160,7 +160,10 @@ :visible="pollFormVisible" @update-poll="setPoll" /> - <div class="form-bottom"> + <div + ref="bottom" + class="form-bottom" + > <div class="form-bottom-left"> <media-upload ref="mediaUpload" diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js @@ -74,6 +74,7 @@ export default { topBarLinkColorLocal: undefined, alertErrorColorLocal: undefined, + alertWarningColorLocal: undefined, badgeOpacityLocal: undefined, badgeNotificationColorLocal: undefined, @@ -147,6 +148,7 @@ export default { btnText: this.btnTextColorLocal, alertError: this.alertErrorColorLocal, + alertWarning: this.alertWarningColorLocal, badgeNotification: this.badgeNotificationColorLocal, faint: this.faintColorLocal, @@ -230,6 +232,7 @@ export default { topBar: hex2rgb(colors.topBar), input: hex2rgb(colors.input), alertError: hex2rgb(colors.alertError), + alertWarning: hex2rgb(colors.alertWarning), badgeNotification: hex2rgb(colors.badgeNotification) } diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue @@ -201,6 +201,13 @@ :fallback="previewTheme.colors.alertError" /> <ContrastRatio :contrast="previewContrast.alertError" /> + <ColorInput + v-model="alertWarningColorLocal" + name="alertWarning" + :label="$t('settings.style.advanced_colors.alert_warning')" + :fallback="previewTheme.colors.alertWarning" + /> + <ContrastRatio :contrast="previewContrast.alertWarning" /> </div> <div class="color-item"> <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -114,7 +114,9 @@ "search_emoji": "Search for an emoji", "add_emoji": "Insert emoji", "custom": "Custom emoji", - "unicode": "Unicode emoji" + "unicode": "Unicode emoji", + "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.", + "load_all": "Loading all {emojiAmount} emoji" }, "interactions": { "favs_repeats": "Repeats and Favorites", @@ -391,6 +393,7 @@ "_tab_label": "Advanced", "alert": "Alert background", "alert_error": "Error", + "alert_warning": "Warning", "badge": "Badge background", "badge_notification": "Notification", "panel_header": "Panel header", diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -36,7 +36,9 @@ const defaultState = { // Nasty stuff pleromaBackend: true, emoji: [], + emojiFetched: false, customEmoji: [], + customEmojiFetched: false, restrictedNicknames: [], postFormats: [], @@ -94,9 +96,68 @@ const instance = { break } }, + async getStaticEmoji ({ commit }) { + try { + const res = await window.fetch('/static/emoji.json') + if (res.ok) { + const values = await res.json() + const emoji = Object.keys(values).map((key) => { + return { + displayText: key, + imageUrl: false, + replacement: values[key] + } + }).sort((a, b) => a.displayText - b.displayText) + commit('setInstanceOption', { name: 'emoji', value: emoji }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load static emoji") + console.warn(e) + } + }, + + async getCustomEmoji ({ commit, state }) { + try { + const res = await window.fetch('/api/pleroma/emoji.json') + if (res.ok) { + const result = await res.json() + const values = Array.isArray(result) ? Object.assign({}, ...result) : result + const emoji = Object.entries(values).map(([key, value]) => { + const imageUrl = value.image_url + return { + displayText: key, + imageUrl: imageUrl ? state.server + imageUrl : value, + tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'], + replacement: `:${key}: ` + } + // Technically could use tags but those are kinda useless right now, + // should have been "pack" field, that would be more useful + }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : 0) + commit('setInstanceOption', { name: 'customEmoji', value: emoji }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load custom emojis") + console.warn(e) + } + }, + setTheme ({ commit }, themeName) { commit('setInstanceOption', { name: 'theme', value: themeName }) return setPreset(themeName, commit) + }, + fetchEmoji ({ dispatch, state }) { + if (!state.customEmojiFetched) { + state.customEmojiFetched = true + dispatch('getCustomEmoji') + } + if (!state.emojiFetched) { + state.emojiFetched = true + dispatch('getStaticEmoji') + } } } } diff --git a/src/modules/users.js b/src/modules/users.js @@ -453,6 +453,8 @@ const users = { commit('setCurrentUser', user) commit('addNewUsers', [user]) + store.dispatch('fetchEmoji') + getNotificationPermission() .then(permission => commit('setNotificationPermission', permission)) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -215,6 +215,10 @@ const generateColors = (input) => { colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text) colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText) + colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange) + colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text) + colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText) + colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed) colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb @@ -222,6 +226,7 @@ const generateColors = (input) => { if (typeof v === 'undefined') return if (k === 'alert') { colors.alertError.a = v + colors.alertWarning.a = v return } if (k === 'faint') {