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:
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') {