Merge branch 'emoji-optimizations' into shigusegubu

* emoji-optimizations:
  eslint
  fixed emoji picker showing up beyond viewport
  start loading emoji when picker is open
  remove the "textbox grows the 'wrong' way" behavior, replace it with more conditions to scroll to bottom
  arbitrary limit with option to overcome it
  emoji picker gradual render
  moved emoji stuff away from after-store and into users module since we only need emoji after login
This commit is contained in:
Henry Jameson 2019-10-08 21:41:45 +03:00
commit d6a7f46480
13 changed files with 211 additions and 73 deletions

View file

@ -658,6 +658,18 @@ nav {
color: var(--alertErrorPanelText, $fallback--text); 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 { .faint {

View file

@ -17,6 +17,7 @@ $fallback--cGreen: #0fa00f;
$fallback--cOrange: orange; $fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5); $fallback--alertError: rgba(211,16,20,.5);
$fallback--alertWarning: rgba(111,111,20,.5);
$fallback--panelRadius: 10px; $fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px; $fallback--checkboxRadius: 2px;

View file

@ -173,56 +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/v1/custom_emojis')
if (res.ok) {
const result = await res.json()
const emoji = result.map(({ url, shortcode, tags, category }) => {
return {
displayText: shortcode,
imageUrl: url,
tags,
category,
replacement: `:${shortcode}: `
}
})
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 getAppSecret = async ({ store }) => {
const { state, commit } = store const { state, commit } = store
const { oauth, instance } = state const { oauth, instance } = state
@ -257,6 +207,7 @@ const getNodeInfo = async ({ store }) => {
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
@ -313,8 +264,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
getTOS({ store }), getTOS({ store }),
getInstancePanel({ store }), getInstancePanel({ store }),
getStickers({ store }), getStickers({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store }) getNodeInfo({ store })
]) ])

View file

@ -165,6 +165,7 @@ const EmojiInput = {
methods: { methods: {
triggerShowPicker () { triggerShowPicker () {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
}) })
@ -181,6 +182,7 @@ const EmojiInput = {
this.showPicker = !this.showPicker this.showPicker = !this.showPicker
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
} }
}, },
replace (replacement) { replace (replacement) {
@ -306,6 +308,17 @@ const EmojiInput = {
} else { } else {
scrollerRef.scrollTop = targetScroll scrollerRef.scrollTop = targetScroll
} }
this.$nextTick(() => {
const { offsetHeight } = this.input.elm
const { picker } = this.$refs
console.log(picker)
const pickerBottom = picker.$el.getBoundingClientRect().bottom
if (pickerBottom > window.innerHeight) {
picker.$el.style.top = 'auto'
picker.$el.style.bottom = offsetHeight + 'px'
}
})
}, },
onTransition (e) { onTransition (e) {
this.resize() this.resize()
@ -419,11 +432,14 @@ const EmojiInput = {
this.caret = selectionStart this.caret = selectionStart
}, },
resize () { resize () {
const { panel } = this.$refs const { panel, picker } = this.$refs
if (!panel) return if (!panel) return
const { offsetHeight, offsetTop } = this.input.elm const { offsetHeight, offsetTop } = this.input.elm
this.$refs.panel.style.top = (offsetTop + offsetHeight) + 'px' const offsetBottom = offsetTop + offsetHeight
this.$refs.picker.$el.style.top = (offsetTop + offsetHeight) + 'px'
panel.style.top = offsetBottom + 'px'
picker.$el.style.top = offsetBottom + 'px'
picker.$el.style.bottom = 'auto'
} }
} }
} }

View file

