Merge branch 'develop' into feature/following_reblogs
This commit is contained in:
commit
19cb98b85f
52 changed files with 1590 additions and 378 deletions
|
@ -8,9 +8,10 @@ import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_pan
|
|||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import { windowWidth } from './services/window_utils/window_utils'
|
||||
|
||||
export default {
|
||||
|
@ -26,9 +27,10 @@ export default {
|
|||
ChatPanel,
|
||||
MediaModal,
|
||||
SideDrawer,
|
||||
MobilePostStatusModal,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
UserReportingModal
|
||||
UserReportingModal,
|
||||
PostStatusModal
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
position: fixed;
|
||||
z-index: -1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: -20px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
|
@ -347,6 +348,7 @@ i[class*=icon-] {
|
|||
align-items: center;
|
||||
position: fixed;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
|
@ -386,6 +388,7 @@ i[class*=icon-] {
|
|||
}
|
||||
|
||||
.inner-nav {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
padding-left: 10px;
|
||||
|
|
30
src/App.vue
30
src/App.vue
|
@ -4,6 +4,7 @@
|
|||
:style="bgAppStyle"
|
||||
>
|
||||
<div
|
||||
id="app_bg_wrapper"
|
||||
class="app-bg-wrapper"
|
||||
:style="bgStyle"
|
||||
/>
|
||||
|
@ -14,20 +15,20 @@
|
|||
class="nav-bar container"
|
||||
@click="scrollToTop()"
|
||||
>
|
||||
<div
|
||||
class="logo"
|
||||
:style="logoBgStyle"
|
||||
>
|
||||
<div
|
||||
class="mask"
|
||||
:style="logoMaskStyle"
|
||||
/>
|
||||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
>
|
||||
</div>
|
||||
<div class="inner-nav">
|
||||
<div
|
||||
class="logo"
|
||||
:style="logoBgStyle"
|
||||
>
|
||||
<div
|
||||
class="mask"
|
||||
:style="logoMaskStyle"
|
||||
/>
|
||||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
>
|
||||
</div>
|
||||
<div class="item">
|
||||
<router-link
|
||||
class="site-name"
|
||||
|
@ -107,8 +108,9 @@
|
|||
:floating="true"
|
||||
class="floating-chat mobile-hidden"
|
||||
/>
|
||||
<MobilePostStatusModal />
|
||||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 === 'ArrowUp') {
|
||||
this.cycleBackward(e)
|
||||
} else if (key === 'ArrowDown') {
|
||||
this.cycleForward(e)
|
||||
}
|
||||
if (key === 'Enter') {
|
||||
if (!ctrlKey) {
|
||||
this.replaceText(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'
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
115
src/components/emoji_picker/emoji_picker.js
Normal file
115
src/components/emoji_picker/emoji_picker.js
Normal file
|
@ -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
|
165
src/components/emoji_picker/emoji_picker.scss
Normal file
165
src/components/emoji_picker/emoji_picker.scss
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
110
src/components/emoji_picker/emoji_picker.vue
Normal file
110
src/components/emoji_picker/emoji_picker.vue
Normal file
|
@ -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>
|
|
@ -10,14 +10,14 @@
|
|||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.muted"
|
||||
v-if="canMute && !status.thread_muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="muteConversation"
|
||||
>
|
||||
<i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canMute && status.muted"
|
||||
v-if="canMute && status.thread_muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unmuteConversation"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="showing"
|
||||
v-body-scroll-lock="showing"
|
||||
class="modal-view media-modal-view"
|
||||
@click.prevent="hide"
|
||||
>
|
||||
|
@ -43,6 +44,10 @@
|
|||
.media-modal-view {
|
||||
z-index: 1001;
|
||||
|
||||
body:not(.scroll-locked) & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.modal-view-button-arrow {
|
||||
opacity: 0.75;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const MobilePostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
const MobilePostStatusButton = {
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
postFormOpen: false,
|
||||
scrollingDown: false,
|
||||
inputActive: false,
|
||||
oldScrollPos: 0,
|
||||
|
@ -28,8 +23,8 @@ const MobilePostStatusModal = {
|
|||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
isHidden () {
|
||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||
|
@ -57,17 +52,7 @@ const MobilePostStatusModal = {
|
|||
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
openPostForm () {
|
||||
this.postFormOpen = true
|
||||
this.hidden = true
|
||||
|
||||
const el = this.$el.querySelector('textarea')
|
||||
this.$nextTick(function () {
|
||||
el.focus()
|
||||
})
|
||||
},
|
||||
closePostForm () {
|
||||
this.postFormOpen = false
|
||||
this.hidden = false
|
||||
this.$store.dispatch('openPostStatusModal')
|
||||
},
|
||||
handleOSK () {
|
||||
// This is a big hack: we're guessing from changed window sizes if the
|
||||
|
@ -105,4 +90,4 @@ const MobilePostStatusModal = {
|
|||
}
|
||||
}
|
||||
|
||||
export default MobilePostStatusModal
|
||||
export default MobilePostStatusButton
|
|
@ -1,23 +1,5 @@
|
|||
<template>
|
||||
<div v-if="currentUser">
|
||||
<div
|
||||
v-show="postFormOpen"
|
||||
class="post-form-modal-view modal-view"
|
||||
@click="closePostForm"
|
||||
>
|
||||
<div
|
||||
class="post-form-modal-panel panel"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.new_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
@posted="closePostForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoggedIn">
|
||||
<button
|
||||
class="new-status-button"
|
||||
:class="{ 'hidden': isHidden }"
|
||||
|
@ -28,27 +10,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mobile_post_status_modal.js"></script>
|
||||
<script src="./mobile_post_status_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
width: 5em;
|
||||
height: 5em;
|
|
@ -9,7 +9,8 @@ const Notification = {
|
|||
data () {
|
||||
return {
|
||||
userExpanded: false,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
|
||||
unmuted: false
|
||||
}
|
||||
},
|
||||
props: [ 'notification' ],
|
||||
|
@ -23,11 +24,14 @@ const Notification = {
|
|||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
generateUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
getUser (notification) {
|
||||
return this.$store.state.users.usersObject[notification.from_profile.id]
|
||||
},
|
||||
toggleMute () {
|
||||
this.unmuted = !this.unmuted
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -47,6 +51,12 @@ const Notification = {
|
|||
return this.userInStore
|
||||
}
|
||||
return this.notification.from_profile
|
||||
},
|
||||
userProfileLink () {
|
||||
return this.generateUserProfileLink(this.user)
|
||||
},
|
||||
needMute () {
|
||||
return this.user.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,104 +4,126 @@
|
|||
:compact="true"
|
||||
:statusoid="notification.status"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="non-mention"
|
||||
:class="[userClass, { highlighted: userStyle }]"
|
||||
:style="[ userStyle ]"
|
||||
>
|
||||
<a
|
||||
class="avatar-container"
|
||||
:href="notification.from_profile.statusnet_profile_url"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="needMute && !unmuted"
|
||||
class="container muted"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
v-html="notification.from_profile.name_html"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
>{{ notification.from_profile.name }}</span>
|
||||
<span v-if="notification.type === 'like'">
|
||||
<i class="fa icon-star lit" />
|
||||
<small>{{ $t('notifications.favorited_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'repeat'">
|
||||
<i
|
||||
class="fa icon-retweet lit"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
<small>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
</small>
|
||||
<a
|
||||
href="#"
|
||||
class="unmute"
|
||||
@click.prevent="toggleMute"
|
||||
><i class="button-icon icon-eye-off" /></a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="non-mention"
|
||||
:class="[userClass, { highlighted: userStyle }]"
|
||||
:style="[ userStyle ]"
|
||||
>
|
||||
<a
|
||||
class="avatar-container"
|
||||
:href="notification.from_profile.statusnet_profile_url"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
v-html="notification.from_profile.name_html"
|
||||
/>
|
||||
<small>{{ $t('notifications.repeated_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow'">
|
||||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
>{{ notification.from_profile.name }}</span>
|
||||
<span v-if="notification.type === 'like'">
|
||||
<i class="fa icon-star lit" />
|
||||
<small>{{ $t('notifications.favorited_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'repeat'">
|
||||
<i
|
||||
class="fa icon-retweet lit"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
/>
|
||||
<small>{{ $t('notifications.repeated_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow'">
|
||||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
v-if="notification.status"
|
||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
||||
class="faint-link"
|
||||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<a
|
||||
v-if="needMute"
|
||||
href="#"
|
||||
@click.prevent="toggleMute"
|
||||
><i class="button-icon icon-eye-off" /></a>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="timeago"
|
||||
class="follow-text"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
v-if="notification.status"
|
||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
||||
class="faint-link"
|
||||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
<router-link :to="userProfileLink">
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
class="follow-text"
|
||||
>
|
||||
<router-link :to="userProfileLink(notification.from_profile)">
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
<template v-else>
|
||||
<status
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
|
||||
.notification {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
@ -47,6 +46,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: .25em .6em;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
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) => {
|
||||
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
|
||||
let allAttentions = [...attentions]
|
||||
|
||||
allAttentions.unshift(user)
|
||||
|
@ -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 }
|
||||
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
|
||||
target.style.height = 'auto'
|
||||
target.style.height = `${target.scrollHeight - vertPadding}px`
|
||||
|
||||
// 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']
|
||||
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'
|
||||
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
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
32
src/components/post_status_modal/post_status_modal.js
Normal file
32
src/components/post_status_modal/post_status_modal.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
|
||||
const PostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
isOpen () {
|
||||
return this.isLoggedIn && this.$store.state.postStatus.modalActivated
|
||||
},
|
||||
params () {
|
||||
return this.$store.state.postStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el.querySelector('textarea').focus())
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closePostStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PostStatusModal
|
43
src/components/post_status_modal/post_status_modal.vue
Normal file
43
src/components/post_status_modal/post_status_modal.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="post-form-modal-view modal-view"
|
||||
@click="closeModal"
|
||||
>
|
||||
<div
|
||||
class="post-form-modal-panel panel"
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.new_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./post_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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 })
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -39,19 +39,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(', ')
|
||||
}
|
||||
|
@ -179,6 +170,9 @@ export default {
|
|||
}
|
||||
this.$store.dispatch('setMedia', [attachment])
|
||||
this.$store.dispatch('setCurrent', attachment)
|
||||
},
|
||||
mentionUser () {
|
||||
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
@ -204,6 +208,15 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
@click="mentionUser"
|
||||
>
|
||||
{{ $t('user_card.mention') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
|
@ -314,7 +327,7 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.user-card {
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
|
||||
.panel-heading {
|
||||
padding: .5em 0;
|
||||
|
@ -323,14 +336,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 {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
rounded="top"
|
||||
/>
|
||||
<div class="panel-footer">
|
||||
<post-status-form v-if="user" />
|
||||
<PostStatusForm />
|
||||
</div>
|
||||
</div>
|
||||
<auth-form
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
69
src/directives/body_scroll_lock.js
Normal file
69
src/directives/body_scroll_lock.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import * as bodyScrollLock from 'body-scroll-lock'
|
||||
|
||||
let previousNavPaddingRight
|
||||
let previousAppBgWrapperRight
|
||||
|
||||
const disableBodyScroll = (el) => {
|
||||
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
|
||||
bodyScrollLock.disableBodyScroll(el, {
|
||||
reserveScrollBarGap: true
|
||||
})
|
||||
setTimeout(() => {
|
||||
// If previousNavPaddingRight is already set, don't set it again.
|
||||
if (previousNavPaddingRight === undefined) {
|
||||
const navEl = document.getElementById('nav')
|
||||
previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
|
||||
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
|
||||
}
|
||||
// If previousAppBgWrapeprRight is already set, don't set it again.
|
||||
if (previousAppBgWrapperRight === undefined) {
|
||||
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
|
||||
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
|
||||
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
|
||||
}
|
||||
document.body.classList.add('scroll-locked')
|
||||
})
|
||||
}
|
||||
|
||||
const enableBodyScroll = (el) => {
|
||||
setTimeout(() => {
|
||||
if (previousNavPaddingRight !== undefined) {
|
||||
document.getElementById('nav').style.paddingRight = previousNavPaddingRight
|
||||
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
|
||||
previousNavPaddingRight = undefined
|
||||
}
|
||||
if (previousAppBgWrapperRight !== undefined) {
|
||||
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
|
||||
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
|
||||
previousAppBgWrapperRight = undefined
|
||||
}
|
||||
document.body.classList.remove('scroll-locked')
|
||||
})
|
||||
bodyScrollLock.enableBodyScroll(el)
|
||||
}
|
||||
|
||||
const directive = {
|
||||
inserted: (el, binding) => {
|
||||
if (binding.value) {
|
||||
disableBodyScroll(el)
|
||||
}
|
||||
},
|
||||
componentUpdated: (el, binding) => {
|
||||
if (binding.oldValue === binding.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.value) {
|
||||
disableBodyScroll(el)
|
||||
} else {
|
||||
enableBodyScroll(el)
|
||||
}
|
||||
},
|
||||
unbind: (el) => {
|
||||
enableBodyScroll(el)
|
||||
}
|
||||
}
|
||||
|
||||
export default (Vue) => {
|
||||
Vue.directive('body-scroll-lock', directive)
|
||||
}
|
|
@ -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",
|
||||
|
@ -529,6 +536,7 @@
|
|||
"follows_you": "Follows you!",
|
||||
"its_you": "It's you!",
|
||||
"media": "Media",
|
||||
"mention": "Mention",
|
||||
"mute": "Mute",
|
||||
"muted": "Muted",
|
||||
"per_day": "per day",
|
||||
|
|
|
@ -508,7 +508,9 @@
|
|||
"pinned": "Fijado",
|
||||
"delete_confirm": "¿Realmente quieres borrar la publicación?",
|
||||
"reply_to": "Respondiendo a",
|
||||
"replies_list": "Respuestas:"
|
||||
"replies_list": "Respuestas:",
|
||||
"mute_conversation": "Silenciar la conversación",
|
||||
"unmute_conversation": "Mostrar la conversación"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Aprobar",
|
||||
|
@ -606,5 +608,16 @@
|
|||
"person_talking": "{count} personas hablando",
|
||||
"people_talking": "{count} gente hablando",
|
||||
"no_results": "Sin resultados"
|
||||
},
|
||||
"password_reset": {
|
||||
"forgot_password": "¿Contraseña olvidada?",
|
||||
"password_reset": "Restablecer la contraseña",
|
||||
"instruction": "Ingrese su dirección de correo electrónico o nombre de usuario. Le enviaremos un enlace para restablecer su contraseña.",
|
||||
"placeholder": "Su correo electrónico o nombre de usuario",
|
||||
"check_email": "Revise su correo electrónico para obtener un enlace para restablecer su contraseña.",
|
||||
"return_home": "Volver a la página de inicio",
|
||||
"not_found": "No pudimos encontrar ese correo electrónico o nombre de usuario.",
|
||||
"too_many_requests": "Has alcanzado el límite de intentos, vuelve a intentarlo más tarde.",
|
||||
"password_reset_disabled": "El restablecimiento de contraseñas está deshabilitado. Póngase en contacto con el administrador de su instancia."
|
||||
}
|
||||
}
|
|
@ -88,7 +88,7 @@
|
|||
"followed_you": "Zu jarraitzen zaitu",
|
||||
"load_older": "Kargatu jakinarazpen zaharragoak",
|
||||
"notifications": "Jakinarazpenak",
|
||||
"read": "Irakurri!",
|
||||
"read": "Irakurrita!",
|
||||
"repeated_you": "zure mezua errepikatu du",
|
||||
"no_more_notifications": "Ez dago jakinarazpen gehiago"
|
||||
},
|
||||
|
@ -116,7 +116,7 @@
|
|||
},
|
||||
"post_status": {
|
||||
"new_status": "Mezu berri bat idatzi",
|
||||
"account_not_locked_warning": "Zure kontua ez dago {0}. Edozeinek jarraitzen hastearekin, zure mezuak irakur dezake.",
|
||||
"account_not_locked_warning": "Zure kontua ez dago {0}. Edozeinek jarraitzen hastearekin, zure mezuak irakur ditzake.",
|
||||
"account_not_locked_warning_link": "Blokeatuta",
|
||||
"attachments_sensitive": "Nabarmendu eranskinak hunkigarri gisa ",
|
||||
"content_type": {
|
||||
|
@ -136,10 +136,10 @@
|
|||
"unlisted": "Mezu hau ez da argitaratuko Denbora-lerro Publikoan ezta Ezagutzen den Sarean"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "Zuzena - Bidali aipatutako erabiltzaileei besterik ez",
|
||||
"private": "Jarraitzaileentzako bakarrik- Bidali jarraitzaileentzat bakarrik",
|
||||
"public": "Publickoa - Bistaratu denbora-lerro publikoetan",
|
||||
"unlisted": "Zerrendatu gabea - ez bidali denbora-lerro publikoetan"
|
||||
"direct": "Zuzena: Bidali aipatutako erabiltzaileei besterik ez",
|
||||
"private": "Jarraitzaileentzako bakarrik: Bidali jarraitzaileentzat bakarrik",
|
||||
"public": "Publikoa: Bistaratu denbora-lerro publikoetan",
|
||||
"unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
|
@ -228,7 +228,7 @@
|
|||
"avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.",
|
||||
"export_theme": "Gorde aurre-ezarpena",
|
||||
"filtering": "Iragazten",
|
||||
"filtering_explanation": "Hitz hauek dituzten muzu guztiak isilduak izango dira. Lerro bakoitzeko bat",
|
||||
"filtering_explanation": "Hitz hauek dituzten mezu guztiak isilduak izango dira. Lerro bakoitzeko bat",
|
||||
"follow_export": "Jarraitzen dituzunak esportatu",
|
||||
"follow_export_button": "Esportatu zure jarraitzaileak csv fitxategi batean",
|
||||
"follow_import": "Jarraitzen dituzunak inportatu",
|
||||
|
@ -276,7 +276,7 @@
|
|||
"no_blocks": "Ez daude erabiltzaile blokeatutak",
|
||||
"no_mutes": "Ez daude erabiltzaile mututuak",
|
||||
"hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen",
|
||||
"hide_followers_description": "Ez erakutsi nor ari de ni jarraitzen",
|
||||
"hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen",
|
||||
"show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan",
|
||||
"show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan",
|
||||
"nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko",
|
||||
|
@ -456,8 +456,8 @@
|
|||
"time": {
|
||||
"day": "{0} egun",
|
||||
"days": "{0} egun",
|
||||
"day_short": "{0}d",
|
||||
"days_short": "{0}d",
|
||||
"day_short": "{0}e",
|
||||
"days_short": "{0}e",
|
||||
"hour": "{0} ordu",
|
||||
"hours": "{0} ordu",
|
||||
"hour_short": "{0}o",
|
||||
|
@ -492,7 +492,7 @@
|
|||
"conversation": "Elkarrizketa",
|
||||
"error_fetching": "Errorea eguneraketak eskuratzen",
|
||||
"load_older": "Kargatu mezu zaharragoak",
|
||||
"no_retweet_hint": "Mezu hau jarraitzailentzko bakarrik markatuta dago eta ezin da errepikatu",
|
||||
"no_retweet_hint": "Mezu hau jarraitzailentzako bakarrik markatuta dago eta ezin da errepikatu",
|
||||
"repeated": "Errepikatuta",
|
||||
"show_new": "Berriena erakutsi",
|
||||
"up_to_date": "Eguneratuta",
|
||||
|
@ -507,8 +507,10 @@
|
|||
"unpin": "Aingura ezeztatu profilatik",
|
||||
"pinned": "Ainguratuta",
|
||||
"delete_confirm": "Mezu hau benetan ezabatu nahi duzu?",
|
||||
"reply_to": "Erantzun",
|
||||
"replies_list": "Erantzunak:"
|
||||
"reply_to": "Erantzuten",
|
||||
"replies_list": "Erantzunak:",
|
||||
"mute_conversation": "Elkarrizketa isilarazi",
|
||||
"unmute_conversation": "Elkarrizketa aktibatu"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Onartu",
|
||||
|
@ -581,7 +583,7 @@
|
|||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "Multimedia igo",
|
||||
"repeat": "Erreplikatu",
|
||||
"repeat": "Errepikatu",
|
||||
"reply": "Erantzun",
|
||||
"favorite": "Gogokoa",
|
||||
"user_settings": "Erabiltzaile ezarpenak"
|
||||
|
@ -601,10 +603,21 @@
|
|||
}
|
||||
},
|
||||
"search": {
|
||||
"people": "Gendea",
|
||||
"people": "Erabiltzaileak",
|
||||
"hashtags": "Traolak",
|
||||
"person_talking": "{count} pertsona hitzegiten",
|
||||
"people_talking": "{count} gende hitzegiten",
|
||||
"people_talking": "{count} jende hitzegiten",
|
||||
"no_results": "Emaitzarik ez"
|
||||
},
|
||||
"password_reset": {
|
||||
"forgot_password": "Pasahitza ahaztua?",
|
||||
"password_reset": "Pasahitza berrezarri",
|
||||
"instruction": "Idatzi zure helbide elektronikoa edo erabiltzaile izena. Pasahitza berrezartzeko esteka bidaliko dizugu.",
|
||||
"placeholder": "Zure e-posta edo erabiltzaile izena",
|
||||
"check_email": "Begiratu zure posta elektronikoa pasahitza berrezarri ahal izateko.",
|
||||
"return_home": "Itzuli hasierara",
|
||||
"not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.",
|
||||
"too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.",
|
||||
"password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin."
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
"exporter": {
|
||||
"export": "Exportar",
|
||||
"processing": "Tractament, vos demandarem lèu de telecargar lo fichièr"
|
||||
},
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "Chat",
|
||||
"gopher": "Gopher",
|
||||
|
@ -30,12 +30,12 @@
|
|||
"cancel": "Anullar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Talhar l’imatge",
|
||||
"save": "Salvar",
|
||||
"save_without_cropping": "Salvar sens talhada",
|
||||
"cancel": "Anullar"
|
||||
"crop_picture": "Talhar l’imatge",
|
||||
"save": "Salvar",
|
||||
"save_without_cropping": "Salvar sens talhada",
|
||||
"cancel": "Anullar"
|
||||
},
|
||||
"importer": {
|
||||
"importer": {
|
||||
"submit": "Mandar",
|
||||
"success": "Corrèctament importat.",
|
||||
"error": "Una error s’es producha pendent l’importacion d’aqueste fichièr."
|
||||
|
@ -65,6 +65,7 @@
|
|||
"timeline": "Flux d’actualitat",
|
||||
"twkn": "Lo malhum conegut",
|
||||
"user_search": "Cèrca d’utilizaires",
|
||||
"search": "Cercar",
|
||||
"who_to_follow": "Qual seguir",
|
||||
"preferences": "Preferéncias"
|
||||
},
|
||||
|
@ -79,19 +80,27 @@
|
|||
"no_more_notifications": "Pas mai de notificacions"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Ajustar un sondatge",
|
||||
"add_poll": "Ajustar un sondatge",
|
||||
"add_option": "Ajustar d’opcions",
|
||||
"option": "Opcion",
|
||||
"votes": "vòtes",
|
||||
"vote": "Votar",
|
||||
"type": "Tipe de sondatge",
|
||||
"single_choice": "Causida unica",
|
||||
"multiple_choices": "Causida multipla",
|
||||
"expiry": "Durada del sondatge",
|
||||
"expires_in": "Lo sondatge s’acabarà {0}",
|
||||
"expired": "Sondatge acabat {0}",
|
||||
"not_enough_options": "I a pas pro d’opcions"
|
||||
},
|
||||
"option": "Opcion",
|
||||
"votes": "vòtes",
|
||||
"vote": "Votar",
|
||||
"type": "Tipe de sondatge",
|
||||
"single_choice": "Causida unica",
|
||||
"multiple_choices": "Causida multipla",
|
||||
"expiry": "Durada del sondatge",
|
||||
"expires_in": "Lo sondatge s’acabarà {0}",
|
||||
"expired": "Sondatge acabat {0}",
|
||||
"not_enough_options": "I a pas pro d’opcions"
|
||||
},
|
||||
"stickers": {
|
||||
"add_sticker": "Ajustar un pegasolet"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Repeticions e favorits",
|
||||
"follows": "Nòus seguidors",
|
||||
"load_older": "Cargar d’interaccions anterioras"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Publicar d’estatuts novèls",
|
||||
"account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu’a vòstres seguidors.",
|
||||
|
@ -137,8 +146,8 @@
|
|||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "O seleccionar tot"
|
||||
},
|
||||
"select_all": "O seleccionar tot"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Nom de l’aplicacion",
|
||||
"attachmentRadius": "Pèças juntas",
|
||||
|
@ -216,7 +225,6 @@
|
|||
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
|
||||
"name": "Nom",
|
||||
"name_bio": "Nom & Bio",
|
||||
|
||||
"new_password": "Nòu senhal",
|
||||
"notification_visibility_follows": "Abonaments",
|
||||
"notification_visibility_likes": "Aimar",
|
||||
|
@ -264,12 +272,12 @@
|
|||
"subject_line_email": "Coma los corrièls : \"re: subjècte\"",
|
||||
"subject_line_mastodon": "Coma mastodon : copiar tal coma es",
|
||||
"subject_line_noop": "Copiar pas",
|
||||
"post_status_content_type": "Publicar lo tipe de contengut dels estatuts",
|
||||
"post_status_content_type": "Publicar lo tipe de contengut dels estatuts",
|
||||
"stop_gifs": "Lançar los GIFs al subrevòl",
|
||||
"streaming": "Activar lo cargament automatic dels novèls estatus en anar amont",
|
||||
"text": "Tèxte",
|
||||
"theme": "Tèma",
|
||||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||
"theme_help_v2_1": "Podètz tanben remplaçar la color d’unes compausants en clicant la case, utilizatz lo boton \"O escafar tot\" per escafar totes las subrecargadas.",
|
||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||
"theme_help": "Emplegatz los còdis de color hex (#rrggbb) per personalizar vòstre tèma de color.",
|
||||
"tooltipRadius": "Astúcias/alèrtas",
|
||||
|
@ -280,14 +288,14 @@
|
|||
"true": "òc"
|
||||
},
|
||||
"notifications": "Notificacions",
|
||||
"notification_setting": "Receber las notificacions de :",
|
||||
"notification_setting": "Recebre las notificacions de :",
|
||||
"notification_setting_follows": "Utilizaires que seguissètz",
|
||||
"notification_setting_non_follows": "Utilizaires que seguissètz pas",
|
||||
"notification_setting_followers": "Utilizaires que vos seguisson",
|
||||
"notification_setting_non_followers": "Utilizaires que vos seguisson pas",
|
||||
"notification_mutes": "Per receber pas mai d’un utilizaire en particular, botatz-lo en silenci.",
|
||||
"notification_mutes": "Per recebre pas mai d’un utilizaire en particular, botatz-lo en silenci.",
|
||||
"notification_blocks": "Blocar un utilizaire arrèsta totas las notificacions tan coma quitar de los seguir.",
|
||||
"enable_web_push_notifications": "Activar las notificacions web push",
|
||||
"enable_web_push_notifications": "Activar las notificacions web push",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Gardar las colors",
|
||||
|
@ -442,7 +450,7 @@
|
|||
"conversation": "Conversacion",
|
||||
"error_fetching": "Error en cercant de mesas a jorn",
|
||||
"load_older": "Ne veire mai",
|
||||
"no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir",
|
||||
"no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir",
|
||||
"repeated": "repetit",
|
||||
"show_new": "Ne veire mai",
|
||||
"up_to_date": "A jorn",
|
||||
|
@ -477,6 +485,8 @@
|
|||
"per_day": "per jorn",
|
||||
"remote_follow": "Seguir a distància",
|
||||
"statuses": "Estatuts",
|
||||
"subscribe": "S’abonar",
|
||||
"unsubscribe": "Se desabonar",
|
||||
"unblock": "Desblocar",
|
||||
"unblock_progress": "Desblocatge...",
|
||||
"block_progress": "Blocatge...",
|
||||
|
@ -501,7 +511,7 @@
|
|||
"quarantine": "Defendre la federacion de las publicacions de l’utilizaire",
|
||||
"delete_user": "Suprimir l’utilizaire",
|
||||
"delete_user_confirmation": "Volètz vertadièrament far aquò ? Aquesta accion se pòt pas anullar."
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Flux utilizaire",
|
||||
|
@ -532,5 +542,12 @@
|
|||
"GiB": "Gio",
|
||||
"TiB": "Tio"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"people": "Gent",
|
||||
"hashtags": "Etiquetas",
|
||||
"person_talking": "{count} persona ne parla",
|
||||
"people_talking": "{count} personas ne parlan",
|
||||
"no_results": "Cap de resultats"
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import mediaViewerModule from './modules/media_viewer.js'
|
|||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
import pollsModule from './modules/polls.js'
|
||||
import postStatusModule from './modules/postStatus.js'
|
||||
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
|
@ -26,6 +27,7 @@ import messages from './i18n/messages.js'
|
|||
import VueChatScroll from 'vue-chat-scroll'
|
||||
import VueClickOutside from 'v-click-outside'
|
||||
import PortalVue from 'portal-vue'
|
||||
import VBodyScrollLock from './directives/body_scroll_lock'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
@ -38,6 +40,7 @@ Vue.use(VueI18n)
|
|||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(VBodyScrollLock)
|
||||
Vue.use(VTooltip)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
|
@ -76,7 +79,8 @@ const persistedStateOptions = {
|
|||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule,
|
||||
reports: reportsModule,
|
||||
polls: pollsModule
|
||||
polls: pollsModule,
|
||||
postStatus: postStatusModule
|
||||
},
|
||||
plugins: [persistedState, pushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
|
|
|
@ -7,6 +7,7 @@ const defaultState = {
|
|||
colors: {},
|
||||
hideMutedPosts: undefined, // instance default
|
||||
collapseMessageWithSubject: undefined, // instance default
|
||||
padEmoji: true,
|
||||
hideAttachments: false,
|
||||
hideAttachmentsInConv: false,
|
||||
maxThumbnails: 16,
|
||||
|
|
25
src/modules/postStatus.js
Normal file
25
src/modules/postStatus.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const postStatus = {
|
||||
state: {
|
||||
params: null,
|
||||
modalActivated: false
|
||||
},
|
||||
mutations: {
|
||||
openPostStatusModal (state, params) {
|
||||
state.params = params
|
||||
state.modalActivated = true
|
||||
},
|
||||
closePostStatusModal (state) {
|
||||
state.modalActivated = false
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
openPostStatusModal ({ commit }, params) {
|
||||
commit('openPostStatusModal', params)
|
||||
},
|
||||
closePostStatusModal ({ commit }) {
|
||||
commit('closePostStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default postStatus
|
|
@ -426,9 +426,13 @@ export const mutations = {
|
|||
newStatus.favoritedBy.push(user)
|
||||
}
|
||||
},
|
||||
setMuted (state, status) {
|
||||
setMutedStatus (state, status) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.muted = status.muted
|
||||
newStatus.thread_muted = status.thread_muted
|
||||
|
||||
if (newStatus.thread_muted !== undefined) {
|
||||
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted })
|
||||
}
|
||||
},
|
||||
setRetweeted (state, { status, value }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
|
@ -566,11 +570,11 @@ const statuses = {
|
|||
},
|
||||
muteConversation ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.muteConversation(statusId)
|
||||
.then((status) => commit('setMuted', status))
|
||||
.then((status) => commit('setMutedStatus', status))
|
||||
},
|
||||
unmuteConversation ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.unmuteConversation(statusId)
|
||||
.then((status) => commit('setMuted', status))
|
||||
.then((status) => commit('setMutedStatus', status))
|
||||
},
|
||||
retweet ({ rootState, commit }, status) {
|
||||
// Optimistic retweeting...
|
||||
|
|
|
@ -10,6 +10,11 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
|||
const args = { credentials }
|
||||
const rootState = store.rootState || store.state
|
||||
const timelineData = rootState.statuses.notifications
|
||||
const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined'
|
||||
? rootState.instance.hideMutedPosts
|
||||
: rootState.config.hideMutedPosts
|
||||
|
||||
args['withMuted'] = !hideMutedPosts
|
||||
|
||||
args['timeline'] = 'notifications'
|
||||
if (older) {
|
||||
|
|
31
src/services/offset_finder/offset_finder.service.js
Normal file
31
src/services/offset_finder/offset_finder.service.js
Normal file
|
@ -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 }
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue