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);
}
}
&.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 {

View file

@ -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;

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 { state, commit } = store
const { oauth, instance } = state
@ -257,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 })
@ -313,8 +264,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
getTOS({ store }),
getInstancePanel({ store }),
getStickers({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store })
])

View file

@ -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,17 @@ const EmojiInput = {
} else {
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) {
this.resize()
@ -419,11 +432,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'
}
}
}

View file

@ -1,4 +1,9 @@
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 = '') => {
return list.filter(x => x.displayText.includes(keyword))
@ -18,7 +23,11 @@ const EmojiPicker = {
activeGroup: 'custom',
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false
keepOpen: false,
customEmojiBuffer: [],
customEmojiTimeout: null,
customEmojiCounter: 0,
customEmojiLoadAllConfirmed: false
}
},
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 () {
this.showingStickers = !this.showingStickers
},
@ -74,6 +117,7 @@ const EmojiPicker = {
watch: {
keyword () {
this.scrolledGroup()
this.startEmojiLoad()
}
},
computed: {
@ -86,15 +130,30 @@ const EmojiPicker = {
}
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 () {
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',

View file

@ -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%;

View file

@ -80,6 +80,20 @@
{{ $t('emoji.keep_open') }}
</Checkbox>
</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
v-if="showingStickers"

View file

@ -292,9 +292,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
@ -331,12 +328,17 @@ const PostStatusForm = {
// 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 isRootBiggerThanScroller = scrollerHeight < rootRef.offsetHeight
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
const totalDelta = textareaSizeChangeDelta +
(isBottomObstructed ? rootChangeDelta : 0)
// The intention is basically this;
// 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
if (scrollerRef === window) {

View file

@ -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)
}

View file

@ -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>

View file

@ -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",
@ -387,6 +389,7 @@
"_tab_label": "Advanced",
"alert": "Alert background",
"alert_error": "Error",
"alert_warning": "Warning",
"badge": "Badge background",
"badge_notification": "Notification",
"panel_header": "Panel header",

View file

@ -60,6 +60,56 @@ const unmuteUser = (store, id) => {
.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 = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@ -434,6 +484,9 @@ const users = {
commit('setCurrentUser', user)
commit('addNewUsers', [user])
getCustomEmoji(store)
getStaticEmoji(store)
getNotificationPermission()
.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.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') {