Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into froth
This commit is contained in:
commit
fdc03f27cc
34 changed files with 9652 additions and 13618 deletions
16
package.json
16
package.json
|
@ -15,12 +15,12 @@
|
|||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.20.7",
|
||||
"@babel/runtime": "7.21.0",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@vuelidate/core": "2.0.0",
|
||||
|
@ -47,11 +47,11 @@
|
|||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.7",
|
||||
"@babel/core": "7.21.0",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/plugin-transform-runtime": "7.19.6",
|
||||
"@babel/plugin-transform-runtime": "7.21.0",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/register": "7.18.9",
|
||||
"@babel/register": "7.21.0",
|
||||
"@intlify/vue-i18n-loader": "5.0.0",
|
||||
"@ungap/event-target": "0.2.3",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
|
|
|
@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
|
|
|
@ -134,6 +134,9 @@ const EmojiInput = {
|
|||
padEmoji () {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
defaultCandidateIndex () {
|
||||
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
||||
},
|
||||
preText () {
|
||||
return this.modelValue.slice(0, this.caret)
|
||||
},
|
||||
|
@ -287,7 +290,7 @@ const EmojiInput = {
|
|||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
this.highlighted = -1
|
||||
this.highlighted = this.defaultCandidateIndex
|
||||
this.$refs.screenReaderNotice.announce(
|
||||
this.$tc('tool_tip.autocomplete_available',
|
||||
this.suggestions.length,
|
||||
|
|
|
@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
user.screen_name && user.name && (
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix))
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
|
|
@ -98,6 +98,11 @@ const EmojiPicker = {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideCustomEmoji: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -280,6 +285,9 @@ const EmojiPicker = {
|
|||
return 0
|
||||
},
|
||||
allCustomGroups () {
|
||||
if (this.hideCustomEmoji) {
|
||||
return {}
|
||||
}
|
||||
const emojis = this.$store.getters.groupedCustomEmojis
|
||||
if (emojis.unpacked) {
|
||||
emojis.unpacked.text = this.$t('emoji.unpacked')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="EmojiReactions">
|
||||
<UserListPopover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
:key="reaction.url || reaction.name"
|
||||
:users="accountsForEmoji[reaction.name]"
|
||||
>
|
||||
<button
|
||||
|
@ -11,7 +11,21 @@
|
|||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||
<span
|
||||
class="reaction-emoji"
|
||||
>
|
||||
<img
|
||||
v-if="reaction.url"
|
||||
:src="reaction.url"
|
||||
:title="reaction.name"
|
||||
class="reaction-emoji-content"
|
||||
width="1em"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="reaction-emoji reaction-emoji-content"
|
||||
>{{ reaction.name }}</span>
|
||||
</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</UserListPopover>
|
||||
|
@ -35,6 +49,8 @@
|
|||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
--emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
@ -45,8 +61,24 @@
|
|||
box-sizing: border-box;
|
||||
|
||||
.reaction-emoji {
|
||||
width: 1.25em;
|
||||
width: var(--emoji-size);
|
||||
height: var(--emoji-size);
|
||||
margin-right: 0.25em;
|
||||
line-height: var(--emoji-size);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reaction-emoji-content {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
line-height: inherit;
|
||||
overflow: hidden;
|
||||
font-size: calc(var(--emoji-size) * 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
:class="{ custom: isCustom }"
|
||||
>
|
||||
<label
|
||||
:id="name + '-label'"
|
||||
:for="preset === 'custom' ? name : name + '-font-switcher'"
|
||||
class="label"
|
||||
>
|
||||
|
@ -12,7 +13,8 @@
|
|||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exlcude-disabled"
|
||||
:aria-labelledby="name + '-label'"
|
||||
class="opt exlcude-disabled visible-for-screenreader-only"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
|
@ -21,6 +23,7 @@
|
|||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
|
|
|
@ -87,3 +87,21 @@ export const ROOT_ITEMS = {
|
|||
criteria: ['announcements']
|
||||
}
|
||||
}
|
||||
|
||||
export function routeTo (item, currentUser) {
|
||||
if (!item.route && !item.routeObject) return null
|
||||
|
||||
let route
|
||||
|
||||
if (item.routeObject) {
|
||||
route = item.routeObject
|
||||
} else {
|
||||
route = { name: (item.anon || currentUser) ? item.route : item.anonRoute }
|
||||
}
|
||||
|
||||
if (USERNAME_ROUTES.has(route.name)) {
|
||||
route.params = { username: currentUser.screen_name, name: currentUser.screen_name }
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||
import { routeTo } from 'src/components/navigation/navigation.js'
|
||||
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||
|
@ -26,17 +26,7 @@ const NavigationEntry = {
|
|||
},
|
||||
computed: {
|
||||
routeTo () {
|
||||
if (!this.item.route && !this.item.routeObject) return null
|
||||
let route
|
||||
if (this.item.routeObject) {
|
||||
route = this.item.routeObject
|
||||
} else {
|
||||
route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute }
|
||||
}
|
||||
if (USERNAME_ROUTES.has(route.name)) {
|
||||
route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name }
|
||||
}
|
||||
return route
|
||||
return routeTo(this.item, this.currentUser)
|
||||
},
|
||||
getters () {
|
||||
return this.$store.getters
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||
import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js'
|
||||
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -31,14 +31,7 @@ const NavPanel = {
|
|||
props: ['limit'],
|
||||
methods: {
|
||||
getRouteTo (item) {
|
||||
if (item.routeObject) {
|
||||
return item.routeObject
|
||||
}
|
||||
const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute }
|
||||
if (USERNAME_ROUTES.has(route.name)) {
|
||||
route.params = { username: this.currentUser.screen_name }
|
||||
}
|
||||
return route
|
||||
return routeTo(item, this.currentUser)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -121,7 +121,16 @@
|
|||
scope="global"
|
||||
keypath="notifications.reacted_with"
|
||||
>
|
||||
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
|
||||
<img
|
||||
v-if="notification.emoji_url"
|
||||
class="emoji-reaction-emoji emoji-reaction-emoji-image"
|
||||
:src="notification.emoji_url"
|
||||
:name="notification.emoji"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-reaction-emoji"
|
||||
>{{ notification.emoji }}</span>
|
||||
</i18n-t>
|
||||
</small>
|
||||
</span>
|
||||
|
@ -153,8 +162,8 @@
|
|||
</router-link>
|
||||
<button
|
||||
class="button-unstyled expand-icon"
|
||||
:title="$t('tool_tip.toggle_expand')"
|
||||
:aria-expanded="statusExpanded"
|
||||
:title="$t('tool_tip.toggle_expand')"
|
||||
@click.prevent="toggleStatusExpanded"
|
||||
>
|
||||
<FAIcon
|
||||
|
|
|
@ -129,6 +129,13 @@
|
|||
|
||||
.emoji-reaction-emoji {
|
||||
font-size: 1.3em;
|
||||
max-width: 1.25em;
|
||||
height: 1.25em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.emoji-reaction-emoji-image {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.notification-details {
|
||||
|
|
|
@ -12,7 +12,8 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
choices: []
|
||||
choices: [],
|
||||
randomSeed: `${Math.random()}`.replace('.', '-')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
|
|
@ -4,53 +4,63 @@
|
|||
:class="containerClass"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="poll-option"
|
||||
:role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')"
|
||||
>
|
||||
<div
|
||||
v-if="showResults"
|
||||
:title="resultTitle(option)"
|
||||
class="option-result"
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="poll-option"
|
||||
>
|
||||
<div class="option-result-label">
|
||||
<span class="result-percentage">
|
||||
{{ percentageForOption(option.votes_count) }}%
|
||||
</span>
|
||||
<RichContent
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
<div
|
||||
v-if="showResults"
|
||||
:title="resultTitle(option)"
|
||||
class="option-result"
|
||||
>
|
||||
<div class="option-result-label">
|
||||
<span class="result-percentage">
|
||||
{{ percentageForOption(option.votes_count) }}%
|
||||
</span>
|
||||
<RichContent
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="result-fill"
|
||||
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="result-fill"
|
||||
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@click="activateOption(index)"
|
||||
>
|
||||
<input
|
||||
v-if="poll.multiple"
|
||||
type="checkbox"
|
||||
:disabled="loading"
|
||||
:value="index"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
type="radio"
|
||||
:disabled="loading"
|
||||
:value="index"
|
||||
tabindex="0"
|
||||
:role="poll.multiple ? 'checkbox' : 'radio'"
|
||||
:aria-labelledby="`option-vote-${randomSeed}-${index}`"
|
||||
:aria-checked="choices[index]"
|
||||
@click="activateOption(index)"
|
||||
>
|
||||
<label class="option-vote">
|
||||
<RichContent
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
v-if="poll.multiple"
|
||||
type="checkbox"
|
||||
class="poll-checkbox"
|
||||
:disabled="loading"
|
||||
:value="index"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
type="radio"
|
||||
:disabled="loading"
|
||||
:value="index"
|
||||
>
|
||||
<label class="option-vote">
|
||||
<RichContent
|
||||
:id="`option-vote-${randomSeed}-${index}`"
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer faint">
|
||||
|
@ -161,5 +171,9 @@
|
|||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.poll-checkbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -281,12 +281,10 @@
|
|||
>
|
||||
{{ $t('post_status.post') }}
|
||||
</button>
|
||||
<!-- touchstart is used to keep the OSK at the same position after a message send -->
|
||||
<button
|
||||
v-else
|
||||
:disabled="uploadingFiles || disableSubmit"
|
||||
class="btn button-default"
|
||||
@touchstart.stop.prevent="postStatus($event, newStatus)"
|
||||
@click.stop.prevent="postStatus($event, newStatus)"
|
||||
>
|
||||
{{ $t('post_status.post') }}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
:class="{ disabled: !present || disabled }"
|
||||
>
|
||||
<label
|
||||
:id="name + '-label'"
|
||||
:for="name"
|
||||
class="label"
|
||||
>
|
||||
|
@ -12,7 +13,8 @@
|
|||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt"
|
||||
:aria-labelledby="name + '-label'"
|
||||
class="opt visible-for-screenreader-only"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@change="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||
|
@ -21,6 +23,7 @@
|
|||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
|
@ -34,9 +37,10 @@
|
|||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
:id="name"
|
||||
:id="name + '-numeric'"
|
||||
class="input-number"
|
||||
type="number"
|
||||
:aria-labelledby="name + '-label'"
|
||||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
:max="hardMax"
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
// import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||
import { trim } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faPlus,
|
||||
|
@ -22,108 +21,33 @@ const ReactButton = {
|
|||
},
|
||||
components: {
|
||||
Popover,
|
||||
Checkbox
|
||||
EmojiPicker
|
||||
},
|
||||
methods: {
|
||||
addReaction (event, emoji, close, keepReactOpen) {
|
||||
addReaction (event) {
|
||||
const emoji = event.insertion
|
||||
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
||||
if (existingReaction && existingReaction.me) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
} else {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
}
|
||||
this.keepReactOpen = keepReactOpen
|
||||
if (!keepReactOpen) {
|
||||
close()
|
||||
},
|
||||
show () {
|
||||
if (!this.expanded) {
|
||||
this.$refs.picker.showPicker()
|
||||
}
|
||||
},
|
||||
onShow () {
|
||||
this.expanded = true
|
||||
this.focusInput()
|
||||
},
|
||||
onClose () {
|
||||
this.expanded = false
|
||||
},
|
||||
focusInput () {
|
||||
this.$nextTick(() => {
|
||||
const input = document.querySelector('.reaction-picker-filter > input')
|
||||
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: {
|
||||
commonEmojis () {
|
||||
const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥', '🤥', '🤔', '🤪', '🤸♂️', '💊', '✍️', '✏️', '📈', '📉', '❔', '❌', '🦧', '🦉', '🍼', '🖍️', '🔧', '🧛🏿'])
|
||||
return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement))
|
||||
},
|
||||
languages () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
const keywordLowercase = trim(this.filterWord.toLowerCase())
|
||||
|
||||
const orderedEmojiList = []
|
||||
for (const emoji of this.$store.getters.standardEmojiList) {
|
||||
const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji)
|
||||
.keywords
|
||||
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
||||
.filter(k => k > -1)
|
||||
|
||||
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
||||
|
||||
if (indexOfKeyword > -1) {
|
||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||
orderedEmojiList[indexOfKeyword] = []
|
||||
}
|
||||
orderedEmojiList[indexOfKeyword].push(emoji)
|
||||
}
|
||||
}
|
||||
return orderedEmojiList.flat()
|
||||
}
|
||||
return this.$store.getters.standardEmojiList || []
|
||||
},
|
||||
mergedConfig () {
|
||||
return this.$store.getters.mergedConfig
|
||||
hideCustomEmoji () {
|
||||
return !this.$store.state.instance.pleromaChatMessagesAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +1,39 @@
|
|||
<template>
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="ReactButton"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
popover-class="ReactButton popover-default"
|
||||
@show="onShow"
|
||||
@close="onClose"
|
||||
>
|
||||
<template #content="{close}">
|
||||
<div class="reaction-picker-filter">
|
||||
<input
|
||||
v-model="filterWord"
|
||||
size="1"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
</div>
|
||||
<div class="keep-open">
|
||||
<Checkbox v-model="keepReactOpen">
|
||||
{{ $t('emoji.keep_open') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="reaction-picker">
|
||||
<span
|
||||
v-for="emoji in commonEmojis"
|
||||
:key="emoji.replacement"
|
||||
class="emoji-button"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
@click="addReaction($event, emoji.replacement, close, keepReactOpen)"
|
||||
>
|
||||
{{ emoji.replacement }}
|
||||
</span>
|
||||
<div class="reaction-picker-divider" />
|
||||
<span
|
||||
v-for="(emoji, key) in emojis"
|
||||
:key="key"
|
||||
class="emoji-button"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
@click="addReaction($event, emoji.replacement, close, keepReactOpen)"
|
||||
>
|
||||
{{ emoji.replacement }}
|
||||
</span>
|
||||
<div class="reaction-bottom-fader" />
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<span
|
||||
class="button-unstyled popover-trigger"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
>
|
||||
<FALayers>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="['far', 'smile-beam']"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="!expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-17"
|
||||
icon="plus"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-17"
|
||||
icon="times"
|
||||
/>
|
||||
</FALayers>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
<span class="ReactButton">
|
||||
<EmojiPicker
|
||||
ref="picker"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
:hide-custom-emoji="hideCustomEmoji"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="addReaction"
|
||||
@show="onShow"
|
||||
@close="onClose"
|
||||
/>
|
||||
<span
|
||||
class="button-unstyled popover-trigger"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
@click.stop.prevent="show"
|
||||
>
|
||||
<FALayers>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="['far', 'smile-beam']"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="!expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-17"
|
||||
icon="plus"
|
||||
/>
|
||||
<FAIcon
|
||||
v-show="expanded"
|
||||
class="focus-marker"
|
||||
transform="shrink-6 up-9 right-17"
|
||||
icon="times"
|
||||
/>
|
||||
</FALayers>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./react_button.js"></script>
|
||||
|
@ -140,11 +101,6 @@
|
|||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-trigger-button {
|
||||
/* override of popover internal stuff */
|
||||
width: auto;
|
||||
|
||||
@include unfocused-style {
|
||||
.focus-marker {
|
||||
|
|
16
src/components/settings_modal/helpers/float_setting.vue
Normal file
16
src/components/settings_modal/helpers/float_setting.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<NumberSetting
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</NumberSetting>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NumberSetting from './number_setting.vue'
|
||||
export default {
|
||||
components: {
|
||||
NumberSetting
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,27 +1,17 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="IntegerSetting"
|
||||
<NumberSetting
|
||||
v-bind="$attrs"
|
||||
truncate="1"
|
||||
>
|
||||
<label :for="path">
|
||||
<slot />
|
||||
</label>
|
||||
<input
|
||||
:id="path"
|
||||
class="number-input"
|
||||
type="number"
|
||||
step="1"
|
||||
:disabled="disabled"
|
||||
:min="min || 0"
|
||||
:value="state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
</span>
|
||||
<slot />
|
||||
</NumberSetting>
|
||||
</template>
|
||||
|
||||
<script src="./integer_setting.js"></script>
|
||||
<script>
|
||||
import NumberSetting from './number_setting.vue'
|
||||
export default {
|
||||
components: {
|
||||
NumberSetting
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,6 +8,8 @@ export default {
|
|||
path: String,
|
||||
disabled: Boolean,
|
||||
min: Number,
|
||||
step: Number,
|
||||
truncate: Number,
|
||||
expert: [Number, String]
|
||||
},
|
||||
computed: {
|
||||
|
@ -15,8 +17,11 @@ export default {
|
|||
const [firstSegment, ...rest] = this.path.split('.')
|
||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
||||
},
|
||||
parent () {
|
||||
return this.$parent.$parent
|
||||
},
|
||||
state () {
|
||||
const value = get(this.$parent, this.path)
|
||||
const value = get(this.parent, this.path)
|
||||
if (value === undefined) {
|
||||
return this.defaultState
|
||||
} else {
|
||||
|
@ -24,21 +29,28 @@ export default {
|
|||
}
|
||||
},
|
||||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
return get(this.parent, this.pathDefault)
|
||||
},
|
||||
isChanged () {
|
||||
return this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
return (this.expert || 0) <= this.parent.expertLevel
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
truncateValue (value) {
|
||||
if (!this.truncate) {
|
||||
return value
|
||||
}
|
||||
|
||||
return Math.trunc(value / this.truncate) * this.truncate
|
||||
},
|
||||
update (e) {
|
||||
set(this.$parent, this.path, parseInt(e.target.value))
|
||||
set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value)))
|
||||
},
|
||||
reset () {
|
||||
set(this.$parent, this.path, this.defaultState)
|
||||
set(this.parent, this.path, this.defaultState)
|
||||
}
|
||||
}
|
||||
}
|
27
src/components/settings_modal/helpers/number_setting.vue
Normal file
27
src/components/settings_modal/helpers/number_setting.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="matchesExpertLevel"
|
||||
class="NumberSetting"
|
||||
>
|
||||
<label :for="path">
|
||||
<slot />
|
||||
</label>
|
||||
<input
|
||||
:id="path"
|
||||
class="number-input"
|
||||
type="number"
|
||||
:step="step || 1"
|
||||
:disabled="disabled"
|
||||
:min="min || 0"
|
||||
:value="state"
|
||||
@change="update"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<ModifiedIndicator
|
||||
:changed="isChanged"
|
||||
:onclick="reset"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./number_setting.js"></script>
|
|
@ -2,6 +2,7 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
|
|||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import FloatSetting from '../helpers/float_setting.vue'
|
||||
import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
|
||||
|
@ -63,6 +64,7 @@ const GeneralTab = {
|
|||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
IntegerSetting,
|
||||
FloatSetting,
|
||||
SizeSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
|
|
|
@ -308,6 +308,15 @@
|
|||
{{ $t('settings.no_rich_text_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<FloatSetting
|
||||
v-if="user"
|
||||
path="emojiReactionsScale"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.emoji_reactions_scale') }}
|
||||
</FloatSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
|
@ -535,6 +544,14 @@
|
|||
{{ $t('settings.pad_emoji') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="autocompleteSelect"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.autocomplete_select_first') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -129,12 +129,13 @@
|
|||
v-model="selected.inset"
|
||||
:disabled="!present"
|
||||
name="inset"
|
||||
class="input-inset"
|
||||
class="input-inset visible-for-screenreader-only"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="checkbox-label"
|
||||
for="inset"
|
||||
:aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -117,6 +117,7 @@ export default {
|
|||
onClick={this.clickTab(index)}
|
||||
class={classesTab.join(' ')}
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
<img src={props.image} title={props['image-tooltip']}/>
|
||||
{props.label ? '' : props.label}
|
||||
|
@ -131,6 +132,7 @@ export default {
|
|||
onClick={this.clickTab(index)}
|
||||
class={classesTab.join(' ')}
|
||||
type="button"
|
||||
role="tab"
|
||||
>
|
||||
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
|
||||
<span class="text">
|
||||
|
@ -167,11 +169,15 @@ export default {
|
|||
|
||||
return (
|
||||
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||
<div class="tabs">
|
||||
<div
|
||||
class="tabs"
|
||||
role="tablist"
|
||||
>
|
||||
{tabs}
|
||||
</div>
|
||||
<div
|
||||
ref="contents"
|
||||
role="tabpanel"
|
||||
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
|
||||
v-body-scroll-lock={this.bodyScrollLock}
|
||||
>
|
||||
|
|
|
@ -472,8 +472,10 @@
|
|||
"domain_mutes": "Domains",
|
||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
||||
"swap_reacts": "Swap Reactions with Favorite Button",
|
||||
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
||||
"emoji_reactions_scale": "Reactions scale factor",
|
||||
"export_theme": "Save preset",
|
||||
"filtering": "Filtering",
|
||||
"wordfilter": "Wordfilter",
|
||||
|
|
|
@ -295,7 +295,7 @@
|
|||
"change_password_error": "修改密码的时候出了点问题。",
|
||||
"changed_password": "成功修改了密码!",
|
||||
"collapse_subject": "折叠带主题的内容",
|
||||
"composing": "写作",
|
||||
"composing": "撰写",
|
||||
"confirm_new_password": "确认新密码",
|
||||
"current_avatar": "当前头像",
|
||||
"current_password": "当前密码",
|
||||
|
@ -737,7 +737,8 @@
|
|||
"mention_link_use_tooltip": "点击提及链接时显示用户卡片",
|
||||
"mention_link_show_avatar": "在链接旁边显示用户头像",
|
||||
"mention_link_show_avatar_quick": "在提及内容旁边显示用户头像",
|
||||
"user_popover_avatar_action_open": "打开个人资料"
|
||||
"user_popover_avatar_action_open": "打开个人资料",
|
||||
"autocomplete_select_first": "当有自动完成的结果时,自动选择第一个候选项"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} 天",
|
||||
|
|
|
@ -105,6 +105,7 @@ export const defaultState = {
|
|||
sidebarColumnWidth: '25rem',
|
||||
contentColumnWidth: '45rem',
|
||||
notifsColumnWidth: '25rem',
|
||||
emojiReactionsScale: 1.0,
|
||||
navbarColumnStretch: false,
|
||||
greentext: undefined, // instance default
|
||||
useAtIcon: undefined, // instance default
|
||||
|
@ -124,6 +125,7 @@ export const defaultState = {
|
|||
conversationOtherRepliesButton: undefined, // instance default
|
||||
conversationTreeFadeAncestors: undefined, // instance default
|
||||
maxDepthInThread: undefined, // instance default
|
||||
autocompleteSelect: undefined, // instance default
|
||||
translationLanguage: undefined, // instance default,
|
||||
supportedTranslationLanguages: [] // instance default
|
||||
}
|
||||
|
@ -194,6 +196,7 @@ const config = {
|
|||
case 'sidebarColumnWidth':
|
||||
case 'contentColumnWidth':
|
||||
case 'notifsColumnWidth':
|
||||
case 'emojiReactionsScale':
|
||||
applyConfig(state)
|
||||
break
|
||||
case 'customTheme':
|
||||
|
|
|
@ -107,6 +107,7 @@ const defaultState = {
|
|||
conversationOtherRepliesButton: 'below',
|
||||
conversationTreeFadeAncestors: false,
|
||||
maxDepthInThread: 6,
|
||||
autocompleteSelect: false,
|
||||
|
||||
// Nasty stuff
|
||||
customEmoji: [],
|
||||
|
@ -125,6 +126,7 @@ const defaultState = {
|
|||
// Feature-set, apparently, not everything here is reported...
|
||||
shoutAvailable: false,
|
||||
pleromaChatMessagesAvailable: false,
|
||||
pleromaCustomEmojiReactionsAvailable: false,
|
||||
gopherAvailable: false,
|
||||
mediaProxyAvailable: false,
|
||||
suggestionsEnabled: false,
|
||||
|
|
|
@ -857,7 +857,7 @@ const postStatus = ({
|
|||
})
|
||||
if (pollOptions.some(option => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: poll.expiresIn,
|
||||
expires_in: parseInt(poll.expiresIn, 10),
|
||||
multiple: poll.multiple
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach(key => {
|
||||
|
@ -917,7 +917,7 @@ const editStatus = ({
|
|||
|
||||
if (pollOptions.some(option => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: poll.expiresIn,
|
||||
expires_in: parseInt(poll.expiresIn, 10),
|
||||
multiple: poll.multiple
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach(key => {
|
||||
|
|
|
@ -445,6 +445,7 @@ export const parseNotification = (data) => {
|
|||
: parseUser(data.target)
|
||||
output.from_profile = parseUser(data.account)
|
||||
output.emoji = data.emoji
|
||||
output.emoji_url = data.emoji_url
|
||||
if (data.report) {
|
||||
output.report = data.report
|
||||
output.report.content = data.report.content
|
||||
|
|
|
@ -21,8 +21,8 @@ export const applyTheme = (input) => {
|
|||
body.classList.remove('hidden')
|
||||
}
|
||||
|
||||
const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth }) =>
|
||||
({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth })
|
||||
const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) =>
|
||||
({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale })
|
||||
|
||||
const defaultConfigColumns = configColumns(defaultState)
|
||||
|
||||
|
|
Loading…
Reference in a new issue