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
This commit is contained in:
HJ 2019-11-08 22:01:42 +00:00
commit 2b68134ab0
16 changed files with 250 additions and 94 deletions

View file

@ -661,6 +661,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,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 getAppSecret = async ({ store }) => {
const { state, commit } = store const { state, commit } = store
const { oauth, instance } = state const { oauth, instance } = state
@ -259,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 })
@ -315,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,16 @@ const EmojiInput = {
} else { } else {
scrollerRef.scrollTop = targetScroll 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) { onTransition (e) {
this.resize() this.resize()
@ -419,11 +431,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

@ -2,6 +2,7 @@
<div <div
v-click-outside="onClickOutside" v-click-outside="onClickOutside"
class="emoji-input" class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
> >
<slot /> <slot />
<template v-if="enableEmojiPicker"> <template v-if="enableEmojiPicker">
@ -63,6 +64,10 @@
flex-direction: column; flex-direction: column;
position: relative; position: relative;
&.with-picker input {
padding-right: 30px;
}
.emoji-picker-icon { .emoji-picker-icon {
position: absolute; position: absolute;
top: 0; top: 0;

View file

@ -1,5 +1,12 @@
import Checkbox from '../checkbox/checkbox.vue' 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 = '') => { const filterByKeyword = (list, keyword = '') => {
return list.filter(x => x.displayText.includes(keyword)) return list.filter(x => x.displayText.includes(keyword))
} }
@ -18,7 +25,10 @@ const EmojiPicker = {
activeGroup: 'custom', activeGroup: 'custom',
showingStickers: false, showingStickers: false,
groupsScrolledClass: 'scrolled-top', groupsScrolledClass: 'scrolled-top',
keepOpen: false keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
} }
}, },
components: { components: {
@ -26,10 +36,22 @@ const EmojiPicker = {
Checkbox Checkbox
}, },
methods: { methods: {
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) { onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen }) 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) { highlight (key) {
const ref = this.$refs['group-' + key] const ref = this.$refs['group-' + key]
const top = ref[0].offsetTop const top = ref[0].offsetTop
@ -39,9 +61,7 @@ const EmojiPicker = {
this.$refs['emoji-groups'].scrollTop = top + 1 this.$refs['emoji-groups'].scrollTop = top + 1
}) })
}, },
scrolledGroup (e) { updateScrolledClass (target) {
const target = (e && e.target) || this.$refs['emoji-groups']
const top = target.scrollTop + 5
if (target.scrollTop <= 5) { if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top' this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) { } else if (target.scrollTop >= target.scrollTopMax - 5) {
@ -49,6 +69,28 @@ const EmojiPicker = {
} else { } else {
this.groupsScrolledClass = 'scrolled-middle' 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.$nextTick(() => {
this.emojisView.forEach(group => { this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id] 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 () { toggleStickers () {
this.showingStickers = !this.showingStickers this.showingStickers = !this.showingStickers
}, },
setShowStickers (value) { setShowStickers (value) {
this.showingStickers = value this.showingStickers = value
},
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
this.$emit('sticker-upload-failed', e)
} }
}, },
watch: { watch: {
keyword () { keyword () {
this.scrolledGroup() this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
} }
}, },
computed: { computed: {
@ -86,15 +147,25 @@ const EmojiPicker = {
} }
return 0 return 0
}, },
filteredEmoji () {
return filterByKeyword(
this.$store.state.instance.customEmoji || [],
this.keyword
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
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

@ -47,7 +47,7 @@
ref="emoji-groups" ref="emoji-groups"
class="emoji-groups" class="emoji-groups"
:class="groupsScrolledClass" :class="groupsScrolledClass"
@scroll="scrolledGroup" @scroll="onScroll"
> >
<div <div
v-for="group in emojisView" v-for="group in emojisView"
@ -73,6 +73,7 @@
:src="emoji.imageUrl" :src="emoji.imageUrl"
> >
</span> </span>
<span :ref="'group-end-' + group.id" />
</div> </div>
</div> </div>
<div class="keep-open"> <div class="keep-open">

View file

@ -276,11 +276,15 @@ const PostStatusForm = {
return 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, /* Scroller is either `window` (replies in TL), sidebar (main post form,
* replies in notifs) or mobile post form. Note that getting and setting * replies in notifs) or mobile post form. Note that getting and setting
* scroll is different for `Window` and `Element`s * 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') || const scrollerRef = this.$el.closest('.sidebar-scroller') ||
this.$el.closest('.post-form-modal-view') || this.$el.closest('.post-form-modal-view') ||
window window
@ -292,9 +296,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
@ -306,7 +307,7 @@ const PostStatusForm = {
* SHRINK the textarea when there's extra space. To workaround that we set * SHRINK the textarea when there's extra space. To workaround that we set
* height to 'auto' which makes textarea tiny again, so that scrollHeight * height to 'auto' which makes textarea tiny again, so that scrollHeight
* will match text height again. HOWEVER, shrinking textarea can screw with * 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, * 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 * so we check current scroll position before shrinking and then restore it
* with needed delta. * with needed delta.
@ -327,16 +328,21 @@ const PostStatusForm = {
target.style.height = `${newHeight}px` target.style.height = `${newHeight}px`
// END content size update // 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) // to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top const bottomBottomBorder = bottomRef.offsetHeight + findOffset(bottomRef, scrollerRef).top + bottomBottomPadding
const textareaSizeChangeDelta = newHeight - oldHeight || 0
const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
const totalDelta = textareaSizeChangeDelta +
(isBottomObstructed ? rootChangeDelta : 0)
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 const targetScroll = currentScroll + totalDelta
if (scrollerRef === window) { if (scrollerRef === window) {

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
ref="root" ref="form"
class="post-status-form" class="post-status-form"
> >
<form <form
@ -160,7 +160,10 @@
:visible="pollFormVisible" :visible="pollFormVisible"
@update-poll="setPoll" @update-poll="setPoll"
/> />
<div class="form-bottom"> <div
ref="bottom"
class="form-bottom"
>
<div class="form-bottom-left"> <div class="form-bottom-left">
<media-upload <media-upload
ref="mediaUpload" ref="mediaUpload"

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",
@ -391,6 +393,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

@ -36,7 +36,9 @@ const defaultState = {
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,
emoji: [], emoji: [],
emojiFetched: false,
customEmoji: [], customEmoji: [],
customEmojiFetched: false,
restrictedNicknames: [], restrictedNicknames: [],
postFormats: [], postFormats: [],
@ -94,9 +96,68 @@ const instance = {
break 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) { setTheme ({ commit }, themeName) {
commit('setInstanceOption', { name: 'theme', value: themeName }) commit('setInstanceOption', { name: 'theme', value: themeName })
return setPreset(themeName, commit) return setPreset(themeName, commit)
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true
dispatch('getCustomEmoji')
}
if (!state.emojiFetched) {
state.emojiFetched = true
dispatch('getStaticEmoji')
}
} }
} }
} }

View file

@ -453,6 +453,8 @@ const users = {
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
store.dispatch('fetchEmoji')
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') {