Merge remote-tracking branch 'upstream/develop' into neckbeard
This commit is contained in:
commit
5cfd544689
|
@ -0,0 +1,25 @@
|
||||||
|
# Environment info
|
||||||
|
|
||||||
|
<!-- Everything is optional and where applicable but the more information the better. -->
|
||||||
|
* Browser, version, OS, platform:
|
||||||
|
* Instance URL:
|
||||||
|
* Frontend version (see settings -> about):
|
||||||
|
* Backend version (see settings -> about):
|
||||||
|
* Browser extensions (ublock, rikaichamp etc):
|
||||||
|
* Known instance/user customizations (i.e. pleromafe mods/forks, instance styles etc)
|
||||||
|
|
||||||
|
# Bug description & reproduction steps
|
||||||
|
|
||||||
|
<!-- Type out here how to reproduce the bug, what goes wrong and what should go right -->
|
||||||
|
<!-- Screenshots and videos help a lot ;) any observations might also help -->
|
||||||
|
<!-- Also mention if there any errors in browser's console if relevant -->
|
||||||
|
|
||||||
|
# Bug seriousness
|
||||||
|
|
||||||
|
<!-- Everything is optional and free-form -->
|
||||||
|
* How annoying it is:
|
||||||
|
* How often does it happen:
|
||||||
|
* How many people does it affect:
|
||||||
|
* Is there a workaround for it:
|
||||||
|
|
||||||
|
/label ~Bug
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Behavior suggestion/Feature request
|
||||||
|
<!--
|
||||||
|
Type out what you want to see changed or what feature you want to see added to
|
||||||
|
PleormaFE. Please also explain how it would benefit users (or admins/moderators)
|
||||||
|
and what intended usecase is. Any background information (i.e. porting behavior
|
||||||
|
from other frontends/services, specific situations, personal preferences etc.)
|
||||||
|
as well as examples would be greatly appreciated.
|
||||||
|
-->
|
||||||
|
|
||||||
|
/label ~suggestion
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<!--
|
||||||
|
please use one of the templates if applicable, otherwise - type out here
|
||||||
|
in free-form
|
||||||
|
-->
|
||||||
|
|
||||||
|
/label ~needs-triage
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!--
|
||||||
|
Feel free to submit merge requests that are work-in-progress, but mark them as
|
||||||
|
Draft: or WIP:.
|
||||||
|
Merge requests that have Draft or WIP status will not be merged and have less chances
|
||||||
|
of being reviewed, but you can still ask people to take a look if you need advice.
|
||||||
|
-->
|
||||||
|
# Changes
|
||||||
|
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
|
||||||
|
<!-- List what your merge request changes and how -->
|
||||||
|
<!--
|
||||||
|
Try to not to break existing behavior, if your changes do break existing behavior
|
||||||
|
make it configurable to toggle between old behavior and new. Which one should be
|
||||||
|
default is up to discussion.
|
||||||
|
-->
|
||||||
|
<!-- If your merge request resolves some issue link it like so: "Closes #99999" -->
|
||||||
|
<!--
|
||||||
|
If merge request adds some new feature that depends on backend:
|
||||||
|
|
||||||
|
1. Make sure it gracefully degrades if backend hasn't been updated to support the feature,
|
||||||
|
we try to make PleromaFE compatible with older versions of BE so that people can still
|
||||||
|
update frontend safely without updating backend since it's costly and much riskier.
|
||||||
|
2. Link related BE merge request here
|
||||||
|
-->
|
||||||
|
<!-- Screenshots are welcome -->
|
||||||
|
|
||||||
|
/label ~needs-review
|
|
@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||||
- Timelines/panels and conversations have sticky headers now
|
- Timelines/panels and conversations have sticky headers now
|
||||||
- Added frontend ui for account migration
|
- Added frontend ui for account migration
|
||||||
|
- Implemented remote interaction with statuses
|
||||||
|
|
||||||
|
|
||||||
## [2.4.2] - 2022-01-09
|
## [2.4.2] - 2022-01-09
|
||||||
|
|
|
@ -430,5 +430,6 @@
|
||||||
The moral of the story is in order to access Bae.st, you need to enable JavaScript.</noscript>
|
The moral of the story is in order to access Bae.st, you need to enable JavaScript.</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
<div id="popovers" />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -74,7 +74,6 @@
|
||||||
<UpdateNotification />
|
<UpdateNotification />
|
||||||
<div id="modal" />
|
<div id="modal" />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
<div id="popovers" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -27,16 +27,16 @@
|
||||||
&.nativeColor {
|
&.nativeColor {
|
||||||
flex: 0 0 2em;
|
flex: 0 0 2em;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
align-self: center;
|
align-self: stretch;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.computedIndicator,
|
.computedIndicator,
|
||||||
.transparentIndicator {
|
.transparentIndicator {
|
||||||
flex: 0 0 2em;
|
flex: 0 0 2em;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
align-self: center;
|
align-self: stretch;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
.transparentIndicator {
|
.transparentIndicator {
|
||||||
// forgot to install counter-strike source, ooops
|
// forgot to install counter-strike source, ooops
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
@ -109,18 +110,20 @@ const EmojiInput = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
input: undefined,
|
input: undefined,
|
||||||
|
caretEl: undefined,
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
caret: 0,
|
caret: 0,
|
||||||
focused: false,
|
focused: false,
|
||||||
blurTimeout: null,
|
blurTimeout: null,
|
||||||
showPicker: false,
|
|
||||||
temporarilyHideSuggestions: false,
|
temporarilyHideSuggestions: false,
|
||||||
keepOpen: false,
|
|
||||||
disableClickOutside: false,
|
disableClickOutside: false,
|
||||||
suggestions: []
|
suggestions: [],
|
||||||
|
overlayStyle: {},
|
||||||
|
pickerShown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
Popover,
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
UnicodeDomainIndicator
|
UnicodeDomainIndicator
|
||||||
},
|
},
|
||||||
|
@ -128,15 +131,21 @@ const EmojiInput = {
|
||||||
padEmoji () {
|
padEmoji () {
|
||||||
return this.$store.getters.mergedConfig.padEmoji
|
return this.$store.getters.mergedConfig.padEmoji
|
||||||
},
|
},
|
||||||
|
preText () {
|
||||||
|
return this.modelValue.slice(0, this.caret)
|
||||||
|
},
|
||||||
|
postText () {
|
||||||
|
return this.modelValue.slice(this.caret)
|
||||||
|
},
|
||||||
showSuggestions () {
|
showSuggestions () {
|
||||||
return this.focused &&
|
return this.focused &&
|
||||||
this.suggestions &&
|
this.suggestions &&
|
||||||
this.suggestions.length > 0 &&
|
this.suggestions.length > 0 &&
|
||||||
!this.showPicker &&
|
!this.pickerShown &&
|
||||||
!this.temporarilyHideSuggestions
|
!this.temporarilyHideSuggestions
|
||||||
},
|
},
|
||||||
textAtCaret () {
|
textAtCaret () {
|
||||||
return (this.wordAtCaret || {}).word || ''
|
return this.wordAtCaret?.word
|
||||||
},
|
},
|
||||||
wordAtCaret () {
|
wordAtCaret () {
|
||||||
if (this.modelValue && this.caret) {
|
if (this.modelValue && this.caret) {
|
||||||
|
@ -188,13 +197,35 @@ const EmojiInput = {
|
||||||
|
|
||||||
return emoji.displayText
|
return emoji.displayText
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onInputScroll () {
|
||||||
|
this.$refs.hiddenOverlay.scrollTo({
|
||||||
|
top: this.input.scrollTop,
|
||||||
|
left: this.input.scrollLeft
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
const { root } = this.$refs
|
const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
|
||||||
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||||
if (!input) return
|
if (!input) return
|
||||||
this.input = input
|
this.input = input
|
||||||
|
this.caretEl = hiddenOverlayCaret
|
||||||
|
if (suggestorPopover.setAnchorEl) {
|
||||||
|
suggestorPopover.setAnchorEl(this.caretEl) // unit test compat
|
||||||
|
this.$refs.picker.setAnchorEl(this.caretEl)
|
||||||
|
} else {
|
||||||
|
console.warn('setAnchorEl not found, are we in a unit test?')
|
||||||
|
}
|
||||||
|
const style = getComputedStyle(this.input)
|
||||||
|
this.overlayStyle.padding = style.padding
|
||||||
|
this.overlayStyle.border = style.border
|
||||||
|
this.overlayStyle.margin = style.margin
|
||||||
|
this.overlayStyle.lineHeight = style.lineHeight
|
||||||
|
this.overlayStyle.fontFamily = style.fontFamily
|
||||||
|
this.overlayStyle.fontSize = style.fontSize
|
||||||
|
this.overlayStyle.wordWrap = style.wordWrap
|
||||||
|
this.overlayStyle.whiteSpace = style.whiteSpace
|
||||||
this.resize()
|
this.resize()
|
||||||
input.addEventListener('blur', this.onBlur)
|
input.addEventListener('blur', this.onBlur)
|
||||||
input.addEventListener('focus', this.onFocus)
|
input.addEventListener('focus', this.onFocus)
|
||||||
|
@ -204,6 +235,7 @@ const EmojiInput = {
|
||||||
input.addEventListener('click', this.onClickInput)
|
input.addEventListener('click', this.onClickInput)
|
||||||
input.addEventListener('transitionend', this.onTransition)
|
input.addEventListener('transitionend', this.onTransition)
|
||||||
input.addEventListener('input', this.onInput)
|
input.addEventListener('input', this.onInput)
|
||||||
|
input.addEventListener('scroll', this.onInputScroll)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
|
@ -216,45 +248,43 @@ const EmojiInput = {
|
||||||
input.removeEventListener('click', this.onClickInput)
|
input.removeEventListener('click', this.onClickInput)
|
||||||
input.removeEventListener('transitionend', this.onTransition)
|
input.removeEventListener('transitionend', this.onTransition)
|
||||||
input.removeEventListener('input', this.onInput)
|
input.removeEventListener('input', this.onInput)
|
||||||
|
input.removeEventListener('scroll', this.onInputScroll)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showSuggestions: function (newValue) {
|
showSuggestions: function (newValue, oldValue) {
|
||||||
this.$emit('shown', newValue)
|
this.$emit('shown', newValue)
|
||||||
|
if (newValue) {
|
||||||
|
this.$refs.suggestorPopover.showPopover()
|
||||||
|
} else {
|
||||||
|
this.$refs.suggestorPopover.hidePopover()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
textAtCaret: async function (newWord) {
|
textAtCaret: async function (newWord) {
|
||||||
|
if (newWord === undefined) return
|
||||||
const firstchar = newWord.charAt(0)
|
const firstchar = newWord.charAt(0)
|
||||||
|
if (newWord === firstchar) {
|
||||||
this.suggestions = []
|
this.suggestions = []
|
||||||
if (newWord === firstchar) return
|
return
|
||||||
|
}
|
||||||
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
|
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
|
||||||
// Async: cancel if textAtCaret has changed during wait
|
// Async: cancel if textAtCaret has changed during wait
|
||||||
if (this.textAtCaret !== newWord) return
|
if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
|
||||||
if (matchedSuggestions.length <= 0) return
|
this.suggestions = []
|
||||||
|
return
|
||||||
|
}
|
||||||
this.suggestions = take(matchedSuggestions, 5)
|
this.suggestions = take(matchedSuggestions, 5)
|
||||||
.map(({ imageUrl, ...rest }) => ({
|
.map(({ imageUrl, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
img: imageUrl || ''
|
img: imageUrl || ''
|
||||||
}))
|
}))
|
||||||
},
|
|
||||||
suggestions: {
|
|
||||||
handler (newValue) {
|
|
||||||
this.$nextTick(this.resize)
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusPickerInput () {
|
|
||||||
const pickerEl = this.$refs.picker.$el
|
|
||||||
if (!pickerEl) return
|
|
||||||
const pickerInput = pickerEl.querySelector('input')
|
|
||||||
if (pickerInput) pickerInput.focus()
|
|
||||||
},
|
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
this.$refs.picker.showPicker()
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
|
||||||
})
|
})
|
||||||
// This temporarily disables "click outside" handler
|
// This temporarily disables "click outside" handler
|
||||||
// since external trigger also means click originates
|
// since external trigger also means click originates
|
||||||
|
@ -266,11 +296,12 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
togglePicker () {
|
togglePicker () {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
this.showPicker = !this.showPicker
|
if (!this.pickerShown) {
|
||||||
if (this.showPicker) {
|
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
|
this.$refs.picker.showPicker()
|
||||||
this.$refs.picker.startEmojiLoad()
|
this.$refs.picker.startEmojiLoad()
|
||||||
this.$nextTick(this.focusPickerInput)
|
} else {
|
||||||
|
this.$refs.picker.hidePicker()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
replace (replacement) {
|
replace (replacement) {
|
||||||
|
@ -307,7 +338,6 @@ const EmojiInput = {
|
||||||
spaceAfter,
|
spaceAfter,
|
||||||
after
|
after
|
||||||
].join('')
|
].join('')
|
||||||
this.keepOpen = keepOpen
|
|
||||||
this.$emit('update:modelValue', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
|
@ -407,8 +437,11 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onTransition (e) {
|
onPickerShown () {
|
||||||
this.resize()
|
this.pickerShown = true
|
||||||
|
},
|
||||||
|
onPickerClosed () {
|
||||||
|
this.pickerShown = false
|
||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur (e) {
|
||||||
// Clicking on any suggestion removes focus from autocomplete,
|
// Clicking on any suggestion removes focus from autocomplete,
|
||||||
|
@ -416,7 +449,6 @@ const EmojiInput = {
|
||||||
this.blurTimeout = setTimeout(() => {
|
this.blurTimeout = setTimeout(() => {
|
||||||
this.focused = false
|
this.focused = false
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
onClick (e, suggestion) {
|
onClick (e, suggestion) {
|
||||||
|
@ -428,18 +460,13 @@ const EmojiInput = {
|
||||||
this.blurTimeout = null
|
this.blurTimeout = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.keepOpen) {
|
|
||||||
this.showPicker = false
|
|
||||||
}
|
|
||||||
this.focused = true
|
this.focused = true
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
|
||||||
this.temporarilyHideSuggestions = false
|
this.temporarilyHideSuggestions = false
|
||||||
},
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
const { key } = e
|
const { key } = e
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
|
||||||
|
|
||||||
// Setting hider in keyUp to prevent suggestions from blinking
|
// Setting hider in keyUp to prevent suggestions from blinking
|
||||||
// when moving away from suggested spot
|
// when moving away from suggested spot
|
||||||
|
@ -451,7 +478,6 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
onPaste (e) {
|
onPaste (e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
|
||||||
},
|
},
|
||||||
onKeyDown (e) {
|
onKeyDown (e) {
|
||||||
const { ctrlKey, shiftKey, key } = e
|
const { ctrlKey, shiftKey, key } = e
|
||||||
|
@ -496,58 +522,24 @@ const EmojiInput = {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showPicker = false
|
|
||||||
this.resize()
|
|
||||||
},
|
},
|
||||||
onInput (e) {
|
onInput (e) {
|
||||||
this.showPicker = false
|
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
|
||||||
this.$emit('update:modelValue', e.target.value)
|
this.$emit('update:modelValue', e.target.value)
|
||||||
},
|
},
|
||||||
onClickInput (e) {
|
|
||||||
this.showPicker = false
|
|
||||||
},
|
|
||||||
onClickOutside (e) {
|
|
||||||
if (this.disableClickOutside) return
|
|
||||||
this.showPicker = false
|
|
||||||
},
|
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
this.showPicker = false
|
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
onStickerUploadFailed (e) {
|
onStickerUploadFailed (e) {
|
||||||
this.showPicker = false
|
|
||||||
this.$emit('sticker-upload-Failed', e)
|
this.$emit('sticker-upload-Failed', e)
|
||||||
},
|
},
|
||||||
setCaret ({ target: { selectionStart } }) {
|
setCaret ({ target: { selectionStart } }) {
|
||||||
this.caret = selectionStart
|
this.caret = selectionStart
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.suggestorPopover.updateStyles()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
resize () {
|
resize () {
|
||||||
const panel = this.$refs.panel
|
|
||||||
if (!panel) return
|
|
||||||
const picker = this.$refs.picker.$el
|
|
||||||
const panelBody = this.$refs['panel-body']
|
|
||||||
const { offsetHeight, offsetTop } = this.input
|
|
||||||
const offsetBottom = offsetTop + offsetHeight
|
|
||||||
|
|
||||||
this.setPlacement(panelBody, panel, offsetBottom)
|
|
||||||
this.setPlacement(picker, picker, offsetBottom)
|
|
||||||
},
|
|
||||||
setPlacement (container, target, offsetBottom) {
|
|
||||||
if (!container || !target) return
|
|
||||||
|
|
||||||
target.style.top = offsetBottom + 'px'
|
|
||||||
target.style.bottom = 'auto'
|
|
||||||
|
|
||||||
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
|
|
||||||
target.style.top = 'auto'
|
|
||||||
target.style.bottom = this.input.offsetHeight + 'px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
overflowsBottom (el) {
|
|
||||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="root"
|
ref="root"
|
||||||
v-click-outside="onClickOutside"
|
|
||||||
class="emoji-input"
|
class="emoji-input"
|
||||||
:class="{ 'with-picker': !hideEmojiButton }"
|
:class="{ 'with-picker': !hideEmojiButton }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
<!-- TODO: make the 'x' disappear if at the end maybe? -->
|
||||||
|
<div
|
||||||
|
ref="hiddenOverlay"
|
||||||
|
class="hidden-overlay"
|
||||||
|
:style="overlayStyle"
|
||||||
|
>
|
||||||
|
<span>{{ preText }}</span>
|
||||||
|
<span
|
||||||
|
ref="hiddenOverlayCaret"
|
||||||
|
class="caret"
|
||||||
|
>x</span>
|
||||||
|
<span>{{ postText }}</span>
|
||||||
|
</div>
|
||||||
<template v-if="enableEmojiPicker">
|
<template v-if="enableEmojiPicker">
|
||||||
<button
|
<button
|
||||||
v-if="!hideEmojiButton"
|
v-if="!hideEmojiButton"
|
||||||
|
@ -18,20 +30,21 @@
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
:class="{ hide: !showPicker }"
|
|
||||||
:showing="showPicker"
|
|
||||||
:enable-sticker-picker="enableStickerPicker"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
class="emoji-picker-panel"
|
class="emoji-picker-panel"
|
||||||
@emoji="insert"
|
@emoji="insert"
|
||||||
@sticker-uploaded="onStickerUploaded"
|
@sticker-uploaded="onStickerUploaded"
|
||||||
@sticker-upload-failed="onStickerUploadFailed"
|
@sticker-upload-failed="onStickerUploadFailed"
|
||||||
|
@show="onPickerShown"
|
||||||
|
@close="onPickerClosed"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<Popover
|
||||||
ref="panel"
|
ref="suggestorPopover"
|
||||||
class="autocomplete-panel"
|
class="autocomplete-panel"
|
||||||
:class="{ hide: !showSuggestions }"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
|
<template #content>
|
||||||
<div
|
<div
|
||||||
ref="panel-body"
|
ref="panel-body"
|
||||||
class="autocomplete-panel-body"
|
class="autocomplete-panel-body"
|
||||||
|
@ -70,7 +83,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -102,6 +116,7 @@
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker-panel {
|
.emoji-picker-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
@ -112,34 +127,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete {
|
input, textarea {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-overlay {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
/* DEBUG STUFF */
|
||||||
|
color: red;
|
||||||
|
/* set opacity to non-zero to see the overlay */
|
||||||
|
|
||||||
|
.caret {
|
||||||
|
width: 0;
|
||||||
|
margin-right: calc(-1ch - 1px);
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.autocomplete {
|
||||||
&-panel {
|
&-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 20;
|
|
||||||
margin-top: 2px;
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
display: none
|
|
||||||
}
|
|
||||||
|
|
||||||
&-body {
|
|
||||||
margin: 0 0.5em 0 0.5em;
|
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
|
||||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
|
||||||
box-shadow: var(--popupShadow);
|
|
||||||
min-width: 75%;
|
|
||||||
background-color: $fallback--bg;
|
|
||||||
background-color: var(--popover, $fallback--bg);
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--popoverText, $fallback--link);
|
|
||||||
--faint: var(--popoverFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
|
||||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
|
||||||
--postLink: var(--popoverPostLink, $fallback--link);
|
|
||||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
|
||||||
--icon: var(--popoverIcon, $fallback--icon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
|
@ -191,10 +205,5 @@
|
||||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
input, textarea {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import StillImage from '../still-image/still-image.vue'
|
import StillImage from '../still-image/still-image.vue'
|
||||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||||
import lozad from 'lozad'
|
import lozad from 'lozad'
|
||||||
|
@ -87,10 +88,6 @@ const EmojiPicker = {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
|
||||||
showing: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -111,15 +108,32 @@ const EmojiPicker = {
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox,
|
Checkbox,
|
||||||
StillImage
|
StillImage,
|
||||||
|
Popover
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showPicker () {
|
||||||
|
this.$refs.popover.showPopover()
|
||||||
|
this.onShowing()
|
||||||
|
},
|
||||||
|
hidePicker () {
|
||||||
|
this.$refs.popover.hidePopover()
|
||||||
|
},
|
||||||
|
setAnchorEl (el) {
|
||||||
|
this.$refs.popover.setAnchorEl(el)
|
||||||
|
},
|
||||||
setGroupRef (name) {
|
setGroupRef (name) {
|
||||||
return el => { this.groupRefs[name] = el }
|
return el => { this.groupRefs[name] = el }
|
||||||
},
|
},
|
||||||
setEmojiRef (name) {
|
setEmojiRef (name) {
|
||||||
return el => { this.emojiRefs[name] = el }
|
return el => { this.emojiRefs[name] = el }
|
||||||
},
|
},
|
||||||
|
onPopoverShown () {
|
||||||
|
this.$emit('show')
|
||||||
|
},
|
||||||
|
onPopoverClosed () {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
|
@ -128,6 +142,9 @@ const EmojiPicker = {
|
||||||
},
|
},
|
||||||
onEmoji (emoji) {
|
onEmoji (emoji) {
|
||||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||||
|
if (!this.keepOpen) {
|
||||||
|
this.$refs.popover.hidePopover()
|
||||||
|
}
|
||||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||||
},
|
},
|
||||||
onScroll (e) {
|
onScroll (e) {
|
||||||
|
@ -223,6 +240,9 @@ const EmojiPicker = {
|
||||||
},
|
},
|
||||||
onShowing () {
|
onShowing () {
|
||||||
const oldContentLoaded = this.contentLoaded
|
const oldContentLoaded = this.contentLoaded
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.search.focus()
|
||||||
|
})
|
||||||
this.contentLoaded = true
|
this.contentLoaded = true
|
||||||
this.waitForDomAndInitializeLazyLoad()
|
this.waitForDomAndInitializeLazyLoad()
|
||||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
|
@ -251,16 +271,6 @@ const EmojiPicker = {
|
||||||
allCustomGroups () {
|
allCustomGroups () {
|
||||||
this.waitForDomAndInitializeLazyLoad()
|
this.waitForDomAndInitializeLazyLoad()
|
||||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
},
|
|
||||||
showing (val) {
|
|
||||||
if (val) {
|
|
||||||
this.onShowing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
if (this.showing) {
|
|
||||||
this.onShowing()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
|
|
|
@ -6,14 +6,10 @@ $emoji-picker-header-picture-height: 32px;
|
||||||
$emoji-picker-emoji-size: 32px;
|
$emoji-picker-emoji-size: 32px;
|
||||||
|
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
|
width: 25em;
|
||||||
|
max-width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
margin: 0 !important;
|
|
||||||
// TODO: actually use popover in emoji picker
|
|
||||||
z-index: var(--ZI_popovers);
|
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--popover, $fallback--bg);
|
background-color: var(--popover, $fallback--bg);
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Popover
|
||||||
class="emoji-picker panel panel-default panel-body"
|
ref="popover"
|
||||||
|
trigger="click"
|
||||||
|
popover-class="emoji-picker popover-default"
|
||||||
|
@show="onPopoverShown"
|
||||||
|
@close="onPopoverClosed"
|
||||||
>
|
>
|
||||||
|
<template #content>
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span
|
<span
|
||||||
ref="header"
|
ref="header"
|
||||||
|
@ -61,6 +66,7 @@
|
||||||
>
|
>
|
||||||
<div class="emoji-search">
|
<div class="emoji-search">
|
||||||
<input
|
<input
|
||||||
|
ref="search"
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@ -123,7 +129,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./emoji_picker.js"></script>
|
<script src="./emoji_picker.js"></script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="emoji-reactions">
|
<div class="EmojiReactions">
|
||||||
<UserListPopover
|
<UserListPopover
|
||||||
v-for="(reaction) in emojiReactions"
|
v-for="(reaction) in emojiReactions"
|
||||||
:key="reaction.name"
|
:key="reaction.name"
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="emoji-reaction btn button-default"
|
class="emoji-reaction btn button-default"
|
||||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||||
@click="emojiOnClick(reaction.name, $event)"
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||||
>
|
>
|
||||||
|
@ -30,13 +30,12 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.emoji-reactions {
|
.EmojiReactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-reaction {
|
.emoji-reaction {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
@ -44,10 +43,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.reaction-emoji {
|
.reaction-emoji {
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
@ -59,9 +60,15 @@
|
||||||
box-shadow: var(--buttonShadow);
|
box-shadow: var(--buttonShadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-reaction-expand {
|
&.-picked-reaction {
|
||||||
|
border: 1px solid var(--accent, $fallback--link);
|
||||||
|
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||||
|
margin-right: calc(0.5em - 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-reaction-expand {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
@ -71,12 +78,7 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.picked-reaction {
|
|
||||||
border: 1px solid var(--accent, $fallback--link);
|
|
||||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
|
||||||
margin-right: calc(0.5em - 1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -113,8 +113,7 @@ const ExtraButtons = {
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
canDelete () {
|
canDelete () {
|
||||||
if (!this.currentUser) { return }
|
if (!this.currentUser) { return }
|
||||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
return this.currentUser.privileges.includes('messages_delete') || this.status.user.id === this.currentUser.id
|
||||||
return superuser || this.status.user.id === this.currentUser.id
|
|
||||||
},
|
},
|
||||||
ownStatus () {
|
ownStatus () {
|
||||||
return this.status.user.id === this.currentUser.id
|
return this.status.user.id === this.currentUser.id
|
||||||
|
|
|
@ -39,7 +39,10 @@ const FavoriteButton = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig']),
|
||||||
|
remoteInteractionLink () {
|
||||||
|
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,13 +33,19 @@
|
||||||
/>
|
/>
|
||||||
</FALayers>
|
</FALayers>
|
||||||
</button>
|
</button>
|
||||||
<span v-else>
|
<a
|
||||||
|
v-else
|
||||||
|
class="button-unstyled interactive"
|
||||||
|
target="_blank"
|
||||||
|
role="button"
|
||||||
|
:href="remoteInteractionLink"
|
||||||
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
:title="$t('tool_tip.favorite')"
|
:title="$t('tool_tip.favorite')"
|
||||||
:icon="['far', 'star']"
|
:icon="['far', 'star']"
|
||||||
/>
|
/>
|
||||||
</span>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
||||||
class="action-counter"
|
class="action-counter"
|
||||||
|
|
|
@ -15,7 +15,7 @@ const Interactions = {
|
||||||
return {
|
return {
|
||||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||||
filterMode: tabModeDict.mentions,
|
filterMode: tabModeDict.mentions,
|
||||||
canSeeReports: ['moderator', 'admin'].includes(this.$store.state.users.currentUser.role)
|
canSeeReports: this.$store.state.users.currentUser.privileges.includes('reports_manage_reports')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<button
|
<button
|
||||||
class="button-unstyled mobile-nav-button"
|
class="button-unstyled mobile-nav-button"
|
||||||
:title="$t('nav.mobile_sidebar')"
|
:title="$t('nav.mobile_sidebar')"
|
||||||
:aria-expanaded="this.$refs.sideDrawer && !this.$refs.sideDrawer.closed"
|
:aria-expanaded="$refs.sideDrawer && !$refs.sideDrawer.closed"
|
||||||
@click.stop.prevent="toggleMobileSidebar()"
|
@click.stop.prevent="toggleMobileSidebar()"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
>
|
>
|
||||||
<div class="mobile-notifications-header">
|
<div class="mobile-notifications-header">
|
||||||
<span class="title">{{ $t('notifications.notifications') }}</span>
|
<span class="title">{{ $t('notifications.notifications') }}</span>
|
||||||
<span class="spacer"/>
|
<span class="spacer" />
|
||||||
<button
|
<button
|
||||||
v-if="notificationsAtTop"
|
v-if="notificationsAtTop"
|
||||||
class="button-unstyled mobile-nav-button"
|
class="button-unstyled mobile-nav-button"
|
||||||
|
@ -79,8 +79,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="mobile-notifications"
|
id="mobile-notifications"
|
||||||
class="mobile-notifications"
|
|
||||||
ref="mobileNotifications"
|
ref="mobileNotifications"
|
||||||
|
class="mobile-notifications"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
@ -41,14 +41,26 @@ const ModerationTools = {
|
||||||
tagsSet () {
|
tagsSet () {
|
||||||
return new Set(this.user.tags)
|
return new Set(this.user.tags)
|
||||||
},
|
},
|
||||||
hasTagPolicy () {
|
canGrantRole () {
|
||||||
return this.$store.state.instance.tagPolicyAvailable
|
return this.user.is_local && !this.user.deactivated && this.$store.state.users.currentUser.role === 'admin'
|
||||||
|
},
|
||||||
|
canChangeActivationState () {
|
||||||
|
return this.privileged('users_manage_activation_state')
|
||||||
|
},
|
||||||
|
canDeleteAccount () {
|
||||||
|
return this.privileged('users_delete')
|
||||||
|
},
|
||||||
|
canUseTagPolicy () {
|
||||||
|
return this.$store.state.instance.tagPolicyAvailable && this.privileged('users_manage_tags')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hasTag (tagName) {
|
hasTag (tagName) {
|
||||||
return this.tagsSet.has(tagName)
|
return this.tagsSet.has(tagName)
|
||||||
},
|
},
|
||||||
|
privileged (privilege) {
|
||||||
|
return this.$store.state.users.currentUser.privileges.includes(privilege)
|
||||||
|
},
|
||||||
toggleTag (tag) {
|
toggleTag (tag) {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
if (this.tagsSet.has(tag)) {
|
if (this.tagsSet.has(tag)) {
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<span v-if="user.is_local">
|
<span v-if="canGrantRole">
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="toggleRight("admin")"
|
@click="toggleRight("admin")"
|
||||||
|
@ -24,28 +24,31 @@
|
||||||
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
|
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
v-if="canChangeActivationState || canDeleteAccount"
|
||||||
role="separator"
|
role="separator"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
|
v-if="canChangeActivationState"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="toggleActivationStatus()"
|
@click="toggleActivationStatus()"
|
||||||
>
|
>
|
||||||
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="canDeleteAccount"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="deleteUserDialog(true)"
|
@click="deleteUserDialog(true)"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.admin_menu.delete_account') }}
|
{{ $t('user_card.admin_menu.delete_account') }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="hasTagPolicy"
|
v-if="canUseTagPolicy"
|
||||||
role="separator"
|
role="separator"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
<span v-if="hasTagPolicy">
|
<span v-if="canUseTagPolicy">
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="toggleTag(tags.FORCE_NSFW)"
|
@click="toggleTag(tags.FORCE_NSFW)"
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
&.router-link-active {
|
&.router-link-active {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--selectedMenuText, $fallback--text);
|
color: var(--panelText, $fallback--text);
|
||||||
border-bottom: 4px solid;
|
border-bottom: 4px solid;
|
||||||
|
|
||||||
& .svg-inline--fa,
|
& .svg-inline--fa,
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
>{{ unseenCount }}</span>
|
>{{ unseenCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rightside-button"
|
|
||||||
v-if="showScrollTop"
|
v-if="showScrollTop"
|
||||||
|
class="rightside-button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled scroll-to-top-button"
|
class="button-unstyled scroll-to-top-button"
|
||||||
|
|
|
@ -56,6 +56,10 @@ const Popover = {
|
||||||
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
|
// lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
|
||||||
// so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
|
// so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
|
||||||
// with popovers refusing to be hidden when user wants to interact with something in below popover
|
// with popovers refusing to be hidden when user wants to interact with something in below popover
|
||||||
|
anchorEl: null,
|
||||||
|
// There's an issue where having teleport enabled by default causes things just...
|
||||||
|
// not render at all, i.e. main post status form and its emoji inputs
|
||||||
|
teleport: false,
|
||||||
lockReEntry: false,
|
lockReEntry: false,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
styles: {},
|
styles: {},
|
||||||
|
@ -64,10 +68,15 @@ const Popover = {
|
||||||
// used to avoid blinking if hovered onto popover
|
// used to avoid blinking if hovered onto popover
|
||||||
graceTimeout: null,
|
graceTimeout: null,
|
||||||
parentPopover: null,
|
parentPopover: null,
|
||||||
|
disableClickOutside: false,
|
||||||
childrenShown: new Set()
|
childrenShown: new Set()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setAnchorEl (el) {
|
||||||
|
this.anchorEl = el
|
||||||
|
this.updateStyles()
|
||||||
|
},
|
||||||
containerBoundingClientRect () {
|
containerBoundingClientRect () {
|
||||||
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
|
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
|
||||||
return container.getBoundingClientRect()
|
return container.getBoundingClientRect()
|
||||||
|
@ -80,7 +89,7 @@ const Popover = {
|
||||||
|
|
||||||
// Popover will be anchored around this element, trigger ref is the container, so
|
// Popover will be anchored around this element, trigger ref is the container, so
|
||||||
// its children are what are inside the slot. Expect only one v-slot:trigger.
|
// its children are what are inside the slot. Expect only one v-slot:trigger.
|
||||||
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
const anchorEl = this.anchorEl || (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||||
// SVGs don't have offsetWidth/Height, use fallback
|
// SVGs don't have offsetWidth/Height, use fallback
|
||||||
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
|
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
|
||||||
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
||||||
|
@ -231,6 +240,10 @@ const Popover = {
|
||||||
},
|
},
|
||||||
showPopover () {
|
showPopover () {
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
|
this.disableClickOutside = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.disableClickOutside = false
|
||||||
|
}, 0)
|
||||||
const wasHidden = this.hidden
|
const wasHidden = this.hidden
|
||||||
this.hidden = false
|
this.hidden = false
|
||||||
this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
|
this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
|
||||||
|
@ -291,6 +304,7 @@ const Popover = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickOutside (e) {
|
onClickOutside (e) {
|
||||||
|
if (this.disableClickOutside) return
|
||||||
if (this.hidden) return
|
if (this.hidden) return
|
||||||
if (this.$refs.content && this.$refs.content.contains(e.target)) return
|
if (this.$refs.content && this.$refs.content.contains(e.target)) return
|
||||||
if (this.$el.contains(e.target)) return
|
if (this.$el.contains(e.target)) return
|
||||||
|
@ -324,6 +338,7 @@ const Popover = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.teleport = true
|
||||||
let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
|
let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
|
||||||
this.$refs.trigger.closest('.mobile-notifications')
|
this.$refs.trigger.closest('.mobile-notifications')
|
||||||
if (!scrollable) scrollable = window
|
if (!scrollable) scrollable = window
|
||||||
|
|
|
@ -12,7 +12,10 @@
|
||||||
>
|
>
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
</button>
|
</button>
|
||||||
<teleport to="#popovers">
|
<teleport
|
||||||
|
:disabled="!teleport"
|
||||||
|
to="#popovers"
|
||||||
|
>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="!hidden"
|
v-if="!hidden"
|
||||||
|
|
|
@ -501,7 +501,6 @@ const PostStatusForm = {
|
||||||
if (target.value === '') {
|
if (target.value === '') {
|
||||||
target.style.height = null
|
target.style.height = null
|
||||||
this.$emit('resize')
|
this.$emit('resize')
|
||||||
this.$refs['emoji-input'].resize()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,8 +587,6 @@ const PostStatusForm = {
|
||||||
} else {
|
} else {
|
||||||
scrollerRef.scrollTop = targetScroll
|
scrollerRef.scrollTop = targetScroll
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs['emoji-input'].resize()
|
|
||||||
},
|
},
|
||||||
showEmojiPicker () {
|
showEmojiPicker () {
|
||||||
this.$refs.textarea.focus()
|
this.$refs.textarea.focus()
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="QuickFilterSettings"
|
class="QuickFilterSettings"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
:triggerAttrs="{ title: $t('timeline.quick_filter_settings') }"
|
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="QuickViewSettings"
|
class="QuickViewSettings"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
:triggerAttrs="{ title: $t('timeline.quick_view_settings') }"
|
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
@ -43,31 +44,73 @@ const ReactButton = {
|
||||||
const input = this.$el.querySelector('input')
|
const input = this.$el.querySelector('input')
|
||||||
if (input) input.focus()
|
if (input) input.focus()
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// Vaguely adjusted copypaste from emoji_input and emoji_picker!
|
||||||
|
maybeLocalizedEmojiNamesAndKeywords (emoji) {
|
||||||
|
const names = [emoji.displayText]
|
||||||
|
const keywords = []
|
||||||
|
|
||||||
|
if (emoji.displayTextI18n) {
|
||||||
|
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.annotations) {
|
||||||
|
this.languages.forEach(lang => {
|
||||||
|
names.push(emoji.annotations[lang]?.name)
|
||||||
|
|
||||||
|
keywords.push(...(emoji.annotations[lang]?.keywords || []))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
names: names.filter(k => k),
|
||||||
|
keywords: keywords.filter(k => k)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeLocalizedEmojiName (emoji) {
|
||||||
|
if (!emoji.annotations) {
|
||||||
|
return emoji.displayText
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.displayTextI18n) {
|
||||||
|
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lang of this.languages) {
|
||||||
|
if (emoji.annotations[lang]?.name) {
|
||||||
|
return emoji.annotations[lang].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji.displayText
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
commonEmojis () {
|
commonEmojis () {
|
||||||
return [
|
const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥'])
|
||||||
{ displayText: 'thumbsup', replacement: '👍' },
|
return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement))
|
||||||
{ displayText: 'angry', replacement: '😠' },
|
},
|
||||||
{ displayText: 'eyes', replacement: '👀' },
|
languages () {
|
||||||
{ displayText: 'joy', replacement: '😂' },
|
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||||
{ displayText: 'fire', replacement: '🔥' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
emojis () {
|
emojis () {
|
||||||
if (this.filterWord !== '') {
|
if (this.filterWord !== '') {
|
||||||
const filterWordLowercase = trim(this.filterWord.toLowerCase())
|
const keywordLowercase = trim(this.filterWord.toLowerCase())
|
||||||
|
|
||||||
const orderedEmojiList = []
|
const orderedEmojiList = []
|
||||||
for (const emoji of this.$store.getters.standardEmojiList) {
|
for (const emoji of this.$store.getters.standardEmojiList) {
|
||||||
if (emoji.replacement === this.filterWord) return [emoji]
|
const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji)
|
||||||
|
.keywords
|
||||||
|
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
||||||
|
.filter(k => k > -1)
|
||||||
|
|
||||||
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
|
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
||||||
if (indexOfFilterWord > -1) {
|
|
||||||
if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {
|
if (indexOfKeyword > -1) {
|
||||||
orderedEmojiList[indexOfFilterWord] = []
|
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||||
|
orderedEmojiList[indexOfKeyword] = []
|
||||||
}
|
}
|
||||||
orderedEmojiList[indexOfFilterWord].push(emoji)
|
orderedEmojiList[indexOfKeyword].push(emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return orderedEmojiList.flat()
|
return orderedEmojiList.flat()
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
v-for="emoji in commonEmojis"
|
v-for="emoji in commonEmojis"
|
||||||
:key="emoji.replacement"
|
:key="emoji.replacement"
|
||||||
class="emoji-button"
|
class="emoji-button"
|
||||||
:title="emoji.displayText"
|
:title="maybeLocalizedEmojiName(emoji)"
|
||||||
@click="addReaction($event, emoji.replacement, close)"
|
@click="addReaction($event, emoji.replacement, close)"
|
||||||
>
|
>
|
||||||
{{ emoji.replacement }}
|
{{ emoji.replacement }}
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
v-for="(emoji, key) in emojis"
|
v-for="(emoji, key) in emojis"
|
||||||
:key="key"
|
:key="key"
|
||||||
class="emoji-button"
|
class="emoji-button"
|
||||||
:title="emoji.displayText"
|
:title="maybeLocalizedEmojiName(emoji)"
|
||||||
@click="addReaction($event, emoji.replacement, close)"
|
@click="addReaction($event, emoji.replacement, close)"
|
||||||
>
|
>
|
||||||
{{ emoji.replacement }}
|
{{ emoji.replacement }}
|
||||||
|
|
|
@ -17,6 +17,9 @@ const ReplyButton = {
|
||||||
computed: {
|
computed: {
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
remoteInteractionLink () {
|
||||||
|
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,19 @@
|
||||||
/>
|
/>
|
||||||
</FALayers>
|
</FALayers>
|
||||||
</button>
|
</button>
|
||||||
<span v-else>
|
<a
|
||||||
|
v-else
|
||||||
|
class="button-unstyled interactive"
|
||||||
|
target="_blank"
|
||||||
|
role="button"
|
||||||
|
:href="remoteInteractionLink"
|
||||||
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="reply"
|
icon="reply"
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
:title="$t('tool_tip.reply')"
|
:title="$t('tool_tip.reply')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="status.replies_count > 0"
|
v-if="status.replies_count > 0"
|
||||||
class="action-counter"
|
class="action-counter"
|
||||||
|
|
|
@ -36,6 +36,9 @@ const RetweetButton = {
|
||||||
computed: {
|
computed: {
|
||||||
mergedConfig () {
|
mergedConfig () {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
|
},
|
||||||
|
remoteInteractionLink () {
|
||||||
|
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,13 +40,19 @@
|
||||||
:title="$t('timeline.no_retweet_hint')"
|
:title="$t('timeline.no_retweet_hint')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<a
|
||||||
|
v-else
|
||||||
|
class="button-unstyled interactive"
|
||||||
|
target="_blank"
|
||||||
|
role="button"
|
||||||
|
:href="remoteInteractionLink"
|
||||||
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="retweet"
|
icon="retweet"
|
||||||
:title="$t('tool_tip.repeat')"
|
:title="$t('tool_tip.repeat')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
|
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
|
||||||
class="no-event"
|
class="no-event"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
faCircleNotch,
|
faCircleNotch,
|
||||||
faSearch
|
faSearch
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { uniqBy } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faCircleNotch,
|
faCircleNotch,
|
||||||
|
@ -32,7 +33,11 @@ const Search = {
|
||||||
userIds: [],
|
userIds: [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
currenResultTab: 'statuses'
|
currenResultTab: 'statuses',
|
||||||
|
|
||||||
|
statusesOffset: 0,
|
||||||
|
lastStatusFetchCount: 0,
|
||||||
|
lastQuery: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -61,26 +66,42 @@ const Search = {
|
||||||
this.$router.push({ name: 'search', query: { query } })
|
this.$router.push({ name: 'search', query: { query } })
|
||||||
this.$refs.searchInput.focus()
|
this.$refs.searchInput.focus()
|
||||||
},
|
},
|
||||||
search (query) {
|
search (query, searchType = null) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.userIds = []
|
|
||||||
this.statuses = []
|
|
||||||
this.hashtags = []
|
|
||||||
this.$refs.searchInput.blur()
|
this.$refs.searchInput.blur()
|
||||||
|
if (this.lastQuery !== query) {
|
||||||
|
this.userIds = []
|
||||||
|
this.hashtags = []
|
||||||
|
this.statuses = []
|
||||||
|
|
||||||
this.$store.dispatch('search', { q: query, resolve: true })
|
this.statusesOffset = 0
|
||||||
|
this.lastStatusFetchCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('search', { q: query, resolve: true, offset: this.statusesOffset, type: searchType })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.userIds = map(data.accounts, 'id')
|
|
||||||
this.statuses = data.statuses
|
const oldLength = this.statuses.length
|
||||||
this.hashtags = data.hashtags
|
|
||||||
|
// Always append to old results. If new results are empty, this doesn't change anything
|
||||||
|
this.userIds = this.userIds.concat(map(data.accounts, 'id'))
|
||||||
|
this.statuses = uniqBy(this.statuses.concat(data.statuses), 'id')
|
||||||
|
this.hashtags = this.hashtags.concat(data.hashtags)
|
||||||
|
|
||||||
this.currenResultTab = this.getActiveTab()
|
this.currenResultTab = this.getActiveTab()
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
|
|
||||||
|
// Offset from whatever we already have
|
||||||
|
this.statusesOffset = this.statuses.length
|
||||||
|
// Because the amount of new statuses can actually be zero, compare to old lenght instead
|
||||||
|
this.lastStatusFetchCount = this.statuses.length - oldLength
|
||||||
|
this.lastQuery = query
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
resultCount (tabName) {
|
resultCount (tabName) {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="loading"
|
v-if="loading && statusesOffset == 0"
|
||||||
class="text-center loading-icon"
|
class="text-center loading-icon"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -55,12 +55,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div v-if="currenResultTab === 'statuses'">
|
<div v-if="currenResultTab === 'statuses'">
|
||||||
<div
|
|
||||||
v-if="visibleStatuses.length === 0 && !loading && loaded"
|
|
||||||
class="search-result-heading"
|
|
||||||
>
|
|
||||||
<h4>{{ $t('search.no_results') }}</h4>
|
|
||||||
</div>
|
|
||||||
<Status
|
<Status
|
||||||
v-for="status in visibleStatuses"
|
v-for="status in visibleStatuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
|
@ -71,6 +65,33 @@
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:no-heading="false"
|
:no-heading="false"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="!loading && loaded && lastStatusFetchCount > 0"
|
||||||
|
class="more-statuses-button button-unstyled -link -fullwidth"
|
||||||
|
@click.prevent="search(searchTerm, 'statuses')"
|
||||||
|
>
|
||||||
|
<div class="new-status-notification text-center">
|
||||||
|
{{ $t('search.load_more') }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-else-if="loading && statusesOffset > 0"
|
||||||
|
class="text-center loading-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="circle-notch"
|
||||||
|
spin
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="(visibleStatuses.length === 0 || lastStatusFetchCount === 0) && !loading && loaded"
|
||||||
|
class="search-result-heading"
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
{{ visibleStatuses.length === 0 ? $t('search.no_results') : $t('search.no_more_results') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="currenResultTab === 'people'">
|
<div v-else-if="currenResultTab === 'people'">
|
||||||
<div
|
<div
|
||||||
|
@ -208,6 +229,11 @@
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-statuses-button {
|
||||||
|
height: 3.5em;
|
||||||
|
line-height: 3.5em;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,7 +13,7 @@ const StaffPanel = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
groupedStaffAccounts () {
|
groupedStaffAccounts () {
|
||||||
const staffAccounts = map(this.staffAccounts, this.findUser).filter(_ => _)
|
const staffAccounts = map(this.staffAccounts, this.findUserByName).filter(_ => _)
|
||||||
const groupedStaffAccounts = groupBy(staffAccounts, 'role')
|
const groupedStaffAccounts = groupBy(staffAccounts, 'role')
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -22,7 +22,7 @@ const StaffPanel = {
|
||||||
].filter(group => group.users)
|
].filter(group => group.users)
|
||||||
},
|
},
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'findUser'
|
'findUserByName'
|
||||||
]),
|
]),
|
||||||
...mapState({
|
...mapState({
|
||||||
staffAccounts: state => state.instance.staffAccounts
|
staffAccounts: state => state.instance.staffAccounts
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
:timeline-name="timelineName"
|
:timeline-name="timelineName"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="rightside-button"
|
|
||||||
v-if="showScrollTop && !embedded"
|
v-if="showScrollTop && !embedded"
|
||||||
|
class="rightside-button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled scroll-to-top-button"
|
class="button-unstyled scroll-to-top-button"
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
</div>
|
</div>
|
||||||
<template v-if="mobileLayout && !embedded">
|
<template v-if="mobileLayout && !embedded">
|
||||||
<div
|
<div
|
||||||
class="rightside-button"
|
|
||||||
v-if="showLoadButton"
|
v-if="showLoadButton"
|
||||||
|
class="rightside-button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled loadmore-button"
|
class="button-unstyled loadmore-button"
|
||||||
|
@ -72,8 +72,14 @@
|
||||||
{{ $t('timeline.up_to_date') }}
|
{{ $t('timeline.up_to_date') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<QuickFilterSettings v-if="!embedded" class="rightside-button"/>
|
<QuickFilterSettings
|
||||||
<QuickViewSettings v-if="!embedded" class="rightside-button"/>
|
v-if="!embedded"
|
||||||
|
class="rightside-button"
|
||||||
|
/>
|
||||||
|
<QuickViewSettings
|
||||||
|
v-if="!embedded"
|
||||||
|
class="rightside-button"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="classes.body">
|
<div :class="classes.body">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -125,6 +125,10 @@ export default {
|
||||||
hideFollowersCount () {
|
hideFollowersCount () {
|
||||||
return this.isOtherUser && this.user.hide_followers_count
|
return this.isOtherUser && this.user.hide_followers_count
|
||||||
},
|
},
|
||||||
|
showModerationMenu () {
|
||||||
|
const privileges = this.loggedIn.privileges
|
||||||
|
return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags')
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -258,7 +258,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ModerationTools
|
<ModerationTools
|
||||||
v-if="loggedIn.role === "admin""
|
v-if="showModerationMenu"
|
||||||
:user="user"
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -986,7 +986,9 @@
|
||||||
"hashtags": "Hashtags",
|
"hashtags": "Hashtags",
|
||||||
"person_talking": "{count} person talking",
|
"person_talking": "{count} person talking",
|
||||||
"people_talking": "{count} people talking",
|
"people_talking": "{count} people talking",
|
||||||
"no_results": "No results"
|
"no_results": "No results",
|
||||||
|
"no_more_results": "No more results",
|
||||||
|
"load_more": "Load more results"
|
||||||
},
|
},
|
||||||
"password_reset": {
|
"password_reset": {
|
||||||
"forgot_password": "Forgot password?",
|
"forgot_password": "Forgot password?",
|
||||||
|
|
|
@ -36,6 +36,8 @@ const REGIONAL_INDICATORS = (() => {
|
||||||
return res
|
return res
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const REMOTE_INTERACTION_URL = '/main/ostatus'
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
// Stuff from apiConfig
|
// Stuff from apiConfig
|
||||||
name: 'Pleroma FE',
|
name: 'Pleroma FE',
|
||||||
|
@ -214,6 +216,18 @@ const instance = {
|
||||||
},
|
},
|
||||||
instanceDomain (state) {
|
instanceDomain (state) {
|
||||||
return new URL(state.server).hostname
|
return new URL(state.server).hostname
|
||||||
|
},
|
||||||
|
remoteInteractionLink (state) {
|
||||||
|
const server = state.server.endsWith('/') ? state.server.slice(0, -1) : state.server
|
||||||
|
const link = server + REMOTE_INTERACTION_URL
|
||||||
|
|
||||||
|
return ({ statusId, nickname }) => {
|
||||||
|
if (statusId) {
|
||||||
|
return `${link}?status_id=${statusId}`
|
||||||
|
} else {
|
||||||
|
return `${link}?nickname=${nickname}`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -761,8 +761,8 @@ const statuses = {
|
||||||
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
|
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
|
||||||
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
|
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
|
||||||
},
|
},
|
||||||
search (store, { q, resolve, limit, offset, following }) {
|
search (store, { q, resolve, limit, offset, following, type }) {
|
||||||
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following })
|
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following, type })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
store.commit('addNewUsers', data.accounts)
|
store.commit('addNewUsers', data.accounts)
|
||||||
store.commit('addNewStatuses', { statuses: data.statuses })
|
store.commit('addNewStatuses', { statuses: data.statuses })
|
||||||
|
|
|
@ -1278,7 +1278,7 @@ const searchUsers = ({ credentials, query }) => {
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
const search2 = ({ credentials, q, resolve, limit, offset, following, type }) => {
|
||||||
let url = MASTODON_SEARCH_2
|
let url = MASTODON_SEARCH_2
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
|
@ -1302,6 +1302,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
||||||
params.push(['following', true])
|
params.push(['following', true])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
params.push(['following', type])
|
||||||
|
}
|
||||||
|
|
||||||
params.push(['with_relationships', true])
|
params.push(['with_relationships', true])
|
||||||
|
|
||||||
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
||||||
|
|
|
@ -124,6 +124,34 @@ export const parseUser = (data) => {
|
||||||
} else {
|
} else {
|
||||||
output.role = 'member'
|
output.role = 'member'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.pleroma.privileges) {
|
||||||
|
output.privileges = data.pleroma.privileges
|
||||||
|
} else if (data.pleroma.is_admin) {
|
||||||
|
output.privileges = [
|
||||||
|
'users_read',
|
||||||
|
'users_manage_invites',
|
||||||
|
'users_manage_activation_state',
|
||||||
|
'users_manage_tags',
|
||||||
|
'users_manage_credentials',
|
||||||
|
'users_delete',
|
||||||
|
'messages_read',
|
||||||
|
'messages_delete',
|
||||||
|
'instances_delete',
|
||||||
|
'reports_manage_reports',
|
||||||
|
'moderation_log_read',
|
||||||
|
'announcements_manage_announcements',
|
||||||
|
'emoji_manage_emoji',
|
||||||
|
'statistics_read'
|
||||||
|
]
|
||||||
|
} else if (data.pleroma.is_moderator) {
|
||||||
|
output.privileges = [
|
||||||
|
'messages_delete',
|
||||||
|
'reports_manage_reports'
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
output.privileges = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.source) {
|
if (data.source) {
|
||||||
|
|
Loading…
Reference in New Issue