aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js12
-rw-r--r--src/components/emoji_input/emoji_input.js (renamed from src/components/emoji-input/emoji-input.js)219
-rw-r--r--src/components/emoji_input/emoji_input.vue (renamed from src/components/emoji-input/emoji-input.vue)55
-rw-r--r--src/components/emoji_input/suggestor.js (renamed from src/components/emoji-input/suggestor.js)0
-rw-r--r--src/components/emoji_picker/emoji_picker.js115
-rw-r--r--src/components/emoji_picker/emoji_picker.scss165
-rw-r--r--src/components/emoji_picker/emoji_picker.vue110
-rw-r--r--src/components/media_upload/media_upload.vue16
-rw-r--r--src/components/post_status_form/post_status_form.js117
-rw-r--r--src/components/post_status_form/post_status_form.vue80
-rw-r--r--src/components/settings/settings.js4
-rw-r--r--src/components/settings/settings.vue8
-rw-r--r--src/components/status/status.vue18
-rw-r--r--src/components/sticker_picker/sticker_picker.js4
-rw-r--r--src/components/sticker_picker/sticker_picker.vue72
-rw-r--r--src/components/tab_switcher/tab_switcher.js26
-rw-r--r--src/components/tab_switcher/tab_switcher.scss11
-rw-r--r--src/components/user_card/user_card.js11
-rw-r--r--src/components/user_card/user_card.vue33
-rw-r--r--src/components/user_panel/user_panel.vue2
-rw-r--r--src/components/user_settings/user_settings.js4
-rw-r--r--src/components/user_settings/user_settings.vue2
-rw-r--r--src/i18n/en.json11
-rw-r--r--src/modules/config.js1
-rw-r--r--src/services/offset_finder/offset_finder.service.js31
25 files changed, 972 insertions, 155 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 5cb2acba..490ac4d0 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -184,7 +184,7 @@ const getStaticEmoji = async ({ store }) => {
imageUrl: false,
replacement: values[key]
}
- })
+ }).sort((a, b) => a.displayText - b.displayText)
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
} else {
throw (res)
@@ -203,14 +203,16 @@ const getCustomEmoji = async ({ store }) => {
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
- const emoji = Object.keys(values).map((key) => {
- const imageUrl = values[key].image_url
+ const emoji = Object.entries(values).map(([key, value]) => {
+ const imageUrl = value.image_url
return {
displayText: key,
- imageUrl: imageUrl ? store.state.instance.server + imageUrl : values[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 {
diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji_input/emoji_input.js
index fab64a69..a586b819 100644
--- a/src/components/emoji-input/emoji-input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -1,5 +1,7 @@
import Completion from '../../services/completion/completion.js'
+import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import { take } from 'lodash'
+import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
/**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@@ -52,6 +54,31 @@ const EmojiInput = {
*/
required: true,
type: String
+ },
+ enableEmojiPicker: {
+ /**
+ * Enables emoji picker support, this implies that custom emoji are supported
+ */
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ hideEmojiButton: {
+ /**
+ * intended to use with external picker trigger, i.e. you have a button outside
+ * input that will open up the picker, see triggerShowPicker()
+ */
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ enableStickerPicker: {
+ /**
+ * Enables sticker picker support, only makes sense when enableEmojiPicker=true
+ */
+ required: false,
+ type: Boolean,
+ default: false
}
},
data () {
@@ -60,10 +87,20 @@ const EmojiInput = {
highlighted: 0,
caret: 0,
focused: false,
- blurTimeout: null
+ blurTimeout: null,
+ showPicker: false,
+ temporarilyHideSuggestions: false,
+ keepOpen: false,
+ disableClickOutside: false
}
},
+ components: {
+ EmojiPicker
+ },
computed: {
+ padEmoji () {
+ return this.$store.state.config.padEmoji
+ },
suggestions () {
const firstchar = this.textAtCaret.charAt(0)
if (this.textAtCaret === firstchar) { return [] }
@@ -79,8 +116,12 @@ const EmojiInput = {
highlighted: index === this.highlighted
}))
},
- showPopup () {
- return this.focused && this.suggestions && this.suggestions.length > 0
+ showSuggestions () {
+ return this.focused &&
+ this.suggestions &&
+ this.suggestions.length > 0 &&
+ !this.showPicker &&
+ !this.temporarilyHideSuggestions
},
textAtCaret () {
return (this.wordAtCaret || {}).word || ''
@@ -104,6 +145,7 @@ const EmojiInput = {
input.elm.addEventListener('paste', this.onPaste)
input.elm.addEventListener('keyup', this.onKeyUp)
input.elm.addEventListener('keydown', this.onKeyDown)
+ input.elm.addEventListener('click', this.onClickInput)
input.elm.addEventListener('transitionend', this.onTransition)
input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
},
@@ -115,16 +157,80 @@ const EmojiInput = {
input.elm.removeEventListener('paste', this.onPaste)
input.elm.removeEventListener('keyup', this.onKeyUp)
input.elm.removeEventListener('keydown', this.onKeyDown)
+ input.elm.removeEventListener('click', this.onClickInput)
input.elm.removeEventListener('transitionend', this.onTransition)
input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
}
},
methods: {
+ triggerShowPicker () {
+ this.showPicker = true
+ this.$nextTick(() => {
+ this.scrollIntoView()
+ })
+ // This temporarily disables "click outside" handler
+ // since external trigger also means click originates
+ // from outside, thus preventing picker from opening
+ this.disableClickOutside = true
+ setTimeout(() => {
+ this.disableClickOutside = false
+ }, 0)
+ },
+ togglePicker () {
+ this.input.elm.focus()
+ this.showPicker = !this.showPicker
+ if (this.showPicker) {
+ this.scrollIntoView()
+ }
+ },
replace (replacement) {
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('input', newValue)
this.caret = 0
},
+ insert ({ insertion, keepOpen }) {
+ const before = this.value.substring(0, this.caret) || ''
+ const after = this.value.substring(this.caret) || ''
+
+ /* Using a bit more smart approach to padding emojis with spaces:
+ * - put a space before cursor if there isn't one already, unless we
+ * are at the beginning of post or in spam mode
+ * - put a space after emoji if there isn't one already unless we are
+ * in spam mode
+ *
+ * The idea is that when you put a cursor somewhere in between sentence
+ * inserting just ' :emoji: ' will add more spaces to post which might
+ * break the flow/spacing, as well as the case where user ends sentence
+ * with a space before adding emoji.
+ *
+ * Spam mode is intended for creating multi-part emojis and overall spamming
+ * them, masto seem to be rendering :emoji::emoji: correctly now so why not
+ */
+ const isSpaceRegex = /\s/
+ const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : ''
+ const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : ''
+
+ const newValue = [
+ before,
+ spaceBefore,
+ insertion,
+ spaceAfter,
+ after
+ ].join('')
+ this.keepOpen = keepOpen
+ this.$emit('input', newValue)
+ const position = this.caret + (insertion + spaceAfter + spaceBefore).length
+ if (!keepOpen) {
+ this.input.elm.focus()
+ }
+
+ this.$nextTick(function () {
+ // Re-focus inputbox after clicking suggestion
+ // Set selection right after the replacement instead of the very end
+ this.input.elm.setSelectionRange(position, position)
+ this.caret = position
+ })
+ },
replaceText (e, suggestion) {
const len = this.suggestions.length || 0
if (this.textAtCaret.length === 1) { return }
@@ -148,7 +254,7 @@ const EmojiInput = {
},
cycleBackward (e) {
const len = this.suggestions.length || 0
- if (len > 0) {
+ if (len > 1) {
this.highlighted -= 1
if (this.highlighted < 0) {
this.highlighted = this.suggestions.length - 1
@@ -160,7 +266,7 @@ const EmojiInput = {
},
cycleForward (e) {
const len = this.suggestions.length || 0
- if (len > 0) {
+ if (len > 1) {
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = 0
@@ -170,6 +276,37 @@ const EmojiInput = {
this.highlighted = 0
}
},
+ scrollIntoView () {
+ const rootRef = this.$refs['picker'].$el
+ /* Scroller is either `window` (replies in TL), sidebar (main post form,
+ * replies in notifs) or mobile post form. Note that getting and setting
+ * scroll is different for `Window` and `Element`s
+ */
+ const scrollerRef = this.$el.closest('.sidebar-scroller') ||
+ this.$el.closest('.post-form-modal-view') ||
+ window
+ const currentScroll = scrollerRef === window
+ ? scrollerRef.scrollY
+ : scrollerRef.scrollTop
+ const scrollerHeight = scrollerRef === window
+ ? scrollerRef.innerHeight
+ : scrollerRef.offsetHeight
+
+ const scrollerBottomBorder = currentScroll + scrollerHeight
+ // We check where the bottom border of root element is, this uses findOffset
+ // to find offset relative to scrollable container (scroller)
+ const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
+
+ const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
+ // could also check top delta but there's no case for it
+ const targetScroll = currentScroll + bottomDelta
+
+ if (scrollerRef === window) {
+ scrollerRef.scroll(0, targetScroll)
+ } else {
+ scrollerRef.scrollTop = targetScroll
+ }
+ },
onTransition (e) {
this.resize()
},
@@ -191,50 +328,93 @@ const EmojiInput = {
this.blurTimeout = null
}
+ if (!this.keepOpen) {
+ this.showPicker = false
+ }
this.focused = true
this.setCaret(e)
this.resize()
+ this.temporarilyHideSuggestions = false
},
onKeyUp (e) {
+ const { key } = e
this.setCaret(e)
this.resize()
+
+ // Setting hider in keyUp to prevent suggestions from blinking
+ // when moving away from suggested spot
+ if (key === 'Escape') {
+ this.temporarilyHideSuggestions = true
+ } else {
+ this.temporarilyHideSuggestions = false
+ }
},
onPaste (e) {
this.setCaret(e)
this.resize()
},
onKeyDown (e) {
- this.setCaret(e)
- this.resize()
-
const { ctrlKey, shiftKey, key } = e
- if (key === 'Tab') {
- if (shiftKey) {
+ // Disable suggestions hotkeys if suggestions are hidden
+ if (!this.temporarilyHideSuggestions) {
+ if (key === 'Tab') {
+ if (shiftKey) {
+ this.cycleBackward(e)
+ } else {
+ this.cycleForward(e)
+ }
+ }
+ if (key === 'ArrowUp') {
this.cycleBackward(e)
- } else {
+ } else if (key === 'ArrowDown') {
this.cycleForward(e)
}
+ if (key === 'Enter') {
+ if (!ctrlKey) {
+ this.replaceText(e)
+ }
+ }
}
- if (key === 'ArrowUp') {
- this.cycleBackward(e)
- } else if (key === 'ArrowDown') {
- this.cycleForward(e)
- }
- if (key === 'Enter') {
- if (!ctrlKey) {
- this.replaceText(e)
+ // Probably add optional keyboard controls for emoji picker?
+
+ // Escape hides suggestions, if suggestions are hidden it
+ // de-focuses the element (i.e. default browser behavior)
+ if (key === 'Escape') {
+ if (!this.temporarilyHideSuggestions) {
+ this.input.elm.focus()
}
}
+
+ this.showPicker = false
+ this.resize()
},
onInput (e) {
+ this.showPicker = false
this.setCaret(e)
+ this.resize()
this.$emit('input', e.target.value)
},
onCompositionUpdate (e) {
+ this.showPicker = false
this.setCaret(e)
this.resize()
this.$emit('input', e.target.value)
},
+ onClickInput (e) {
+ this.showPicker = false
+ },
+ onClickOutside (e) {
+ if (this.disableClickOutside) return
+ this.showPicker = false
+ },
+ onStickerUploaded (e) {
+ this.showPicker = false
+ this.$emit('sticker-uploaded', e)
+ },
+ onStickerUploadFailed (e) {
+ this.showPicker = false
+ this.$emit('sticker-upload-Failed', e)
+ },
setCaret ({ target: { selectionStart } }) {
this.caret = selectionStart
},
@@ -243,6 +423,7 @@ const EmojiInput = {
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'
}
}
}
diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji_input/emoji_input.vue
index 48739ec8..13530e8b 100644
--- a/src/components/emoji-input/emoji-input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -1,10 +1,32 @@
<template>
- <div class="emoji-input">
+ <div
+ v-click-outside="onClickOutside"
+ class="emoji-input"
+ >
<slot />
+ <template v-if="enableEmojiPicker">
+ <div
+ v-if="!hideEmojiButton"
+ class="emoji-picker-icon"
+ @click.prevent="togglePicker"
+ >
+ <i class="icon-smile" />
+ </div>
+ <EmojiPicker
+ v-if="enableEmojiPicker"
+ ref="picker"
+ :class="{ hide: !showPicker }"
+ :enable-sticker-picker="enableStickerPicker"
+ class="emoji-picker-panel"
+ @emoji="insert"
+ @sticker-uploaded="onStickerUploaded"
+ @sticker-upload-failed="onStickerUploadFailed"
+ />
+ </template>
<div
ref="panel"
class="autocomplete-panel"
- :class="{ hide: !showPopup }"
+ :class="{ hide: !showSuggestions }"
>
<div class="autocomplete-panel-body">
<div
@@ -31,7 +53,7 @@
</div>
</template>
-<script src="./emoji-input.js"></script>
+<script src="./emoji_input.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@@ -39,11 +61,36 @@
.emoji-input {
display: flex;
flex-direction: column;
+ position: relative;
+
+ .emoji-picker-icon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: .2em .25em;
+ font-size: 16px;
+ cursor: pointer;
+ line-height: 24px;
+
+ &:hover i {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+ }
+ .emoji-picker-panel {
+ position: absolute;
+ z-index: 20;
+ margin-top: 2px;
+
+ &.hide {
+ display: none
+ }
+ }
.autocomplete {
&-panel {
position: absolute;
- z-index: 9;
+ z-index: 20;
margin-top: 2px;
&.hide {
diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji_input/suggestor.js
index aec5c39d..aec5c39d 100644
--- a/src/components/emoji-input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
new file mode 100644
index 00000000..824412dd
--- /dev/null
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -0,0 +1,115 @@
+
+const filterByKeyword = (list, keyword = '') => {
+ return list.filter(x => x.displayText.includes(keyword))
+}
+
+const EmojiPicker = {
+ props: {
+ enableStickerPicker: {
+ required: false,
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ labelKey: String(Math.random() * 100000),
+ keyword: '',
+ activeGroup: 'custom',
+ showingStickers: false,
+ groupsScrolledClass: 'scrolled-top',
+ keepOpen: false
+ }
+ },
+ components: {
+ StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
+ },
+ methods: {
+ onEmoji (emoji) {
+ const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
+ this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
+ },
+ highlight (key) {
+ const ref = this.$refs['group-' + key]
+ const top = ref[0].offsetTop
+ this.setShowStickers(false)
+ this.activeGroup = key
+ this.$nextTick(() => {
+ this.$refs['emoji-groups'].scrollTop = top + 1
+ })
+ },
+ scrolledGroup (e) {
+ const target = (e && e.target) || this.$refs['emoji-groups']
+ const top = target.scrollTop + 5
+ if (target.scrollTop <= 5) {
+ this.groupsScrolledClass = 'scrolled-top'
+ } else if (target.scrollTop >= target.scrollTopMax - 5) {
+ this.groupsScrolledClass = 'scrolled-bottom'
+ } else {
+ this.groupsScrolledClass = 'scrolled-middle'
+ }
+ this.$nextTick(() => {
+ this.emojisView.forEach(group => {
+ const ref = this.$refs['group-' + group.id]
+ if (ref[0].offsetTop <= top) {
+ this.activeGroup = group.id
+ }
+ })
+ })
+ },
+ toggleStickers () {
+ this.showingStickers = !this.showingStickers
+ },
+ setShowStickers (value) {
+ this.showingStickers = value
+ },
+ onStickerUploaded (e) {
+ this.$emit('sticker-uploaded', e)
+ },
+ onStickerUploadFailed (e) {
+ this.$emit('sticker-upload-failed', e)
+ }
+ },
+ watch: {
+ keyword () {
+ this.scrolledGroup()
+ }
+ },
+ computed: {
+ activeGroupView () {
+ return this.showingStickers ? '' : this.activeGroup
+ },
+ stickersAvailable () {
+ if (this.$store.state.instance.stickers) {
+ return this.$store.state.instance.stickers.length > 0
+ }
+ return 0
+ },
+ emojis () {
+ const standardEmojis = this.$store.state.instance.emoji || []
+ const customEmojis = this.$store.state.instance.customEmoji || []
+ return [
+ {
+ id: 'custom',
+ text: this.$t('emoji.custom'),
+ icon: 'icon-smile',
+ emojis: filterByKeyword(customEmojis, this.keyword)
+ },
+ {
+ id: 'standard',
+ text: this.$t('emoji.unicode'),
+ icon: 'icon-picture',
+ emojis: filterByKeyword(standardEmojis, this.keyword)
+ }
+ ]
+ },
+ emojisView () {
+ return this.emojis.filter(value => value.emojis.length > 0)
+ },
+ stickerPickerEnabled () {
+ return (this.$store.state.instance.stickers || []).length !== 0
+ }
+ }
+}
+
+export default EmojiPicker
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
new file mode 100644
index 00000000..b0ed00e9
--- /dev/null
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -0,0 +1,165 @@
+@import '../../_variables.scss';
+
+.emoji-picker {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ right: 0;
+ left: 0;
+ height: 320px;
+ margin: 0 !important;
+ z-index: 1;
+
+ .keep-open {
+ padding: 7px;
+ line-height: normal;
+ }
+ .keep-open-label {
+ padding: 0 7px;
+ display: flex;
+ }
+
+ .heading {
+ display: flex;
+ height: 32px;
+ padding: 10px 7px 5px;
+ }
+
+ .content {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0px;
+ }
+
+ .emoji-tabs {
+ flex-grow: 1;
+ }
+
+ .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;
+ }
+
+ .additional-tabs,
+ .emoji-tabs {
+ display: block;
+ min-width: 0;
+ flex-basis: auto;
+ flex-shrink: 1;
+
+ &-item {
+ padding: 0 7px;
+ cursor: pointer;
+ font-size: 24px;
+
+ &.disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ &.active {
+ border-bottom: 4px solid;
+
+ i {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ }
+ }
+ }
+
+ .sticker-picker {
+ flex: 1 1 0
+ }
+
+ .stickers,
+ .emoji {
+ &-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0;
+
+ &.hidden {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ }
+ }
+ }
+
+ .emoji {
+ &-search {
+ padding: 5px;
+ flex: 0 0 0;
+
+ input {
+ width: 100%;
+ }
+ }
+
+ &-groups {
+ flex: 1 1 1px;
+ position: relative;
+ overflow: auto;
+ user-select: none;
+ mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+ linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+ linear-gradient(to top, white, white);
+ transition: mask-size 150ms;
+ mask-size: 100% 20px, 100% 20px, auto;
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ &.scrolled {
+ &-top {
+ mask-size: 100% 20px, 100% 0, auto;
+ }
+ &-bottom {
+ mask-size: 100% 0, 100% 20px, auto;
+ }
+ }
+ }
+
+ &-group {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ padding-left: 5px;
+ justify-content: left;
+
+ &-title {
+ font-size: 12px;
+ width: 100%;
+ margin: 0;
+ &.disabled {
+ display: none;
+ }
+ }
+ }
+
+ &-item {
+ width: 32px;
+ height: 32px;
+ box-sizing: border-box;
+ display: flex;
+ font-size: 32px;
+ align-items: center;
+ justify-content: center;
+ margin: 4px;
+
+ cursor: pointer;
+
+ img {
+ object-fit: contain;
+ max-width: 100%;
+ max-height: 100%;
+ }
+ }
+
+ }
+
+}
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
new file mode 100644
index 00000000..42f20130
--- /dev/null
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -0,0 +1,110 @@
+<template>
+ <div class="emoji-picker panel panel-default panel-body">
+ <div class="heading">
+ <span class="emoji-tabs">
+ <span
+ v-for="group in emojis"
+ :key="group.id"
+ class="emoji-tabs-item"
+ :class="{
+ active: activeGroupView === group.id,
+ disabled: group.emojis.length === 0
+ }"
+ :title="group.text"
+ @click.prevent="highlight(group.id)"
+ >
+ <i :class="group.icon" />
+ </span>
+ </span>
+ <span
+ v-if="stickerPickerEnabled"
+ class="additional-tabs"
+ >
+ <span
+ class="stickers-tab-icon additional-tabs-item"
+ :class="{active: showingStickers}"
+ :title="$t('emoji.stickers')"
+ @click.prevent="toggleStickers"
+ >
+ <i class="icon-star" />
+ </span>
+ </span>
+ </div>
+ <div class="content">
+ <div
+ class="emoji-content"
+ :class="{hidden: showingStickers}"
+ >
+ <div class="emoji-search">
+ <input
+ v-model="keyword"
+ type="text"
+ class="form-control"
+ :placeholder="$t('emoji.search_emoji')"
+ >
+ </div>
+ <div
+ ref="emoji-groups"
+ class="emoji-groups"
+ :class="groupsScrolledClass"
+ @scroll="scrolledGroup"
+ >
+ <div
+ v-for="group in emojisView"
+ :key="group.id"
+ class="emoji-group"
+ >
+ <h6
+ :ref="'group-' + group.id"
+ class="emoji-group-title"
+ >
+ {{ group.text }}
+ </h6>
+ <span
+ v-for="emoji in group.emojis"
+ :key="group.id + emoji.displayText"
+ :title="emoji.displayText"
+ class="emoji-item"
+ @click.stop.prevent="onEmoji(emoji)"
+ >
+ <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
+ <img
+ v-else
+ :src="emoji.imageUrl"
+ >
+ </span>
+ </div>
+ </div>
+ <div
+ class="keep-open"
+ >
+ <input
+ :id="labelKey + 'keep-open'"
+ v-model="keepOpen"
+ type="checkbox"
+ >
+ <label
+ class="keep-open-label"
+ :for="labelKey + 'keep-open'"
+ >
+ <div class="keep-open-label-text">
+ {{ $t('emoji.keep_open') }}
+ </div>
+ </label>
+ </div>
+ </div>
+ <div
+ v-if="showingStickers"
+ class="stickers-content"
+ >
+ <sticker-picker
+ @uploaded="onStickerUploaded"
+ @upload-failed="onStickerUploadFailed"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./emoji_picker.js"></script>
+<style lang="scss" src="./emoji_picker.scss"></style>
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index ac32ae83..1dda7bc1 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -31,12 +31,14 @@
<script src="./media_upload.js" ></script>
<style>
- .media-upload {
- font-size: 26px;
- min-width: 50px;
- }
+.media-upload {
+ .icon-upload {
+ cursor: pointer;
+ }
- .icon-upload {
- cursor: pointer;
- }
+ label {
+ display: block;
+ width: 100%;
+ }
+}
</style>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index dc4b419c..9b2a9c90 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -1,12 +1,12 @@
import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
-import EmojiInput from '../emoji-input/emoji-input.vue'
+import EmojiInput from '../emoji_input/emoji_input.vue'
import PollForm from '../poll/poll_form.vue'
-import StickerPicker from '../sticker_picker/sticker_picker.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
+import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy } from 'lodash'
-import suggestor from '../emoji-input/suggestor.js'
+import suggestor from '../emoji_input/suggestor.js'
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
let allAttentions = [...attentions]
@@ -35,7 +35,6 @@ const PostStatusForm = {
MediaUpload,
EmojiInput,
PollForm,
- StickerPicker,
ScopeSelector
},
mounted () {
@@ -84,8 +83,7 @@ const PostStatusForm = {
contentType
},
caret: 0,
- pollFormVisible: false,
- stickerPickerVisible: false
+ pollFormVisible: false
}
},
computed: {
@@ -161,12 +159,6 @@ const PostStatusForm = {
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
- stickersAvailable () {
- if (this.$store.state.instance.stickers) {
- return this.$store.state.instance.stickers.length > 0
- }
- return 0
- },
pollsAvailable () {
return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2
@@ -222,7 +214,6 @@ const PostStatusForm = {
poll: {}
}
this.pollFormVisible = false
- this.stickerPickerVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted')
@@ -239,7 +230,6 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
this.enableSubmit()
- this.stickerPickerVisible = false
},
removeMediaFile (fileInfo) {
let index = this.newStatus.files.indexOf(fileInfo)
@@ -260,6 +250,7 @@ const PostStatusForm = {
return fileTypeService.fileType(fileInfo.mimetype)
},
paste (e) {
+ this.resize(e)
if (e.clipboardData.files.length > 0) {
// prevent pasting of file as text
e.preventDefault()
@@ -278,20 +269,96 @@ const PostStatusForm = {
fileDrag (e) {
e.dataTransfer.dropEffect = 'copy'
},
+ onEmojiInputInput (e) {
+ this.$nextTick(() => {
+ this.resize(this.$refs['textarea'])
+ })
+ },
resize (e) {
const target = e.target || e
if (!(target instanceof window.Element)) { return }
+
+ // Reset to default height for empty form, nothing else to do here.
+ if (target.value === '') {
+ target.style.height = null
+ this.$refs['emoji-input'].resize()
+ return
+ }
+
+ const rootRef = this.$refs['root']
+ /* Scroller is either `window` (replies in TL), sidebar (main post form,
+ * replies in notifs) or mobile post form. Note that getting and setting
+ * scroll is different for `Window` and `Element`s
+ */
+ const scrollerRef = this.$el.closest('.sidebar-scroller') ||
+ this.$el.closest('.post-form-modal-view') ||
+ window
+
+ // Getting info about padding we have to account for, removing 'px' part
const topPaddingStr = window.getComputedStyle(target)['padding-top']
const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
- // Remove "px" at the end of the values
- const vertPadding = Number(topPaddingStr.substr(0, topPaddingStr.length - 2)) +
- Number(bottomPaddingStr.substr(0, bottomPaddingStr.length - 2))
- // Auto is needed to make textbox shrink when removing lines
+ const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
+ 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
+ * scrollHeight returns element's scrollable content height, i.e. visible
+ * element + overscrolled parts of it. We use it to determine when text
+ * inside the textarea exceeded its height, so we can set height to prevent
+ * overscroll, i.e. make textarea grow with the text. HOWEVER, since we
+ * explicitly set new height, scrollHeight won't go below that, so we can't
+ * SHRINK the textarea when there's extra space. To workaround that we set
+ * height to 'auto' which makes textarea tiny again, so that scrollHeight
+ * will match text height again. HOWEVER, shrinking textarea can screw with
+ * the scroll since there might be not enough padding around root to even
+ * warrant a scroll, so it will jump to 0 and refuse to move anywhere,
+ * so we check current scroll position before shrinking and then restore it
+ * with needed delta.
+ */
+
+ // this part has to be BEFORE the content size update
+ const currentScroll = scrollerRef === window
+ ? scrollerRef.scrollY
+ : scrollerRef.scrollTop
+ const scrollerHeight = scrollerRef === window
+ ? scrollerRef.innerHeight
+ : scrollerRef.offsetHeight
+ const scrollerBottomBorder = currentScroll + scrollerHeight
+
+ // BEGIN content size update
target.style.height = 'auto'
- target.style.height = `${target.scrollHeight - vertPadding}px`
- if (target.value === '') {
- target.style.height = null
+ const newHeight = target.scrollHeight - vertPadding
+ target.style.height = `${newHeight}px`
+ // END content size update
+
+ // We check where the bottom border of root element is, this uses findOffset
+ // to find offset relative to scrollable container (scroller)
+ const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
+
+ const textareaSizeChangeDelta = newHeight - oldHeight || 0
+ const isBottomObstructed = scrollerBottomBorder < rootBottomBorder
+ const rootChangeDelta = rootBottomBorder - scrollerBottomBorder
+ const totalDelta = textareaSizeChangeDelta +
+ (isBottomObstructed ? rootChangeDelta : 0)
+
+ const targetScroll = currentScroll + totalDelta
+
+ if (scrollerRef === window) {
+ scrollerRef.scroll(0, targetScroll)
+ } else {
+ scrollerRef.scrollTop = targetScroll
}
+
+ this.$refs['emoji-input'].resize()
+ },
+ showEmojiPicker () {
+ this.$refs['textarea'].focus()
+ this.$refs['emoji-input'].triggerShowPicker()
},
clearError () {
this.error = null
@@ -299,14 +366,6 @@ const PostStatusForm = {
changeVis (visibility) {
this.newStatus.visibility = visibility
},
- toggleStickerPicker () {
- this.stickerPickerVisible = !this.stickerPickerVisible
- },
- clearStickerPicker () {
- if (this.$refs.stickerPicker) {
- this.$refs.stickerPicker.clear()
- }
- },
togglePollForm () {
this.pollFormVisible = !this.pollFormVisible
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index d29d47e4..4916d988 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -1,5 +1,8 @@
<template>
- <div class="post-status-form">
+ <div
+ ref="root"
+ class="post-status-form"
+ >
<form
autocomplete="off"
@submit.prevent="postStatus(newStatus)"
@@ -61,6 +64,7 @@
<EmojiInput
v-if="newStatus.spoilerText || alwaysShowSubject"
v-model="newStatus.spoilerText"
+ enable-emoji-picker
:suggest="emojiSuggestor"
class="form-control"
>
@@ -73,9 +77,16 @@
>
</EmojiInput>
<EmojiInput
+ ref="emoji-input"
v-model="newStatus.status"
:suggest="emojiUserSuggestor"
class="form-control main-input"
+ enable-emoji-picker
+ hide-emoji-button
+ enable-sticker-picker
+ @input="onEmojiInputInput"
+ @sticker-uploaded="addMediaFile"
+ @sticker-upload-failed="uploadFailed"
>
<textarea
ref="textarea"
@@ -89,6 +100,7 @@
@drop="fileDrop"
@dragover.prevent="fileDrag"
@input="resize"
+ @compositionupdate="resize"
@paste="paste"
/>
<p
@@ -152,30 +164,29 @@
<div class="form-bottom-left">
<media-upload
ref="mediaUpload"
+ class="media-upload-icon"
:drop-files="dropFiles"
@uploading="disableSubmit"
@uploaded="addMediaFile"
@upload-failed="uploadFailed"
/>
<div
- v-if="stickersAvailable"
- class="sticker-icon"
+ class="emoji-icon"
>
<i
- :title="$t('stickers.add_sticker')"
- class="icon-picture btn btn-default"
- :class="{ selected: stickerPickerVisible }"
- @click="toggleStickerPicker"
+ :title="$t('emoji.add_emoji')"
+ class="icon-smile btn btn-default"
+ @click="showEmojiPicker"
/>
</div>
<div
v-if="pollsAvailable"
class="poll-icon"
+ :class="{ selected: pollFormVisible }"
>
<i
:title="$t('polls.add_poll')"
class="icon-chart-bar btn btn-default"
- :class="pollFormVisible && 'selected'"
@click="togglePollForm"
/>
</div>
@@ -258,11 +269,6 @@
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div>
</form>
- <sticker-picker
- v-if="stickerPickerVisible"
- ref="stickerPicker"
- @uploaded="addMediaFile"
- />
</div>
</template>
@@ -299,6 +305,7 @@
.post-status-form {
.form-bottom {
display: flex;
+ justify-content: space-between;
padding: 0.5em;
height: 32px;
@@ -316,6 +323,9 @@
.form-bottom-left {
display: flex;
flex: 1;
+ padding-right: 7px;
+ margin-right: 7px;
+ max-width: 10em;
}
.text-format {
@@ -325,19 +335,38 @@
}
}
- .poll-icon, .sticker-icon {
+ .media-upload-icon, .poll-icon, .emoji-icon {
font-size: 26px;
flex: 1;
- .selected {
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
+ i {
+ display: block;
+ width: 100%;
+ }
+
+ &.selected, &:hover {
+ // needs to be specific to override icon default color
+ i, label {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
}
}
- .sticker-icon {
- flex: 0;
- min-width: 50px;
+ // Order is not necessary but a good indicator
+ .media-upload-icon {
+ order: 1;
+ text-align: left;
+ }
+
+ .emoji-icon {
+ order: 2;
+ text-align: center;
+ }
+
+ .poll-icon {
+ order: 3;
+ text-align: right;
}
.icon-chart-bar {
@@ -369,6 +398,13 @@
}
}
+ .status-input-wrapper {
+ display: flex;
+ position: relative;
+ width: 100%;
+ flex-direction: column;
+ }
+
.attachments {
padding: 0 0.5em;
@@ -444,10 +480,6 @@
box-sizing: content-box;
}
- .form-post-body:focus {
- min-height: 48px;
- }
-
.main-input {
position: relative;
}
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index c4aa45b2..b6540d7e 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -16,6 +16,7 @@ const settings = {
return {
hideAttachmentsLocal: user.hideAttachments,
+ padEmojiLocal: user.padEmoji,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw,
@@ -127,6 +128,9 @@ const settings = {
hideAttachmentsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideAttachments', value })
},
+ padEmojiLocal (value) {
+ this.$store.dispatch('setOption', { name: 'padEmoji', value })
+ },
hideAttachmentsInConvLocal (value) {
this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
},
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 744ec566..6d87a060 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -198,6 +198,14 @@
>
<label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
</li>
+ <li>
+ <input
+ id="padEmoji"
+ v-model="padEmojiLocal"
+ type="checkbox"
+ >
+ <label for="padEmoji">{{ $t('settings.pad_emoji') }}</label>
+ </li>
</ul>
</div>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 64218f6e..93f37a49 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -413,7 +413,7 @@
v-if="replying"
class="container"
>
- <post-status-form
+ <PostStatusForm
class="reply-body"
:reply-to="status.id"
:attentions="status.attentions"
@@ -665,6 +665,15 @@ $status-margin: 0.75em;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
+ z-index: 1;
+ .status-content {
+ height: 100%;
+ mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+ linear-gradient(to top, white, white);
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ }
}
.tall-status-hider {
@@ -676,12 +685,7 @@ $status-margin: 0.75em;
width: 100%;
text-align: center;
line-height: 110px;
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
- &_focused {
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--lightBg 80%);
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--lightBg, $fallback--lightBg) 80%);
- }
+ z-index: 2;
}
.status-unhider, .cw-status-hider {
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
index a6dcded3..8daf3f07 100644
--- a/src/components/sticker_picker/sticker_picker.js
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -3,9 +3,9 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import TabSwitcher from '../tab_switcher/tab_switcher.js'
const StickerPicker = {
- components: [
+ components: {
TabSwitcher
- ],
+ },
data () {
return {
meta: {
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
index 938204c8..323855b9 100644
--- a/src/components/sticker_picker/sticker_picker.vue
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -2,32 +2,30 @@
<div
class="sticker-picker"
>
- <div
- class="sticker-picker-panel"
+ <tab-switcher
+ class="tab-switcher"
+ :render-only-focused="true"
+ scrollable-tabs
>
- <tab-switcher
- :render-only-focused="true"
+ <div
+ v-for="stickerpack in pack"
+ :key="stickerpack.path"
+ :image-tooltip="stickerpack.meta.title"
+ :image="stickerpack.path + stickerpack.meta.tabIcon"
+ class="sticker-picker-content"
>
<div
- v-for="stickerpack in pack"
- :key="stickerpack.path"
- :image-tooltip="stickerpack.meta.title"
- :image="stickerpack.path + stickerpack.meta.tabIcon"
- class="sticker-picker-content"
+ v-for="sticker in stickerpack.meta.stickers"
+ :key="sticker"
+ class="sticker"
+ @click.stop.prevent="pick(stickerpack.path + sticker, stickerpack.meta.title)"
>
- <div
- v-for="sticker in stickerpack.meta.stickers"
- :key="sticker"
- class="sticker"
- @click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
+ <img
+ :src="stickerpack.path + sticker"
>
- <img
- :src="stickerpack.path + sticker"
- >
- </div>
</div>
- </tab-switcher>
- </div>
+ </div>
+ </tab-switcher>
</div>
</template>
@@ -37,22 +35,24 @@
@import '../../_variables.scss';
.sticker-picker {
- .sticker-picker-panel {
- display: inline-block;
- width: 100%;
- .sticker-picker-content {
- max-height: 300px;
- overflow-y: scroll;
- overflow-x: auto;
- .sticker {
- display: inline-block;
- width: 20%;
- height: 20%;
- img {
- width: 100%;
- &:hover {
- filter: drop-shadow(0 0 5px var(--link, $fallback--link));
- }
+ width: 100%;
+ position: relative;
+ .tab-switcher {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ .sticker-picker-content {
+ .sticker {
+ display: inline-block;
+ width: 20%;
+ height: 20%;
+ img {
+ width: 100%;
+ &:hover {
+ filter: drop-shadow(0 0 5px var(--link, $fallback--link));
}
}
}
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 08d5d08f..3ca316b9 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,7 +4,26 @@ import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
- props: ['renderOnlyFocused', 'onSwitch', 'activeTab'],
+ props: {
+ renderOnlyFocused: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ onSwitch: {
+ required: false,
+ type: Function
+ },
+ activeTab: {
+ required: false,
+ type: String
+ },
+ scrollableTabs: {
+ required: false,
+ type: Boolean,
+ default: false
+ }
+ },
data () {
return {
active: this.$slots.default.findIndex(_ => _.tag)
@@ -28,7 +47,8 @@ export default Vue.component('tab-switcher', {
},
methods: {
activateTab (index) {
- return () => {
+ return (e) => {
+ e.preventDefault()
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.$slots.default[index].key)
}
@@ -87,7 +107,7 @@ export default Vue.component('tab-switcher', {
<div class="tabs">
{tabs}
</div>
- <div class="contents">
+ <div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
{contents}
</div>
</div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 4eeb42e0..3e5eacd5 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -1,10 +1,21 @@
@import '../../_variables.scss';
.tab-switcher {
+ display: flex;
+ flex-direction: column;
+
.contents {
+ flex: 1 0 auto;
+ min-height: 0px;
+
.hidden {
display: none;
}
+
+ &.scrollable-tabs {
+ flex-basis: 0;
+ overflow-y: auto;
+ }
}
.tabs {
display: flex;
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 0c200ad1..9c931c01 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -37,19 +37,10 @@ export default {
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
- const gradient = [
- [tintColor, this.hideBio ? '60%' : ''],
- this.hideBio ? [
- color, '100%'
- ] : [
- tintColor, ''
- ]
- ].map(_ => _.join(' ')).join(', ')
-
return {
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
backgroundImage: [
- `linear-gradient(to bottom, ${gradient})`,
+ `linear-gradient(to bottom, ${tintColor}, ${tintColor})`,
`url(${this.user.cover_photo})`
].join(', ')
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index f25d16d3..5b6f66e7 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -2,8 +2,12 @@
<div
class="user-card"
:class="classes"
- :style="style"
>
+ <div
+ :class="{ 'hide-bio': hideBio }"
+ :style="style"
+ class="background-image"
+ />
<div class="panel-heading">
<div class="user-info">
<div class="container">
@@ -307,7 +311,7 @@
@import '../../_variables.scss';
.user-card {
- background-size: cover;
+ position: relative;
.panel-heading {
padding: .5em 0;
@@ -316,14 +320,35 @@
background: transparent;
flex-direction: column;
align-items: stretch;
+ // create new stacking context
+ position: relative;
}
.panel-body {
word-wrap: break-word;
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
border-bottom-right-radius: inherit;
border-bottom-left-radius: inherit;
+ // create new stacking context
+ position: relative;
+ }
+
+ .background-image {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ mask: linear-gradient(to top, white, transparent) bottom no-repeat,
+ linear-gradient(to top, white, white);
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ background-size: cover;
+ mask-size: 100% 60%;
+
+ &.hide-bio {
+ mask-size: 100% 40px;
+ }
}
p {
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index e859d612..e9f08015 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -11,7 +11,7 @@
rounded="top"
/>
<div class="panel-footer">
- <post-status-form />
+ <PostStatusForm />
</div>
</div>
<auth-form
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index b5a7f0df..ae04ce73 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -11,8 +11,8 @@ import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
-import EmojiInput from '../emoji-input/emoji-input.vue'
-import suggestor from '../emoji-input/suggestor.js'
+import EmojiInput from '../emoji_input/emoji_input.vue'
+import suggestor from '../emoji_input/suggestor.js'
import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 34ea8569..97833acb 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -32,6 +32,7 @@
<p>{{ $t('settings.name') }}</p>
<EmojiInput
v-model="newName"
+ enable-emoji-picker
:suggest="emojiSuggestor"
>
<input
@@ -43,6 +44,7 @@
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
v-model="newBio"
+ enable-emoji-picker
:suggest="emojiUserSuggestor"
>
<textarea
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e4af507e..3d81fdca 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,8 +106,14 @@
"expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll"
},
- "stickers": {
- "add_sticker": "Add Sticker"
+ "emoji": {
+ "stickers": "Stickers",
+ "emoji": "Emoji",
+ "keep_open": "Keep picker open",
+ "search_emoji": "Search for an emoji",
+ "add_emoji": "Insert emoji",
+ "custom": "Custom emoji",
+ "unicode": "Unicode emoji"
},
"interactions": {
"favs_repeats": "Repeats and Favorites",
@@ -226,6 +232,7 @@
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
+ "pad_emoji": "Pad emoji with spaces when adding from picker",
"export_theme": "Save preset",
"filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line",
diff --git a/src/modules/config.js b/src/modules/config.js
index 2bfad8f6..cf04d14f 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -7,6 +7,7 @@ const defaultState = {
colors: {},
hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
+ padEmoji: true,
hideAttachments: false,
hideAttachmentsInConv: false,
maxThumbnails: 16,
diff --git a/src/services/offset_finder/offset_finder.service.js b/src/services/offset_finder/offset_finder.service.js
new file mode 100644
index 00000000..9034f8c8
--- /dev/null
+++ b/src/services/offset_finder/offset_finder.service.js
@@ -0,0 +1,31 @@
+export const findOffset = (child, parent, { top = 0, left = 0 } = {}, ignorePadding = true) => {
+ const result = {
+ top: top + child.offsetTop,
+ left: left + child.offsetLeft
+ }
+ if (!ignorePadding && child !== window) {
+ const { topPadding, leftPadding } = findPadding(child)
+ result.top += ignorePadding ? 0 : topPadding
+ result.left += ignorePadding ? 0 : leftPadding
+ }
+
+ if (child.offsetParent && (parent === window || parent.contains(child.offsetParent) || parent === child.offsetParent)) {
+ return findOffset(child.offsetParent, parent, result, false)
+ } else {
+ if (parent !== window) {
+ const { topPadding, leftPadding } = findPadding(parent)
+ result.top += topPadding
+ result.left += leftPadding
+ }
+ return result
+ }
+}
+
+const findPadding = (el) => {
+ const topPaddingStr = window.getComputedStyle(el)['padding-top']
+ const topPadding = Number(topPaddingStr.substring(0, topPaddingStr.length - 2))
+ const leftPaddingStr = window.getComputedStyle(el)['padding-left']
+ const leftPadding = Number(leftPaddingStr.substring(0, leftPaddingStr.length - 2))
+
+ return { topPadding, leftPadding }
+}