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:
commit
2b68134ab0
12
src/App.scss
12
src/App.scss
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 })
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
Loading…
Reference in New Issue