@ -1,4 +1,9 @@
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import { set } from 'vue'
const LOAD_EMOJI_BY = 50
const LOAD_EMOJI_INTERVAL = 100
const LOAD_EMOJI_SANE_AMOUNT = 500
const filterByKeyword = (list, keyword = '') => { const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword)) return list.filter(x => x.displayText.includes(keyword))
@ -18,7 +23,11 @@ const EmojiPicker = {
activeGroup: 'custom', activeGroup: 'custom',
showingStickers: false, showingStickers: false,
groupsScrolledClass: 'scrolled-top', groupsScrolledClass: 'scrolled-top',
keepOpen: false keepOpen: false,
customEmojiBuffer: [],
customEmojiTimeout: null,
customEmojiCounter: 0,
customEmojiLoadAllConfirmed: false
} }
}, },
components: { components: {
@ -58,6 +67,40 @@ const EmojiPicker = {
}) })
}) })
}, },
loadEmojiInsane () {
this.customEmojiLoadAllConfirmed = true
this.continueEmojiLoad()
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
const saneLoaded = this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
if (allLoaded || saneLoaded) {
return
}
this.customEmojiBuffer.push(
...this.filteredEmoji.slice(
this.customEmojiCounter,
this.customEmojiCounter + LOAD_EMOJI_BY
)
)
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
this.customEmojiCounter += LOAD_EMOJI_BY
},
startEmojiLoad () {
if (this.customEmojiTimeout) {
window.clearTimeout(this.customEmojiTimeout)
}
set(this, 'customEmojiBuffer', [])
this.customEmojiCounter = 0
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
},
continueEmojiLoad () {
this.customEmojiTimeout = window.setTimeout(this.loadEmoji, LOAD_EMOJI_INTERVAL)
},
toggleStickers () { toggleStickers () {
this.showingStickers = !this.showingStickers this.showingStickers = !this.showingStickers
}, },
@ -74,6 +117,7 @@ const EmojiPicker = {
watch: { watch: {
keyword () { keyword () {
this.scrolledGroup() this.scrolledGroup()
this.startEmojiLoad()
} }
}, },
computed: { computed: {
@ -86,15 +130,30 @@ const EmojiPicker = {
} }
return 0 return 0
}, },
saneAmount () {
// for UI
return LOAD_EMOJI_SANE_AMOUNT
},
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
this.keyword
)
},
askForSanity () {
return this.customEmojiBuffer.length >= LOAD_EMOJI_SANE_AMOUNT &&
!this.customEmojiLoadAllConfirmed
},
emojis () { emojis () {
const standardEmojis = this.$store.state.instance.emoji || [] const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.$store.state.instance.customEmoji || [] const customEmojis = this.customEmojiBuffer
return [ return [
{ {
id: 'custom', id: 'custom',
text: this.$t('emoji.custom'), text: this.$t('emoji.custom'),
icon: 'icon-smile', icon: 'icon-smile',
emojis: filterByKeyword(customEmojis, this.keyword) emojis: customEmojis
}, },
{ {
id: 'standard', id: 'standard',

View file

@ -6,15 +6,25 @@
position: absolute; position: absolute;
right: 0; right: 0;
left: 0; left: 0;
height: 320px;
margin: 0 !important; margin: 0 !important;
z-index: 1; z-index: 1;
.keep-open { .keep-open,
.too-many-emoji {
padding: 7px; padding: 7px;
line-height: normal; line-height: normal;
} }
.too-many-emoji {
display: flex;
flex-direction: column;
}
.keep-open-label {
padding: 0 7px;
display: flex;
}
.heading { .heading {
display: flex; display: flex;
height: 32px; height: 32px;
@ -24,7 +34,7 @@
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 0; flex: 1 1 auto;
min-height: 0px; min-height: 0px;
} }
@ -32,12 +42,16 @@
flex-grow: 1; flex-grow: 1;
} }
.emoji-groups {
min-height: 200px;
}
.additional-tabs { .additional-tabs {
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);
padding-left: 7px; padding-left: 7px;
flex: 0 0 0; flex: 0 0 auto;
} }
.additional-tabs, .additional-tabs,
@ -68,7 +82,7 @@
} }
.sticker-picker { .sticker-picker {
flex: 1 1 0 flex: 1 1 auto
} }
.stickers, .stickers,
@ -76,7 +90,7 @@
&-content { &-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 0; flex: 1 1 auto;
min-height: 0; min-height: 0;
&.hidden { &.hidden {
@ -90,7 +104,7 @@
.emoji { .emoji {
&-search { &-search {
padding: 5px; padding: 5px;
flex: 0 0 0; flex: 0 0 auto;
input { input {
width: 100%; width: 100%;

View file

@ -80,6 +80,20 @@
{{ $t('emoji.keep_open') }} {{ $t('emoji.keep_open') }}
</Checkbox> </Checkbox>
</div> </div>
<div
v-if="askForSanity"
class="too-many-emoji"
>
<div class="alert warning hint">
{{ $t('emoji.load_all_hint', { saneAmount } ) }}
</div>
<button
class="btn btn-default"
@click="loadEmojiInsane"
>
{{ $t('emoji.load_all', { emojiAmount: filteredEmoji.length } ) }}
</button>
</div>
</div> </div>
<div <div
v-if="showingStickers" v-if="showingStickers"

View file

@ -292,9 +292,6 @@ const PostStatusForm = {
const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2)) const bottomPadding = Number(bottomPaddingStr.substring(0, bottomPaddingStr.length - 2))
const vertPadding = topPadding + bottomPadding const vertPadding = topPadding + bottomPadding
const oldHeightStr = target.style.height || ''
const oldHeight = Number(oldHeightStr.substring(0, oldHeightStr.length - 2))
/* Explanation: /* Explanation:
* *
* https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
@ -331,12 +328,17 @@ const PostStatusForm = {
// to find offset relative to scrollable container (scroller) // to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const textareaSizeChangeDelta = newHeight - oldHeight || 0
const isBottomObstructed = scrollerBottomBorder < rootBottomBorder const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
const isRootBiggerThanScroller = scrollerHeight < rootRef.offsetHeight
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
const totalDelta = textareaSizeChangeDelta + // The intention is basically this;
(isBottomObstructed ? rootChangeDelta : 0) // Keep bottom side always visible so that submit button is in view EXCEPT
// if root 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 &&
!(isRootBiggerThanScroller &&
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
const totalDelta = shouldScrollToBottom ? rootChangeDelta : 0
const targetScroll = currentScroll + totalDelta const targetScroll = currentScroll + totalDelta
if (scrollerRef === window) { if (scrollerRef === window) {

View file

@ -74,6 +74,7 @@ export default {
topBarLinkColorLocal: undefined, topBarLinkColorLocal: undefined,
alertErrorColorLocal: undefined, alertErrorColorLocal: undefined,
alertWarningColorLocal: undefined,
badgeOpacityLocal: undefined, badgeOpacityLocal: undefined,
badgeNotificationColorLocal: undefined, badgeNotificationColorLocal: undefined,
@ -147,6 +148,7 @@ export default {
btnText: this.btnTextColorLocal, btnText: this.btnTextColorLocal,
alertError: this.alertErrorColorLocal, alertError: this.alertErrorColorLocal,
alertWarning: this.alertWarningColorLocal,
badgeNotification: this.badgeNotificationColorLocal, badgeNotification: this.badgeNotificationColorLocal,
faint: this.faintColorLocal, faint: this.faintColorLocal,
@ -230,6 +232,7 @@ export default {
topBar: hex2rgb(colors.topBar), topBar: hex2rgb(colors.topBar),
input: hex2rgb(colors.input), input: hex2rgb(colors.input),
alertError: hex2rgb(colors.alertError), alertError: hex2rgb(colors.alertError),
alertWarning: hex2rgb(colors.alertWarning),
badgeNotification: hex2rgb(colors.badgeNotification) badgeNotification: hex2rgb(colors.badgeNotification)
} }

View file

@ -201,6 +201,13 @@
:fallback="previewTheme.colors.alertError" :fallback="previewTheme.colors.alertError"
/> />
<ContrastRatio :contrast="previewContrast.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>
<div class="color-item"> <div class="color-item">
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4> <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>

View file

@ -114,7 +114,9 @@
"search_emoji": "Search for an emoji", "search_emoji": "Search for an emoji",
"add_emoji": "Insert emoji", "add_emoji": "Insert emoji",
"custom": "Custom 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": { "interactions": {
"favs_repeats": "Repeats and Favorites", "favs_repeats": "Repeats and Favorites",
@ -387,6 +389,7 @@
"_tab_label": "Advanced", "_tab_label": "Advanced",
"alert": "Alert background", "alert": "Alert background",
"alert_error": "Error", "alert_error": "Error",
"alert_warning": "Warning",
"badge": "Badge background", "badge": "Badge background",
"badge_notification": "Notification", "badge_notification": "Notification",
"panel_header": "Panel header", "panel_header": "Panel header",

View file

@ -60,6 +60,56 @@ const unmuteUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
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.rootState.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 })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load custom emojis")
console.warn(e)
}
}
export const mutations = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -434,6 +484,9 @@ const users = {
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
getCustomEmoji(store)
getStaticEmoji(store)
getNotificationPermission() getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission)) .then(permission => commit('setNotificationPermission', permission))

View file

@ -215,6 +215,10 @@ const generateColors = (input) => {
colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text) 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.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.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
@ -222,6 +226,7 @@ const generateColors = (input) => {
if (typeof v === 'undefined') return if (typeof v === 'undefined') return
if (k === 'alert') { if (k === 'alert') {
colors.alertError.a = v colors.alertError.a = v
colors.alertWarning.a = v
return return
} }
if (k === 'faint') { if (k === 'faint') {