aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/checkbox/checkbox.vue37
-rw-r--r--src/components/desktop_nav/desktop_nav.js5
-rw-r--r--src/components/desktop_nav/desktop_nav.vue16
-rw-r--r--src/components/emoji_input/emoji_input.js57
-rw-r--r--src/components/emoji_input/emoji_input.vue21
-rw-r--r--src/components/emoji_input/suggestor.js5
-rw-r--r--src/components/emoji_picker/emoji_picker.js8
-rw-r--r--src/components/emoji_picker/emoji_picker.vue1
-rw-r--r--src/components/emoji_reactions/emoji_reactions.js26
-rw-r--r--src/components/emoji_reactions/emoji_reactions.vue163
-rw-r--r--src/components/favorite_button/favorite_button.vue17
-rw-r--r--src/components/font_control/font_control.vue5
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue6
-rw-r--r--src/components/list/list.vue6
-rw-r--r--src/components/media_upload/media_upload.js18
-rw-r--r--src/components/media_upload/media_upload.vue21
-rw-r--r--src/components/navigation/navigation.js18
-rw-r--r--src/components/navigation/navigation_entry.js14
-rw-r--r--src/components/navigation/navigation_pins.js14
-rw-r--r--src/components/notification/notification.vue14
-rw-r--r--src/components/notifications/notifications.scss7
-rw-r--r--src/components/poll/poll.js3
-rw-r--r--src/components/poll/poll.vue92
-rw-r--r--src/components/popover/popover.js3
-rw-r--r--src/components/popover/popover.vue3
-rw-r--r--src/components/post_status_form/post_status_form.js4
-rw-r--r--src/components/post_status_form/post_status_form.vue80
-rw-r--r--src/components/quick_filter_settings/quick_filter_settings.vue29
-rw-r--r--src/components/quick_view_settings/quick_view_settings.vue65
-rw-r--r--src/components/range_input/range_input.vue8
-rw-r--r--src/components/react_button/react_button.js96
-rw-r--r--src/components/react_button/react_button.vue109
-rw-r--r--src/components/registration/registration.js4
-rw-r--r--src/components/registration/registration.vue1
-rw-r--r--src/components/reply_button/reply_button.vue18
-rw-r--r--src/components/retweet_button/retweet_button.vue17
-rw-r--r--src/components/screen_reader_notice/screen_reader_notice.js21
-rw-r--r--src/components/screen_reader_notice/screen_reader_notice.vue10
-rw-r--r--src/components/search_bar/search_bar.vue4
-rw-r--r--src/components/select/select.js3
-rw-r--r--src/components/select/select.vue1
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.js64
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.scss13
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.vue184
-rw-r--r--src/components/settings_modal/admin_tabs/instance_tab.js38
-rw-r--r--src/components/settings_modal/admin_tabs/instance_tab.vue196
-rw-r--r--src/components/settings_modal/admin_tabs/limits_tab.js29
-rw-r--r--src/components/settings_modal/admin_tabs/limits_tab.vue136
-rw-r--r--src/components/settings_modal/helpers/attachment_setting.js43
-rw-r--r--src/components/settings_modal/helpers/attachment_setting.vue96
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.js63
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.vue34
-rw-r--r--src/components/settings_modal/helpers/choice_setting.js68
-rw-r--r--src/components/settings_modal/helpers/choice_setting.vue20
-rw-r--r--src/components/settings_modal/helpers/draft_buttons.vue88
-rw-r--r--src/components/settings_modal/helpers/float_setting.vue16
-rw-r--r--src/components/settings_modal/helpers/group_setting.js13
-rw-r--r--src/components/settings_modal/helpers/group_setting.vue15
-rw-r--r--src/components/settings_modal/helpers/integer_setting.js44
-rw-r--r--src/components/settings_modal/helpers/integer_setting.vue36
-rw-r--r--src/components/settings_modal/helpers/modified_indicator.vue2
-rw-r--r--src/components/settings_modal/helpers/number_setting.js24
-rw-r--r--src/components/settings_modal/helpers/number_setting.vue45
-rw-r--r--src/components/settings_modal/helpers/profile_setting_indicator.vue (renamed from src/components/settings_modal/helpers/server_side_indicator.vue)12
-rw-r--r--src/components/settings_modal/helpers/setting.js237
-rw-r--r--src/components/settings_modal/helpers/shared_computed_object.js56
-rw-r--r--src/components/settings_modal/helpers/size_setting.js51
-rw-r--r--src/components/settings_modal/helpers/size_setting.vue19
-rw-r--r--src/components/settings_modal/helpers/string_setting.js5
-rw-r--r--src/components/settings_modal/helpers/string_setting.vue42
-rw-r--r--src/components/settings_modal/settings_modal.js37
-rw-r--r--src/components/settings_modal/settings_modal.scss12
-rw-r--r--src/components/settings_modal/settings_modal.vue40
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.js93
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.scss (renamed from src/components/settings_modal/settings_modal_content.scss)4
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.vue68
-rw-r--r--src/components/settings_modal/settings_modal_user_content.js (renamed from src/components/settings_modal/settings_modal_content.js)0
-rw-r--r--src/components/settings_modal/settings_modal_user_content.scss52
-rw-r--r--src/components/settings_modal/settings_modal_user_content.vue (renamed from src/components/settings_modal/settings_modal_content.vue)4
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue14
-rw-r--r--src/components/settings_modal/tabs/general_tab.js8
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue49
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.js7
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue8
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js4
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue96
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.vue22
-rw-r--r--src/components/shadow_control/shadow_control.vue3
-rw-r--r--src/components/side_drawer/side_drawer.js5
-rw-r--r--src/components/side_drawer/side_drawer.vue8
-rw-r--r--src/components/tab_switcher/tab_switcher.jsx16
91 files changed, 2555 insertions, 734 deletions
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 7139d4fc..b8b77e7c 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -1,16 +1,21 @@
<template>
<label
class="checkbox"
- :class="{ disabled, indeterminate }"
+ :class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
>
<input
type="checkbox"
+ class="visible-for-screenreader-only"
:disabled="disabled"
:checked="modelValue"
:indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)"
>
- <i class="checkbox-indicator" />
+ <i
+ class="checkbox-indicator"
+ :aria-hidden="true"
+ @transitionend.capture="onTransitionEnd"
+ />
<span
v-if="!!$slots.default"
class="label"
@@ -27,12 +32,30 @@ export default {
'indeterminate',
'disabled'
],
- emits: ['update:modelValue']
+ emits: ['update:modelValue'],
+ data: (vm) => ({
+ indeterminateTransitionFix: vm.indeterminate
+ }),
+ watch: {
+ indeterminate (e) {
+ if (e) {
+ this.indeterminateTransitionFix = true
+ }
+ }
+ },
+ methods: {
+ onTransitionEnd (e) {
+ if (!this.indeterminate) {
+ this.indeterminateTransitionFix = false
+ }
+ }
+ }
}
</script>
<style lang="scss">
@import "../../variables";
+@import "../../mixins";
.checkbox {
position: relative;
@@ -81,8 +104,6 @@ export default {
}
input[type="checkbox"] {
- display: none;
-
&:checked + .checkbox-indicator::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
@@ -95,6 +116,12 @@ export default {
}
}
+ &.indeterminate-fix {
+ input[type="checkbox"] + .checkbox-indicator::before {
+ content: "–";
+ }
+ }
+
& > span {
margin-left: 0.5em;
}
diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js
index 745b1a81..f6a2e294 100644
--- a/src/components/desktop_nav/desktop_nav.js
+++ b/src/components/desktop_nav/desktop_nav.js
@@ -107,7 +107,10 @@ export default {
this.searchBarHidden = hidden
},
openSettingsModal () {
- this.$store.dispatch('openSettingsModal')
+ this.$store.dispatch('openSettingsModal', 'user')
+ },
+ openAdminModal () {
+ this.$store.dispatch('openSettingsModal', 'admin')
}
}
}
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
index 92622cf9..49382f8e 100644
--- a/src/components/desktop_nav/desktop_nav.vue
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -20,6 +20,7 @@
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
+ :title="sitename"
>
<div
class="mask"
@@ -38,40 +39,39 @@
/>
<button
class="button-unstyled nav-icon"
+ :title="$t('nav.preferences')"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
- :title="$t('nav.preferences')"
/>
</button>
- <a
+ <button
v-if="currentUser && currentUser.role === 'admin'"
- href="/pleroma/admin/#/login-pleroma"
- class="nav-icon"
+ class="button-unstyled nav-icon"
target="_blank"
- @click.stop
+ :title="$t('nav.administration')"
+ @click.stop="openAdminModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
- :title="$t('nav.administration')"
/>
- </a>
+ </button>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
+ :title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
- :title="$t('login.logout')"
/>
</button>
</div>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index ba5f7552..68654f69 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -1,6 +1,7 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue'
+import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@@ -109,9 +110,10 @@ const EmojiInput = {
},
data () {
return {
+ randomSeed: `${Math.random()}`.replace('.', '-'),
input: undefined,
caretEl: undefined,
- highlighted: 0,
+ highlighted: -1,
caret: 0,
focused: false,
blurTimeout: null,
@@ -125,12 +127,16 @@ const EmojiInput = {
components: {
Popover,
EmojiPicker,
- UnicodeDomainIndicator
+ UnicodeDomainIndicator,
+ ScreenReaderNotice
},
computed: {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
+ defaultCandidateIndex () {
+ return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
+ },
preText () {
return this.modelValue.slice(0, this.caret)
},
@@ -203,6 +209,12 @@ const EmojiInput = {
top: this.input.scrollTop,
left: this.input.scrollLeft
})
+ },
+ suggestionListId () {
+ return `suggestions-${this.randomSeed}`
+ },
+ suggestionItemId () {
+ return (index) => `suggestion-item-${index}-${this.randomSeed}`
}
},
mounted () {
@@ -278,6 +290,11 @@ const EmojiInput = {
...rest,
img: imageUrl || ''
}))
+ this.highlighted = this.defaultCandidateIndex
+ this.$refs.screenReaderNotice.announce(
+ this.$tc('tool_tip.autocomplete_available',
+ this.suggestions.length,
+ { number: this.suggestions.length }))
}
},
methods: {
@@ -374,26 +391,27 @@ const EmojiInput = {
},
cycleBackward (e) {
const len = this.suggestions.length || 0
- if (len > 1) {
- this.highlighted -= 1
- if (this.highlighted < 0) {
- this.highlighted = this.suggestions.length - 1
- }
+
+ this.highlighted -= 1
+ if (this.highlighted === -1) {
+ this.input.focus()
+ } else if (this.highlighted < -1) {
+ this.highlighted = len - 1
+ }
+ if (len > 0) {
e.preventDefault()
- } else {
- this.highlighted = 0
}
},
cycleForward (e) {
const len = this.suggestions.length || 0
- if (len > 1) {
- this.highlighted += 1
- if (this.highlighted >= len) {
- this.highlighted = 0
- }
+
+ this.highlighted += 1
+ if (this.highlighted >= len) {
+ this.highlighted = -1
+ this.input.focus()
+ }
+ if (len > 0) {
e.preventDefault()
- } else {
- this.highlighted = 0
}
},
scrollIntoView () {
@@ -540,6 +558,13 @@ const EmojiInput = {
})
},
resize () {
+ },
+ autoCompleteItemLabel (suggestion) {
+ if (suggestion.user) {
+ return suggestion.displayText + ' ' + suggestion.detailText
+ } else {
+ return this.maybeLocalizedEmojiName(suggestion)
+ }
}
}
}
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index ccba0393..7f9ecc99 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -4,12 +4,19 @@
class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
>
- <slot />
+ <slot
+ :id="'textbox-' + randomSeed"
+ :aria-owns="suggestionListId"
+ aria-autocomplete="both"
+ :aria-expanded="showSuggestions"
+ :aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
+ />
<!-- TODO: make the 'x' disappear if at the end maybe? -->
<div
ref="hiddenOverlay"
class="hidden-overlay"
:style="overlayStyle"
+ :aria-hidden="true"
>
<span>{{ preText }}</span>
<span
@@ -18,11 +25,16 @@
>x</span>
<span>{{ postText }}</span>
</div>
+ <screen-reader-notice
+ ref="screenReaderNotice"
+ aria-live="assertive"
+ />
<template v-if="enableEmojiPicker">
<button
v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon"
type="button"
+ :title="$t('emoji.add_emoji')"
@click.prevent="togglePicker"
>
<FAIcon :icon="['far', 'smile-beam']" />
@@ -43,17 +55,24 @@
ref="suggestorPopover"
class="autocomplete-panel"
placement="bottom"
+ :trigger-attrs="{ 'aria-hidden': true }"
>
<template #content>
<div
+ :id="suggestionListId"
ref="panel-body"
class="autocomplete-panel-body"
+ role="listbox"
>
<div
v-for="(suggestion, index) in suggestions"
+ :id="suggestionItemId(index)"
:key="index"
class="autocomplete-item"
+ role="option"
:class="{ highlighted: index === highlighted }"
+ :aria-label="autoCompleteItemLabel(suggestion)"
+ :aria-selected="index === highlighted"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index adaa879e..e746dcd7 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -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
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 0d7ca812..349b043d 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -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')
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index ca90bf26..6972164b 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -3,6 +3,7 @@
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
+ :trigger-attrs="{ 'aria-hidden': true }"
@show="onPopoverShown"
@close="onPopoverClosed"
>
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
index bb11b840..b4936424 100644
--- a/src/components/emoji_reactions/emoji_reactions.js
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -1,5 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faPlus,
+ faMinus,
+ faCheck
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faPlus,
+ faMinus,
+ faCheck
+)
const EMOJI_REACTION_COUNT_CUTOFF = 12
@@ -33,6 +45,9 @@ const EmojiReactions = {
},
loggedIn () {
return !!this.$store.state.users.currentUser
+ },
+ remoteInteractionLink () {
+ return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
},
methods: {
@@ -62,6 +77,17 @@ const EmojiReactions = {
} else {
this.reactWith(emoji)
}
+ },
+ counterTriggerAttrs (reaction) {
+ return {
+ class: [
+ 'btn',
+ 'button-default',
+ 'emoji-reaction-count-button',
+ { '-picked-reaction': this.reactedWith(reaction.name) }
+ ],
+ 'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count })
+ }
}
}
}
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index a63daa97..92fc58ca 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -1,20 +1,64 @@
<template>
<div class="EmojiReactions">
- <UserListPopover
+ <span
v-for="(reaction) in emojiReactions"
- :key="reaction.name"
- :users="accountsForEmoji[reaction.name]"
+ :key="reaction.url || reaction.name"
+ class="emoji-reaction-container btn-group"
>
- <button
+ <component
+ :is="loggedIn ? 'button' : 'a'"
+ v-bind="!loggedIn ? { href: remoteInteractionLink } : {}"
+ role="button"
class="emoji-reaction btn button-default"
- :class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
+ :class="{ '-picked-reaction': reactedWith(reaction.name) }"
+ :title="reaction.url ? reaction.name : undefined"
+ :aria-pressed="reactedWith(reaction.name)"
@click="emojiOnClick(reaction.name, $event)"
- @mouseenter="fetchEmojiReactionsByIfMissing()"
>
- <span class="reaction-emoji">{{ reaction.name }}</span>
- <span>{{ reaction.count }}</span>
- </button>
- </UserListPopover>
+ <span
+ class="reaction-emoji"
+ >
+ <img
+ v-if="reaction.url"
+ :src="reaction.url"
+ class="reaction-emoji-content"
+ width="1em"
+ >
+ <span
+ v-else
+ class="reaction-emoji reaction-emoji-content"
+ >{{ reaction.name }}</span>
+ </span>
+ <FALayers>
+ <FAIcon
+ v-if="reactedWith(reaction.name)"
+ class="active-marker"
+ transform="shrink-6 up-9"
+ icon="check"
+ />
+ <FAIcon
+ v-if="!reactedWith(reaction.name)"
+ class="focus-marker"
+ transform="shrink-6 up-9"
+ icon="plus"
+ />
+ <FAIcon
+ v-else
+ class="focus-marker"
+ transform="shrink-6 up-9"
+ icon="minus"
+ />
+ </FALayers>
+ </component>
+ <UserListPopover
+ :users="accountsForEmoji[reaction.name]"
+ class="emoji-reaction-popover"
+ :trigger-attrs="counterTriggerAttrs(reaction)"
+ @show="fetchEmojiReactionsByIfMissing()"
+ >
+ <span class="emoji-reaction-counts">{{ reaction.count }}</span>
+ </UserListPopover>
+ </span>
<a
v-if="tooManyReactions"
class="emoji-reaction-expand faint"
@@ -29,43 +73,118 @@
<script src="./emoji_reactions.js"></script>
<style lang="scss">
@import "../../variables";
+@import "../../mixins";
.EmojiReactions {
display: flex;
margin-top: 0.25em;
flex-wrap: wrap;
- .emoji-reaction {
- padding: 0 0.5em;
- margin-right: 0.5em;
+ --emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
+
+ .emoji-reaction-container {
+ display: flex;
+ align-items: stretch;
margin-top: 0.5em;
+ margin-right: 0.5em;
+
+ .emoji-reaction-popover {
+ padding: 0;
+
+ .emoji-reaction-count-button {
+ background-color: var(--btn);
+ height: 100%;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ box-sizing: border-box;
+ min-width: 2em;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ color: $fallback--text;
+ color: var(--btnText, $fallback--text);
+
+ &.-picked-reaction {
+ border: 1px solid var(--accent, $fallback--link);
+ margin-right: -1px;
+ }
+ }
+ }
+ }
+
+ .emoji-reaction {
+ padding-left: 0.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
.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 {
outline: none;
}
- &.not-clickable {
- cursor: default;
-
- &:hover {
- box-shadow: $fallback--buttonShadow;
- box-shadow: var(--buttonShadow);
- }
+ .svg-inline--fa {
+ color: $fallback--text;
+ color: var(--btnText, $fallback--text);
}
&.-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);
+ margin-right: -1px;
+
+ .svg-inline--fa {
+ color: $fallback--link;
+ color: var(--accent, $fallback--link);
+ }
+ }
+
+ @include unfocused-style {
+ .focus-marker {
+ visibility: hidden;
+ }
+
+ .active-marker {
+ visibility: visible;
+ }
+ }
+
+ @include focused-style {
+ .svg-inline--fa {
+ color: $fallback--link;
+ color: var(--accent, $fallback--link);
+ }
+
+ .focus-marker {
+ visibility: visible;
+ }
+
+ .active-marker {
+ visibility: hidden;
+ }
}
}
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 58d14945..8c883c13 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -38,13 +38,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
+ :title="$t('tool_tip.favorite')"
:href="remoteInteractionLink"
>
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- :title="$t('tool_tip.favorite')"
- :icon="['far', 'star']"
- />
+ <FALayers class="fa-scale-110 fa-old-padding-layer">
+ <FAIcon
+ class="fa-scale-110"
+ :icon="['far', 'star']"
+ />
+ <FAIcon
+ class="focus-marker"
+ transform="shrink-6 up-9 right-12"
+ icon="plus"
+ />
+ </FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index bb7e64bc..e2ba74d1 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -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
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index c9e7506b..364791a1 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -36,7 +36,9 @@
<button
class="button-default btn"
@click="addLanguage"
- >{{ $t('settings.add_language') }}</button>
+ >
+ {{ $t('settings.add_language') }}
+ </button>
</li>
</ul>
</div>
@@ -102,7 +104,7 @@ export default {
</script>
<style lang="scss">
-@import '../../_variables.scss';
+@import "../../variables";
.interface-language-switcher {
.language-select {
diff --git a/src/components/list/list.vue b/src/components/list/list.vue
index f17766b4..a3562c5d 100644
--- a/src/components/list/list.vue
+++ b/src/components/list/list.vue
@@ -1,9 +1,13 @@
<template>
- <div class="list">
+ <div
+ class="list"
+ role="list"
+ >
<div
v-for="item in items"
:key="getKey(item)"
class="list-item"
+ role="listitem"
>
<slot
name="item"
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index cfd42d4c..8c9e5f71 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -23,6 +23,11 @@ const mediaUpload = {
}
},
methods: {
+ onClick () {
+ if (this.uploadReady) {
+ this.$refs.input.click()
+ }
+ },
uploadFile (file) {
const self = this
const store = this.$store
@@ -69,10 +74,15 @@ const mediaUpload = {
this.multiUpload(target.files)
}
},
- props: [
- 'dropFiles',
- 'disabled'
- ],
+ props: {
+ dropFiles: Object,
+ disabled: Boolean,
+ normalButton: Boolean,
+ acceptTypes: {
+ type: String,
+ default: '*/*'
+ }
+ },
watch: {
dropFiles: function (fileInfos) {
if (!this.uploading) {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 2799495b..2ea5567a 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,8 +1,9 @@
<template>
- <label
+ <button
class="media-upload"
- :class="{ disabled: disabled }"
+ :class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]"
:title="$t('tool_tip.media_upload')"
+ @click="onClick"
>
<FAIcon
v-if="uploading"
@@ -15,15 +16,21 @@
class="new-icon"
icon="upload"
/>
+ <template v-if="normalButton">
+ {{ ' ' }}
+ {{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }}
+ </template>
<input
v-if="uploadReady"
+ ref="input"
class="hidden-input-file"
:disabled="disabled"
type="file"
multiple="true"
+ :accept="acceptTypes"
@change="change"
>
- </label>
+ </button>
</template>
<script src="./media_upload.js"></script>
@@ -32,10 +39,12 @@
@import "../../variables";
.media-upload {
- cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
-
.hidden-input-file {
display: none;
}
}
- </style>
+
+label.media-upload {
+ cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
+}
+</style>
diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js
index 7f096316..face430e 100644
--- a/src/components/navigation/navigation.js
+++ b/src/components/navigation/navigation.js
@@ -80,3 +80,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
+}
diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js
index 81cc936a..22ed77d9 100644
--- a/src/components/navigation/navigation_entry.js
+++ b/src/components/navigation/navigation_entry.js
@@ -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
diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js
index 9dd795aa..86c33d1f 100644
--- a/src/components/navigation/navigation_pins.js
+++ b/src/components/navigation/navigation_pins.js
@@ -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: {
@@ -52,6 +45,7 @@ const NavPanel = {
privateMode: state => state.instance.private,
federating: state => state.instance.federating,
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
+ supportsAnnouncements: state => state.announcements.supportsAnnouncements,
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
}),
pinnedList () {
@@ -63,6 +57,7 @@ const NavPanel = {
],
{
hasChats: this.pleromaChatMessagesAvailable,
+ hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
@@ -82,6 +77,7 @@ const NavPanel = {
],
{
hasChats: this.pleromaChatMessagesAvailable,
+ hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index e1ea42ad..6b3315f9 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -121,7 +121,17 @@
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"
+ :alt="notification.emoji"
+ :title="notification.emoji"
+ >
+ <span
+ v-else
+ class="emoji-reaction-emoji"
+ >{{ notification.emoji }}</span>
</i18n-t>
</small>
</span>
@@ -153,9 +163,9 @@
</router-link>
<button
class="button-unstyled expand-icon"
- @click.prevent="toggleStatusExpanded"
:title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded"
+ @click.prevent="toggleStatusExpanded"
>
<FAIcon
class="fa-scale-110"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 41cfcef0..61f7317e 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -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 {
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
index eda1733a..e4d6869a 100644
--- a/src/components/poll/poll.js
+++ b/src/components/poll/poll.js
@@ -12,7 +12,8 @@ export default {
data () {
return {
loading: false,
- choices: []
+ choices: [],
+ randomSeed: `${Math.random()}`.replace('.', '-')
}
},
created () {
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index cacc3298..b3a74c49 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -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>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index d44b266b..bc078533 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -45,6 +45,9 @@ const Popover = {
// Lets hover popover stay when clicking inside of it
stayOnClick: Boolean,
+ // Use styled button (to avoid nested buttons)
+ normalButton: Boolean,
+
triggerAttrs: {
type: Object,
default: {}
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index fd0fd821..1a4bffd9 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -5,7 +5,8 @@
>
<button
ref="trigger"
- class="button-unstyled popover-trigger-button"
+ class="popover-trigger-button"
+ :class="normalButton ? 'button-default btn' : 'button-unstyled'"
type="button"
v-bind="triggerAttrs"
@click="onClick"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index eb55cfcc..b75fee69 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -8,6 +8,7 @@ import Gallery from 'src/components/gallery/gallery.vue'
import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
+import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
@@ -629,6 +630,9 @@ const PostStatusForm = {
},
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
+ },
+ propsToNative (props) {
+ return propsToNative(props)
}
}
}
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index c51639db..86c1f907 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -30,6 +30,9 @@
<span>{{ $t('post_status.scope_notice.public') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
+ :title="$t('post_status.scope_notice_dismiss')"
+ role="button"
+ tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@@ -42,6 +45,9 @@
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
+ :title="$t('post_status.scope_notice_dismiss')"
+ role="button"
+ tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@@ -54,6 +60,9 @@
<span>{{ $t('post_status.scope_notice.private') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
+ :title="$t('post_status.scope_notice_dismiss')"
+ role="button"
+ tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@@ -124,14 +133,17 @@
:suggest="emojiSuggestor"
class="form-control"
>
- <input
- v-model="newStatus.spoilerText"
- type="text"
- :placeholder="$t('post_status.content_warning')"
- :disabled="posting && !optimisticPosting"
- size="1"
- class="form-post-subject"
- >
+ <template #default="inputProps">
+ <input
+ v-model="newStatus.spoilerText"
+ type="text"
+ :placeholder="$t('post_status.content_warning')"
+ :disabled="posting && !optimisticPosting"
+ v-bind="propsToNative(inputProps)"
+ size="1"
+ class="form-post-subject"
+ >
+ </template>
</EmojiInput>
<EmojiInput
ref="emoji-input"
@@ -148,29 +160,32 @@
@sticker-upload-failed="uploadFailed"
@shown="handleEmojiInputShow"
>
- <textarea
- ref="textarea"
- v-model="newStatus.status"
- :placeholder="placeholder || $t('post_status.default')"
- rows="1"
- cols="1"
- :disabled="posting && !optimisticPosting"
- class="form-post-body"
- :class="{ 'scrollable-form': !!maxHeight }"
- @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
- @keydown.meta.enter="postStatus($event, newStatus)"
- @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
- @input="resize"
- @compositionupdate="resize"
- @paste="paste"
- />
- <p
- v-if="hasStatusLengthLimit"
- class="character-counter faint"
- :class="{ error: isOverLengthLimit }"
- >
- {{ charactersLeft }}
- </p>
+ <template #default="inputProps">
+ <textarea
+ ref="textarea"
+ v-model="newStatus.status"
+ :placeholder="placeholder || $t('post_status.default')"
+ rows="1"
+ cols="1"
+ :disabled="posting && !optimisticPosting"
+ class="form-post-body"
+ :class="{ 'scrollable-form': !!maxHeight }"
+ v-bind="propsToNative(inputProps)"
+ @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
+ @keydown.meta.enter="postStatus($event, newStatus)"
+ @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
+ @input="resize"
+ @compositionupdate="resize"
+ @paste="paste"
+ />
+ <p
+ v-if="hasStatusLengthLimit"
+ class="character-counter faint"
+ :class="{ error: isOverLengthLimit }"
+ >
+ {{ charactersLeft }}
+ </p>
+ </template>
</EmojiInput>
<div
v-if="!disableScopeSelector"
@@ -193,6 +208,7 @@
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
+ :attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
>
<option
v-for="postFormat in postFormats"
@@ -265,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') }}
diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue
index f2aa61ee..b81215a1 100644
--- a/src/components/quick_filter_settings/quick_filter_settings.vue
+++ b/src/components/quick_filter_settings/quick_filter_settings.vue
@@ -6,36 +6,51 @@
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
>
<template #content>
- <div class="dropdown-menu">
- <div v-if="loggedIn">
+ <div
+ class="dropdown-menu"
+ role="menu"
+ >
+ <div
+ v-if="loggedIn"
+ role="group"
+ >
<button
v-if="!conversation"
class="button-default dropdown-item"
+ :aria-checked="replyVisibilityAll"
+ role="menuitemradio"
@click="replyVisibilityAll = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
+ :aria-hidden="true"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
+ :aria-checked="replyVisibilityFollowing"
+ role="menuitemradio"
@click="replyVisibilityFollowing = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
+ :aria-hidden="true"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
+ :aria-checked="replyVisibilitySelf"
+ role="menuitemradio"
@click="replyVisibilitySelf = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
+ :aria-hidden="true"
/>{{ $t('settings.reply_visibility_self_short') }}
</button>
<div
@@ -46,33 +61,43 @@
</div>
<button
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="muteBotStatuses"
@click="muteBotStatuses = !muteBotStatuses"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
+ :aria-hidden="true"
/>{{ $t('settings.mute_bot_posts') }}
</button>
<button
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="hideMedia"
@click="hideMedia = !hideMedia"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMedia }"
+ :aria-hidden="true"
/>{{ $t('settings.hide_media_previews') }}
</button>
<button
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="hideMutedPosts"
@click="hideMutedPosts = !hideMutedPosts"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
+ :aria-hidden="true"
/>{{ $t('settings.hide_all_muted_posts') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
+ role="menuitem"
@click="openTab('filtering')"
>
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}
diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue
index 4bd81c5b..9f5cdabc 100644
--- a/src/components/quick_view_settings/quick_view_settings.vue
+++ b/src/components/quick_view_settings/quick_view_settings.vue
@@ -6,60 +6,87 @@
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
>
<template #content>
- <div class="dropdown-menu">
- <button
- class="button-default dropdown-item"
- @click="conversationDisplay = 'tree'"
- >
- <span
- class="menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
- /><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }}
- </button>
- <button
- class="button-default dropdown-item"
- @click="conversationDisplay = 'linear'"
- >
- <span
- class="menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
- /><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }}
- </button>
+ <div
+ class="dropdown-menu"
+ role="menu"
+ >
+ <div role="group">
+ <button
+ class="button-default dropdown-item"
+ :aria-checked="conversationDisplay === 'tree'"
+ role="menuitemradio"
+ @click="conversationDisplay = 'tree'"
+ >
+ <span
+ class="menu-checkbox -radio"
+ :aria-hidden="true"
+ :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
+ /><FAIcon
+ icon="folder-tree"
+ :aria-hidden="true"
+ /> {{ $t('settings.conversation_display_tree_quick') }}
+ </button>
+ <button
+ class="button-default dropdown-item"
+ :aria-checked="conversationDisplay === 'linear'"
+ role="menuitemradio"
+ @click="conversationDisplay = 'linear'"
+ >
+ <span
+ class="menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
+ :aria-hidden="true"
+ /><FAIcon
+ icon="list"
+ :aria-hidden="true"
+ /> {{ $t('settings.conversation_display_linear_quick') }}
+ </button>
+ </div>
<div
role="separator"
class="dropdown-divider"
/>
<button
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="showUserAvatars"
@click="showUserAvatars = !showUserAvatars"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': showUserAvatars }"
+ :aria-hidden="true"
/>{{ $t('settings.mention_link_show_avatar_quick') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="autoUpdate"
@click="autoUpdate = !autoUpdate"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': autoUpdate }"
+ :aria-hidden="true"
/>{{ $t('settings.auto_update') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
+ role="menuitemcheckbox"
+ :aria-checked="collapseWithSubjects"
@click="collapseWithSubjects = !collapseWithSubjects"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
+ :aria-hidden="true"
/>{{ $t('settings.collapse_subject') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
+ role="menuitem"
@click="openTab('general')"
>
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index 1e7e42d5..1e720105 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -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"
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 47a48623..0d252155 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,9 +1,8 @@
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,
@@ -20,105 +19,34 @@ const ReactButton = {
}
},
components: {
- Popover
+ Popover,
+ EmojiPicker
},
methods: {
- addReaction (event, emoji, close) {
+ 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 })
}
- 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.pleromaCustomEmojiReactionsAvailable
}
}
}
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index a813b6fd..947536a1 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -1,73 +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="reaction-picker">
- <span
- v-for="emoji in commonEmojis"
- :key="emoji.replacement"
- class="emoji-button"
- :title="maybeLocalizedEmojiName(emoji)"
- @click="addReaction($event, emoji.replacement, close)"
- >
- {{ 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)"
- >
- {{ 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>
@@ -135,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 {
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 22ca6ad6..b88bdeec 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -16,7 +16,7 @@ const registration = {
confirm: '',
birthday: '',
reason: '',
- language: ''
+ language: ['']
},
captcha: {}
}),
@@ -100,7 +100,7 @@ const registration = {
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
- this.user.language = localeService.internalToBackendLocale(this.user.language)
+ this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k))
}
this.v$.$touch()
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 5701b05e..7438a5f4 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -210,6 +210,7 @@
:prompt-text="$t('registration.email_language')"
:language="v$.user.language.$model"
:set-language="val => v$.user.language.$model = val"
+ @click.stop.prevent
/>
</div>
diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue
index 6e3964b7..60a40a08 100644
--- a/src/components/reply_button/reply_button.vue
+++ b/src/components/reply_button/reply_button.vue
@@ -32,12 +32,20 @@
target="_blank"
role="button"
:href="remoteInteractionLink"
+ :title="$t('tool_tip.reply')"
>
- <FAIcon
- icon="reply"
- class="fa-scale-110 fa-old-padding"
- :title="$t('tool_tip.reply')"
- />
+ <FALayers class="fa-old-padding-layer">
+ <FAIcon
+ class="fa-scale-110"
+ icon="reply"
+ />
+ <FAIcon
+ v-if="!replying"
+ class="focus-marker"
+ transform="shrink-6 up-8 right-16"
+ icon="plus"
+ />
+ </FALayers>
</a>
<span
v-if="status.replies_count > 0"
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 134fcb36..e1b6b153 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -45,13 +45,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
+ :title="$t('tool_tip.repeat')"
:href="remoteInteractionLink"
>
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- icon="retweet"
- :title="$t('tool_tip.repeat')"
- />
+ <FALayers class="fa-old-padding-layer">
+ <FAIcon
+ class="fa-scale-110"
+ icon="retweet"
+ />
+ <FAIcon
+ class="focus-marker"
+ transform="shrink-6 up-9 right-12"
+ icon="plus"
+ />
+ </FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
diff --git a/src/components/screen_reader_notice/screen_reader_notice.js b/src/components/screen_reader_notice/screen_reader_notice.js
new file mode 100644
index 00000000..3b8eaf37
--- /dev/null
+++ b/src/components/screen_reader_notice/screen_reader_notice.js
@@ -0,0 +1,21 @@
+const ScreenReaderNotice = {
+ props: {
+ ariaLive: {
+ type: String,
+ defualt: 'assertive'
+ }
+ },
+ data () {
+ return {
+ currentText: ''
+ }
+ },
+ methods: {
+ announce (text) {
+ this.currentText = text
+ setTimeout(() => { this.currentText = '' }, 1000)
+ }
+ }
+}
+
+export default ScreenReaderNotice
diff --git a/src/components/screen_reader_notice/screen_reader_notice.vue b/src/components/screen_reader_notice/screen_reader_notice.vue
new file mode 100644
index 00000000..8384ae6b
--- /dev/null
+++ b/src/components/screen_reader_notice/screen_reader_notice.vue
@@ -0,0 +1,10 @@
+<template>
+ <div
+ class="visible-for-screenreader-only"
+ :aria-live="ariaLive"
+ >
+ {{ currentText }}
+ </div>
+</template>
+
+<script src="./screen_reader_notice.js"></script>
diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue
index 3969d8de..9da2b272 100644
--- a/src/components/search_bar/search_bar.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -8,6 +8,7 @@
class="button-unstyled nav-icon"
:title="$t('nav.search')"
type="button"
+ :aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon
@@ -29,6 +30,7 @@
<button
class="button-default search-button"
type="submit"
+ :title="$t('nav.search')"
@click="find(searchTerm)"
>
<FAIcon
@@ -39,6 +41,8 @@
<button
class="button-unstyled cancel-search"
type="button"
+ :title="$t('nav.search_close')"
+ :aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon
diff --git a/src/components/select/select.js b/src/components/select/select.js
index ec571a14..34d64fd2 100644
--- a/src/components/select/select.js
+++ b/src/components/select/select.js
@@ -13,6 +13,7 @@ export default {
'modelValue',
'disabled',
'unstyled',
- 'kind'
+ 'kind',
+ 'attrs'
]
}
diff --git a/src/components/select/select.vue b/src/components/select/select.vue
index 0ce4ea82..1797afc8 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -6,6 +6,7 @@
<select
:disabled="disabled"
:value="modelValue"
+ v-bind="attrs"
@change="$emit('update:modelValue', $event.target.value)"
>
<slot />
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
new file mode 100644
index 00000000..a2c27c2a
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -0,0 +1,64 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+import GroupSetting from '../helpers/group_setting.vue'
+import Popover from 'src/components/popover/popover.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const FrontendsTab = {
+ provide () {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin'
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting,
+ GroupSetting,
+ Popover
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadFrontendsStuff')
+ }
+ },
+ computed: {
+ frontends () {
+ return this.$store.state.adminSettings.frontends
+ },
+ ...SharedComputedObject()
+ },
+ methods: {
+ update (frontend, suggestRef) {
+ const ref = suggestRef || frontend.refs[0]
+ const { name } = frontend
+ const payload = { name, ref }
+
+ this.$store.state.api.backendInteractor.installFrontend({ payload })
+ .then((externalUser) => {
+ this.$store.dispatch('loadFrontendsStuff')
+ })
+ },
+ setDefault (frontend, suggestRef) {
+ const ref = suggestRef || frontend.refs[0]
+ const { name } = frontend
+
+ this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
+ }
+ }
+}
+
+export default FrontendsTab
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.scss b/src/components/settings_modal/admin_tabs/frontends_tab.scss
new file mode 100644
index 00000000..e3e04bc6
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.scss
@@ -0,0 +1,13 @@
+.frontends-tab {
+ .cards-list {
+ padding: 0;
+ }
+
+ dd {
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+ white-space: nowrap;
+ overflow-x: hidden;
+ max-width: 10em;
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
new file mode 100644
index 00000000..13b8fa6b
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -0,0 +1,184 @@
+<template>
+ <div
+ class="frontends-tab"
+ :label="$t('admin_dash.tabs.frontends')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
+ <p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
+ <ul class="setting-list">
+ <li>
+ <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
+ <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
+ <p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p>
+ <ul class="setting-list">
+ <li>
+ <StringSetting path=":pleroma.:frontends.:primary.name" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:frontends.:primary.ref" />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:frontends.:primary" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <div class="setting-list">
+ <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
+ <ul class="cards-list">
+ <li
+ v-for="frontend in frontends"
+ :key="frontend.name"
+ >
+ <strong>{{ frontend.name }}</strong>
+ {{ ' ' }}
+ <span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name">
+ <i18n-t
+ v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]"
+ keypath="admin_dash.frontend.is_default"
+ />
+ <i18n-t
+ v-else
+ keypath="admin_dash.frontend.is_default_custom"
+ >
+ <template #version>
+ <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
+ </template>
+ </i18n-t>
+ </span>
+ <dl>
+ <dt>{{ $t('admin_dash.frontend.repository') }}</dt>
+ <dd>
+ <a
+ :href="frontend.git"
+ target="_blank"
+ >{{ frontend.git }}</a>
+ </dd>
+ <template v-if="expertLevel">
+ <dt>{{ $t('admin_dash.frontend.versions') }}</dt>
+ <dd
+ v-for="ref in frontend.refs"
+ :key="ref"
+ >
+ <code>{{ ref }}</code>
+ </dd>
+ </template>
+ <dt v-if="expertLevel">
+ {{ $t('admin_dash.frontend.build_url') }}
+ </dt>
+ <dd v-if="expertLevel">
+ <a
+ :href="frontend.build_url"
+ target="_blank"
+ >{{ frontend.build_url }}</a>
+ </dd>
+ </dl>
+ <div>
+ <span class="btn-group">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="update(frontend)"
+ >
+ {{
+ frontend.installed
+ ? $t('admin_dash.frontend.reinstall')
+ : $t('admin_dash.frontend.install')
+ }}
+ </button>
+ <Popover
+ v-if="frontend.refs.length > 1"
+ trigger="click"
+ class="button-dropdown"
+ placement="bottom"
+ >
+ <template #content>
+ <div class="dropdown-menu">
+ <button
+ v-for="ref in frontend.refs"
+ :key="ref"
+ class="button-default dropdown-item"
+ @click="update(frontend, ref)"
+ >
+ <i18n-t keypath="admin_dash.frontend.install_version">
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </button>
+ </div>
+ </template>
+ <template #trigger>
+ <button
+ class="button button-default btn dropdown-button"
+ type="button"
+ :title="$t('admin_dash.frontend.more_install_options')"
+ >
+ <FAIcon icon="chevron-down" />
+ </button>
+ </template>
+ </Popover>
+ </span>
+ <span
+ v-if="frontend.installed && frontend.name !== 'admin-fe'"
+ class="btn-group"
+ >
+ <button
+ class="button button-default btn"
+ type="button"
+ :disabled="
+ adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name &&
+ adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]
+ "
+ @click="setDefault(frontend)"
+ >
+ {{
+ $t('admin_dash.frontend.set_default')
+ }}
+ </button>
+ {{ ' ' }}
+ <Popover
+ v-if="frontend.refs.length > 1"
+ trigger="click"
+ class="button-dropdown"
+ placement="bottom"
+ >
+ <template #content>
+ <div class="dropdown-menu">
+ <button
+ v-for="ref in frontend.refs.slice(1)"
+ :key="ref"
+ class="button-default dropdown-item"
+ @click="setDefault(frontend, ref)"
+ >
+ <i18n-t keypath="admin_dash.frontend.set_default_version">
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </button>
+ </div>
+ </template>
+ <template #trigger>
+ <button
+ class="button button-default btn dropdown-button"
+ type="button"
+ :title="$t('admin_dash.frontend.more_default_options')"
+ >
+ <FAIcon icon="chevron-down" />
+ </button>
+ </template>
+ </Popover>
+ </span>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./frontends_tab.js"></script>
+
+<style lang="scss" src="./frontends_tab.scss"></style>
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js
new file mode 100644
index 00000000..b07bafe8
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/instance_tab.js
@@ -0,0 +1,38 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+import GroupSetting from '../helpers/group_setting.vue'
+import AttachmentSetting from '../helpers/attachment_setting.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const InstanceTab = {
+ provide () {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin'
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting,
+ AttachmentSetting,
+ GroupSetting
+ },
+ computed: {
+ ...SharedComputedObject()
+ }
+}
+
+export default InstanceTab
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue
new file mode 100644
index 00000000..a6be776b
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/instance_tab.vue
@@ -0,0 +1,196 @@
+<template>
+ <div :label="$t('admin_dash.tabs.instance')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.instance') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <StringSetting path=":pleroma.:instance.:name" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:email" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:description" />
+ </li>
+ <li>
+ <StringSetting path=":pleroma.:instance.:short_description" />
+ </li>
+ <li>
+ <AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" />
+ </li>
+ <li>
+ <AttachmentSetting path=":pleroma.:instance.:background_image" />
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.registrations') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:registrations_open" />
+ <ul class="setting-list suboptions">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:instance.:invites_enabled"
+ parent-path=":pleroma.:instance.:registrations_open"
+ parent-invert
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:birthday_required" />
+ <ul class="setting-list suboptions">
+ <li>
+ <IntegerSetting
+ path=":pleroma.:instance.:birthday_min_age"
+ parent-path=":pleroma.:instance.:birthday_required"
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:account_activation_required" />
+ </li>
+ <li>
+ <BooleanSetting path=":pleroma.:instance.:account_approval_required" />
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
+ <ul class="setting-list suboptions">
+ <li>
+ <ChoiceSetting
+ :path="[':pleroma', 'Pleroma.Captcha', ':method']"
+ :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
+ :option-label-map="{
+ 'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
+ 'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
+ }"
+ />
+ <IntegerSetting
+ :path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
+ :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
+ />
+ </li>
+ <li
+ v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
+ >
+ <h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.instance.access') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ override-backend-description
+ override-backend-description-label
+ path=":pleroma.:instance.:public"
+ />
+ </li>
+ <li>
+ <ChoiceSetting
+ override-backend-description
+ override-backend-description-label
+ path=":pleroma.:instance.:limit_to_local_content"
+ />
+ </li>
+ <li v-if="expertLevel">
+ <h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
+ <p>
+ {{ $t('admin_dash.instance.restrict.description') }}
+ </p>
+ <ul class="setting-list">
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:timelines.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:profiles.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:activities.:local"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <BooleanSetting
+ path=":pleroma.:restrict_unauthenticated.:activities.:remote"
+ indeterminate-state=":if_instance_is_private"
+ swap-description-and-label
+ hide-description
+ />
+ </li>
+ <li>
+ <GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
+
+<script src="./instance_tab.js"></script>
diff --git a/src/components/settings_modal/admin_tabs/limits_tab.js b/src/components/settings_modal/admin_tabs/limits_tab.js
new file mode 100644
index 00000000..684739c3
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/limits_tab.js
@@ -0,0 +1,29 @@
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import StringSetting from '../helpers/string_setting.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faGlobe
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faGlobe
+)
+
+const LimitsTab = {
+ data () {},
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ StringSetting
+ },
+ computed: {
+ ...SharedComputedObject()
+ }
+}
+
+export default LimitsTab
diff --git a/src/components/settings_modal/admin_tabs/limits_tab.vue b/src/components/settings_modal/admin_tabs/limits_tab.vue
new file mode 100644
index 00000000..ef4b9271
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/limits_tab.vue
@@ -0,0 +1,136 @@
+<template>
+ <div :label="$t('admin_dash.tabs.limits')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <h3>{{ $t('admin_dash.limits.posts') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:remote_limit"
+ expert="1"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.limits.uploads') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:description_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:upload_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_media_attachments"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h3>{{ $t('admin_dash.limits.users') }}</h3>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_pinned_statuses"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:user_bio_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:user_name_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_account_fields"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:max_remote_account_fields"
+ draft-mode
+ expert="1"
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:account_field_name_length"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:account_field_value_length"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:avatar_upload_limit"
+ draft-mode
+ />
+ </li>
+ <li>
+ <IntegerSetting
+ source="admin"
+ path=":pleroma.:instance.:banner_upload_limit"
+ draft-mode
+ />
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
+
+<script src="./limits_tab.js"></script>
diff --git a/src/components/settings_modal/helpers/attachment_setting.js b/src/components/settings_modal/helpers/attachment_setting.js
new file mode 100644
index 00000000..ac5c6f86
--- /dev/null
+++ b/src/components/settings_modal/helpers/attachment_setting.js
@@ -0,0 +1,43 @@
+import Setting from './setting.js'
+import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
+import MediaUpload from 'src/components/media_upload/media_upload.vue'
+import Attachment from 'src/components/attachment/attachment.vue'
+
+export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ acceptTypes: {
+ type: String,
+ required: false,
+ default: 'image/*'
+ }
+ },
+ components: {
+ ...Setting.components,
+ MediaUpload,
+ Attachment
+ },
+ computed: {
+ ...Setting.computed,
+ attachment () {
+ const path = this.realDraftMode ? this.draft : this.state
+ // The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
+ const url = path.includes('://') ? path : this.$store.state.instance.server + path
+ return {
+ mimetype: fileTypeExt(url),
+ url
+ }
+ }
+ },
+ methods: {
+ ...Setting.methods,
+ setMediaFile (fileInfo) {
+ if (this.realDraftMode) {
+ this.draft = fileInfo.url
+ } else {
+ this.configSink(this.path, fileInfo.url)
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue
new file mode 100644
index 00000000..bbc5172c
--- /dev/null
+++ b/src/components/settings_modal/helpers/attachment_setting.vue
@@ -0,0 +1,96 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="AttachmentSetting"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+
+ </label>
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ <div class="attachment-input">
+ <div>{{ $t('settings.url') }}</div>
+ <div class="controls">
+ <input
+ :id="path"
+ class="string-input"
+ :disabled="shouldBeDisabled"
+ :value="realDraftMode ? draft : state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ </div>
+ <div>{{ $t('settings.preview') }}</div>
+ <Attachment
+ class="attachment"
+ :compact="compact"
+ :attachment="attachment"
+ size="small"
+ hide-description
+ @setMedia="onMedia"
+ @naturalSizeLoad="onNaturalSizeLoad"
+ />
+ <div class="controls">
+ <MediaUpload
+ ref="mediaUpload"
+ class="media-upload-icon"
+ :drop-files="dropFiles"
+ normal-button
+ :accept-types="acceptTypes"
+ @uploaded="setMediaFile"
+ @upload-failed="uploadFailed"
+ />
+ </div>
+ </div>
+ <DraftButtons />
+ </span>
+</template>
+
+<script src="./attachment_setting.js"></script>
+
+<style lang="scss">
+.AttachmentSetting {
+ .attachment {
+ display: block;
+ width: 100%;
+ height: 15em;
+ margin-bottom: 0.5em;
+ }
+
+ .attachment-input {
+ margin-left: 1em;
+ display: flex;
+ flex-direction: column;
+ width: 20em;
+ }
+
+ .controls {
+ margin-bottom: 0.5em;
+
+ input,
+ button {
+ width: 100%;
+ }
+ }
+}
+</style>
diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js
index 2e6992cb..199d3d0f 100644
--- a/src/components/settings_modal/helpers/boolean_setting.js
+++ b/src/components/settings_modal/helpers/boolean_setting.js
@@ -1,56 +1,31 @@
-import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
-import ModifiedIndicator from './modified_indicator.vue'
-import ServerSideIndicator from './server_side_indicator.vue'
+import Setting from './setting.js'
+
export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ indeterminateState: [String, Object]
+ },
components: {
- Checkbox,
- ModifiedIndicator,
- ServerSideIndicator
+ ...Setting.components,
+ Checkbox
},
- props: [
- 'path',
- 'disabled',
- 'expert'
- ],
computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isServerSide () {
- return this.path.startsWith('serverSide_')
- },
- isChanged () {
- return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
+ ...Setting.computed,
+ isIndeterminate () {
+ return this.visibleState === this.indeterminateState
}
},
methods: {
- update (e) {
- const [firstSegment, ...rest] = this.path.split('.')
- set(this.$parent, this.path, e)
- // Updating nested properties does not trigger update on its parent.
- // probably still not as reliable, but works for depth=1 at least
- if (rest.length > 0) {
- set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
+ ...Setting.methods,
+ getValue (e) {
+ // Basic tri-state toggle implementation
+ if (!!this.indeterminateState && !e && this.visibleState === true) {
+ // If we have indeterminate state, switching from true to false first goes through indeterminate
+ return this.indeterminateState
}
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
+ return e
}
}
}
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index 41142966..5a9eab34 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -4,23 +4,37 @@
class="BooleanSetting"
>
<Checkbox
- :model-value="state"
- :disabled="disabled"
+ :model-value="visibleState"
+ :disabled="shouldBeDisabled"
+ :indeterminate="isIndeterminate"
@update:modelValue="update"
>
<span
- v-if="!!$slots.default"
class="label"
+ :class="{ 'faint': shouldBeDisabled }"
>
- <slot />
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
</span>
- {{ ' ' }}
- <ModifiedIndicator
- :changed="isChanged"
- :onclick="reset"
- />
- <ServerSideIndicator :server-side="isServerSide" />
</Checkbox>
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
</label>
</template>
diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js
index 3da559fe..bdeece76 100644
--- a/src/components/settings_modal/helpers/choice_setting.js
+++ b/src/components/settings_modal/helpers/choice_setting.js
@@ -1,51 +1,41 @@
-import { get, set } from 'lodash'
import Select from 'src/components/select/select.vue'
-import ModifiedIndicator from './modified_indicator.vue'
-import ServerSideIndicator from './server_side_indicator.vue'
+import Setting from './setting.js'
+
export default {
+ ...Setting,
components: {
- Select,
- ModifiedIndicator,
- ServerSideIndicator
+ ...Setting.components,
+ Select
},
- props: [
- 'path',
- 'disabled',
- 'options',
- 'expert'
- ],
- computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
+ props: {
+ ...Setting.props,
+ options: {
+ type: Array,
+ required: false
},
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
+ optionLabelMap: {
+ type: Object,
+ required: false,
+ default: {}
+ }
+ },
+ computed: {
+ ...Setting.computed,
+ realOptions () {
+ if (this.realSource === 'admin') {
+ return this.backendDescriptionSuggestions.map(x => ({
+ key: x,
+ value: x,
+ label: this.optionLabelMap[x] || x
+ }))
}
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isServerSide () {
- return this.path.startsWith('serverSide_')
- },
- isChanged () {
- return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
+ return this.options
}
},
methods: {
- update (e) {
- set(this.$parent, this.path, e)
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
+ ...Setting.methods,
+ getValue (e) {
+ return e
}
}
}
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index 8fdbb5d3..114e9b7d 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -3,15 +3,20 @@
v-if="matchesExpertLevel"
class="ChoiceSetting"
>
- <slot />
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel }}
+ </template>
+ <template v-else>
+ <slot />
+ </template>
{{ ' ' }}
<Select
- :model-value="state"
+ :model-value="realDraftMode ? draft :state"
:disabled="disabled"
@update:modelValue="update"
>
<option
- v-for="option in options"
+ v-for="option in realOptions"
:key="option.key"
:value="option.value"
>
@@ -23,7 +28,14 @@
:changed="isChanged"
:onclick="reset"
/>
- <ServerSideIndicator :server-side="isServerSide" />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
</label>
</template>
diff --git a/src/components/settings_modal/helpers/draft_buttons.vue b/src/components/settings_modal/helpers/draft_buttons.vue
new file mode 100644
index 00000000..46a70e86
--- /dev/null
+++ b/src/components/settings_modal/helpers/draft_buttons.vue
@@ -0,0 +1,88 @@
+<!-- this is a helper exclusive to Setting components -->
+<!-- TODO make it reusable -->
+<template>
+ <span
+ class="DraftButtons"
+ >
+ <Popover
+ v-if="$parent.isDirty"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
+ @click="$parent.commitDraft"
+ >
+ <template #trigger>
+ {{ $t('settings.commit_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.commit_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ <Popover
+ v-if="$parent.isDirty"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
+ @click="$parent.reset"
+ >
+ <template #trigger>
+ {{ $t('settings.reset_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.reset_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ <Popover
+ v-if="$parent.canHardReset"
+ trigger="hover"
+ normal-button
+ :trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
+ @click="$parent.hardReset"
+ >
+ <template #trigger>
+ {{ $t('settings.hard_reset_value') }}
+ </template>
+ <template #content>
+ <div class="modified-tooltip">
+ {{ $t('settings.hard_reset_value_tooltip') }}
+ </div>
+ </template>
+ </Popover>
+ </span>
+</template>
+
+<script>
+import Popover from 'src/components/popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faWrench } from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faWrench
+)
+
+export default {
+ components: { Popover },
+ props: ['changed']
+}
+</script>
+
+<style lang="scss">
+.DraftButtons {
+ display: inline-block;
+ position: relative;
+
+ .button-default {
+ margin-left: 0.5em;
+ }
+}
+
+.draft-tooltip {
+ margin: 0.5em 1em;
+ min-width: 10em;
+ text-align: center;
+}
+</style>
diff --git a/src/components/settings_modal/helpers/float_setting.vue b/src/components/settings_modal/helpers/float_setting.vue
new file mode 100644
index 00000000..15edb3c3
--- /dev/null
+++ b/src/components/settings_modal/helpers/float_setting.vue
@@ -0,0 +1,16 @@
+<template>
+ <NumberSetting
+ v-bind="$attrs"
+ >
+ <slot />
+ </NumberSetting>
+</template>
+
+<script>
+import NumberSetting from './number_setting.vue'
+export default {
+ components: {
+ NumberSetting
+ }
+}
+</script>
diff --git a/src/components/settings_modal/helpers/group_setting.js b/src/components/settings_modal/helpers/group_setting.js
new file mode 100644
index 00000000..23a2a202
--- /dev/null
+++ b/src/components/settings_modal/helpers/group_setting.js
@@ -0,0 +1,13 @@
+import { isEqual } from 'lodash'
+
+import Setting from './setting.js'
+
+export default {
+ ...Setting,
+ computed: {
+ ...Setting.computed,
+ isDirty () {
+ return !isEqual(this.state, this.draft)
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/group_setting.vue b/src/components/settings_modal/helpers/group_setting.vue
new file mode 100644
index 00000000..a4df4bf3
--- /dev/null
+++ b/src/components/settings_modal/helpers/group_setting.vue
@@ -0,0 +1,15 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="GroupSetting"
+ >
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ </span>
+</template>
+
+<script src="./group_setting.js"></script>
diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
deleted file mode 100644
index e64d0cee..00000000
--- a/src/components/settings_modal/helpers/integer_setting.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { get, set } from 'lodash'
-import ModifiedIndicator from './modified_indicator.vue'
-export default {
- components: {
- ModifiedIndicator
- },
- props: {
- path: String,
- disabled: Boolean,
- min: Number,
- expert: [Number, String]
- },
- computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isChanged () {
- return this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
- }
- },
- methods: {
- update (e) {
- set(this.$parent, this.path, parseInt(e.target.value))
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
- }
- }
-}
diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue
index 695e2673..43fa7e1a 100644
--- a/src/components/settings_modal/helpers/integer_setting.vue
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -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>
diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue
index 8311533a..45db3fc2 100644
--- a/src/components/settings_modal/helpers/modified_indicator.vue
+++ b/src/components/settings_modal/helpers/modified_indicator.vue
@@ -5,12 +5,12 @@
>
<Popover
trigger="hover"
+ :trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon
icon="wrench"
- :aria-label="$t('settings.setting_changed')"
/>
</template>
<template #content>
diff --git a/src/components/settings_modal/helpers/number_setting.js b/src/components/settings_modal/helpers/number_setting.js
new file mode 100644
index 00000000..676a0d22
--- /dev/null
+++ b/src/components/settings_modal/helpers/number_setting.js
@@ -0,0 +1,24 @@
+import Setting from './setting.js'
+
+export default {
+ ...Setting,
+ props: {
+ ...Setting.props,
+ truncate: {
+ type: Number,
+ required: false,
+ default: 1
+ }
+ },
+ methods: {
+ ...Setting.methods,
+ getValue (e) {
+ if (!this.truncate === 1) {
+ return parseInt(e.target.value)
+ } else if (this.truncate > 1) {
+ return Math.trunc(e.target.value / this.truncate) * this.truncate
+ }
+ return parseFloat(e.target.value)
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue
new file mode 100644
index 00000000..93f11331
--- /dev/null
+++ b/src/components/settings_modal/helpers/number_setting.vue
@@ -0,0 +1,45 @@
+<template>
+ <span
+ v-if="matchesExpertLevel"
+ class="NumberSetting"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+ </label>
+ <input
+ :id="path"
+ class="number-input"
+ type="number"
+ :step="step || 1"
+ :disabled="shouldBeDisabled"
+ :min="min || 0"
+ :value="realDraftMode ? draft :state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ </span>
+</template>
+
+<script src="./number_setting.js"></script>
diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/profile_setting_indicator.vue
index bf181959..d160781b 100644
--- a/src/components/settings_modal/helpers/server_side_indicator.vue
+++ b/src/components/settings_modal/helpers/profile_setting_indicator.vue
@@ -1,7 +1,7 @@
<template>
<span
- v-if="serverSide"
- class="ServerSideIndicator"
+ v-if="isProfile"
+ class="ProfileSettingIndicator"
>
<Popover
trigger="hover"
@@ -14,7 +14,7 @@
/>
</template>
<template #content>
- <div class="serverside-tooltip">
+ <div class="profilesetting-tooltip">
{{ $t('settings.setting_server_side') }}
</div>
</template>
@@ -33,17 +33,17 @@ library.add(
export default {
components: { Popover },
- props: ['serverSide']
+ props: ['isProfile']
}
</script>
<style lang="scss">
-.ServerSideIndicator {
+.ProfileSettingIndicator {
display: inline-block;
position: relative;
}
-.serverside-tooltip {
+.profilesetting-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
new file mode 100644
index 00000000..b3add346
--- /dev/null
+++ b/src/components/settings_modal/helpers/setting.js
@@ -0,0 +1,237 @@
+import ModifiedIndicator from './modified_indicator.vue'
+import ProfileSettingIndicator from './profile_setting_indicator.vue'
+import DraftButtons from './draft_buttons.vue'
+import { get, set, cloneDeep } from 'lodash'
+
+export default {
+ components: {
+ ModifiedIndicator,
+ DraftButtons,
+ ProfileSettingIndicator
+ },
+ props: {
+ path: {
+ type: [String, Array],
+ required: true
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ parentPath: {
+ type: [String, Array]
+ },
+ parentInvert: {
+ type: Boolean,
+ default: false
+ },
+ expert: {
+ type: [Number, String],
+ default: 0
+ },
+ source: {
+ type: String,
+ default: undefined
+ },
+ hideDescription: {
+ type: Boolean
+ },
+ swapDescriptionAndLabel: {
+ type: Boolean
+ },
+ overrideBackendDescription: {
+ type: Boolean
+ },
+ overrideBackendDescriptionLabel: {
+ type: Boolean
+ },
+ draftMode: {
+ type: Boolean,
+ default: undefined
+ }
+ },
+ inject: {
+ defaultSource: {
+ default: 'default'
+ },
+ defaultDraftMode: {
+ default: false
+ }
+ },
+ data () {
+ return {
+ localDraft: null
+ }
+ },
+ created () {
+ if (this.realDraftMode && this.realSource !== 'admin') {
+ this.draft = this.state
+ }
+ },
+ computed: {
+ draft: {
+ // TODO allow passing shared draft object?
+ get () {
+ if (this.realSource === 'admin') {
+ return get(this.$store.state.adminSettings.draft, this.canonPath)
+ } else {
+ return this.localDraft
+ }
+ },
+ set (value) {
+ if (this.realSource === 'admin') {
+ this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
+ } else {
+ this.localDraft = value
+ }
+ }
+ },
+ state () {
+ const value = get(this.configSource, this.canonPath)
+ if (value === undefined) {
+ return this.defaultState
+ } else {
+ return value
+ }
+ },
+ visibleState () {
+ return this.realDraftMode ? this.draft : this.state
+ },
+ realSource () {
+ return this.source || this.defaultSource
+ },
+ realDraftMode () {
+ return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
+ },
+ backendDescription () {
+ return get(this.$store.state.adminSettings.descriptions, this.path)
+ },
+ backendDescriptionLabel () {
+ if (this.realSource !== 'admin') return ''
+ if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
+ return this.$t([
+ 'admin_dash',
+ 'temp_overrides',
+ ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
+ 'label'
+ ].join('.'))
+ } else {
+ return this.swapDescriptionAndLabel
+ ? this.backendDescription?.description
+ : this.backendDescription?.label
+ }
+ },
+ backendDescriptionDescription () {
+ if (this.realSource !== 'admin') return ''
+ if (this.hideDescription) return null
+ if (!this.backendDescription || this.overrideBackendDescription) {
+ return this.$t([
+ 'admin_dash',
+ 'temp_overrides',
+ ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
+ 'description'
+ ].join('.'))
+ } else {
+ return this.swapDescriptionAndLabel
+ ? this.backendDescription?.label
+ : this.backendDescription?.description
+ }
+ },
+ backendDescriptionSuggestions () {
+ return this.backendDescription?.suggestions
+ },
+ shouldBeDisabled () {
+ const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
+ return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
+ },
+ configSource () {
+ switch (this.realSource) {
+ case 'profile':
+ return this.$store.state.profileConfig
+ case 'admin':
+ return this.$store.state.adminSettings.config
+ default:
+ return this.$store.getters.mergedConfig
+ }
+ },
+ configSink () {
+ switch (this.realSource) {
+ case 'profile':
+ return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
+ case 'admin':
+ return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
+ default:
+ return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
+ }
+ },
+ defaultState () {
+ switch (this.realSource) {
+ case 'profile':
+ return {}
+ default:
+ return get(this.$store.getters.defaultConfig, this.path)
+ }
+ },
+ isProfileSetting () {
+ return this.realSource === 'profile'
+ },
+ isChanged () {
+ switch (this.realSource) {
+ case 'profile':
+ case 'admin':
+ return false
+ default:
+ return this.state !== this.defaultState
+ }
+ },
+ canonPath () {
+ return Array.isArray(this.path) ? this.path : this.path.split('.')
+ },
+ isDirty () {
+ if (this.realSource === 'admin' && this.canonPath.length > 3) {
+ return false // should not show draft buttons for "grouped" values
+ } else {
+ return this.realDraftMode && this.draft !== this.state
+ }
+ },
+ canHardReset () {
+ return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
+ },
+ matchesExpertLevel () {
+ return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
+ }
+ },
+ methods: {
+ getValue (e) {
+ return e.target.value
+ },
+ update (e) {
+ if (this.realDraftMode) {
+ this.draft = this.getValue(e)
+ } else {
+ this.configSink(this.path, this.getValue(e))
+ }
+ },
+ commitDraft () {
+ if (this.realDraftMode) {
+ this.configSink(this.path, this.draft)
+ }
+ },
+ reset () {
+ if (this.realDraftMode) {
+ this.draft = cloneDeep(this.state)
+ } else {
+ set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
+ }
+ },
+ hardReset () {
+ switch (this.realSource) {
+ case 'admin':
+ return this.$store.dispatch('resetAdminSetting', { path: this.path })
+ .then(() => { this.draft = this.state })
+ default:
+ console.warn('Hard reset not implemented yet!')
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index 12431dca..bb3d36ac 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -1,52 +1,18 @@
-import { defaultState as configDefaultState } from 'src/modules/config.js'
-import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
-
const SharedComputedObject = () => ({
user () {
return this.$store.state.users.currentUser
},
- // Getting values for default properties
- ...Object.keys(configDefaultState)
- .map(key => [
- key + 'DefaultValue',
- function () {
- return this.$store.getters.defaultConfig[key]
- }
- ])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Generating computed values for vuex properties
- ...Object.keys(configDefaultState)
- .map(key => [key, {
- get () { return this.$store.getters.mergedConfig[key] },
- set (value) {
- this.$store.dispatch('setOption', { name: key, value })
- }
- }])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- ...Object.keys(serverSideConfigDefaultState)
- .map(key => ['serverSide_' + key, {
- get () { return this.$store.state.serverSideConfig[key] },
- set (value) {
- this.$store.dispatch('setServerSideOption', { name: key, value })
- }
- }])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Special cases (need to transform values or perform actions first)
- useStreamingApi: {
- get () { return this.$store.getters.mergedConfig.useStreamingApi },
- set (value) {
- const promise = value
- ? this.$store.dispatch('enableMastoSockets')
- : this.$store.dispatch('disableMastoSockets')
-
- promise.then(() => {
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
- }).catch((e) => {
- console.error('Failed starting MastoAPI Streaming socket', e)
- this.$store.dispatch('disableMastoSockets')
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
- })
- }
+ expertLevel () {
+ return this.$store.getters.mergedConfig.expertLevel > 0
+ },
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ },
+ adminConfig () {
+ return this.$store.state.adminSettings.config
+ },
+ adminDraft () {
+ return this.$store.state.adminSettings.draft
}
})
diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js
index 58697412..12cef705 100644
--- a/src/components/settings_modal/helpers/size_setting.js
+++ b/src/components/settings_modal/helpers/size_setting.js
@@ -1,67 +1,40 @@
-import { get, set } from 'lodash'
-import ModifiedIndicator from './modified_indicator.vue'
import Select from 'src/components/select/select.vue'
+import Setting from './setting.js'
export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
export const defaultVerticalUnits = ['px', 'rem', 'vh']
export default {
+ ...Setting,
components: {
- ModifiedIndicator,
+ ...Setting.components,
Select
},
props: {
- path: String,
- disabled: Boolean,
+ ...Setting.props,
min: Number,
units: {
- type: [String],
+ type: Array,
default: () => allCssUnits
- },
- expert: [Number, String]
+ }
},
computed: {
- pathDefault () {
- const [firstSegment, ...rest] = this.path.split('.')
- return [firstSegment + 'DefaultValue', ...rest].join('.')
- },
+ ...Setting.computed,
stateUnit () {
- return (this.state || '').replace(/\d+/, '')
+ return this.state.replace(/\d+/, '')
},
stateValue () {
- return (this.state || '').replace(/\D+/, '')
- },
- state () {
- const value = get(this.$parent, this.path)
- if (value === undefined) {
- return this.defaultState
- } else {
- return value
- }
- },
- defaultState () {
- return get(this.$parent, this.pathDefault)
- },
- isChanged () {
- return this.state !== this.defaultState
- },
- matchesExpertLevel () {
- return (this.expert || 0) <= this.$parent.expertLevel
+ return this.state.replace(/\D+/, '')
}
},
methods: {
- update (e) {
- set(this.$parent, this.path, e)
- },
- reset () {
- set(this.$parent, this.path, this.defaultState)
- },
+ ...Setting.methods,
updateValue (e) {
- set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit)
+ this.configSink(this.path, parseInt(e.target.value) + this.stateUnit)
},
updateUnit (e) {
- set(this.$parent, this.path, this.stateValue + e.target.value)
+ this.configSink(this.path, this.stateValue + e.target.value)
}
}
}
diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue
index 5a78f100..6c3fbaeb 100644
--- a/src/components/settings_modal/helpers/size_setting.vue
+++ b/src/components/settings_modal/helpers/size_setting.vue
@@ -45,11 +45,18 @@
<script src="./size_setting.js"></script>
<style lang="scss">
-.css-unit-input,
-.css-unit-input select {
- margin-left: 0.5em;
- width: 4em;
- max-width: 4em;
- min-width: 4em;
+.SizeSetting {
+ .number-input {
+ max-width: 6.5em;
+ }
+
+ .css-unit-input,
+ .css-unit-input select {
+ margin-left: 0.5em;
+ width: 4em;
+ max-width: 4em;
+ min-width: 4em;
+ }
}
+
</style>
diff --git a/src/components/settings_modal/helpers/string_setting.js b/src/components/settings_modal/helpers/string_setting.js
new file mode 100644
index 00000000..b368cfc8
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.js
@@ -0,0 +1,5 @@
+import Setting from './setting.js'
+
+export default {
+ ...Setting
+}
diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue
new file mode 100644
index 00000000..0cfa61ce
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.vue
@@ -0,0 +1,42 @@
+<template>
+ <label
+ v-if="matchesExpertLevel"
+ class="StringSetting"
+ >
+ <label
+ :for="path"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ <template v-if="backendDescriptionLabel">
+ {{ backendDescriptionLabel + ' ' }}
+ </template>
+ <template v-else-if="source === 'admin'">
+ MISSING LABEL FOR {{ path }}
+ </template>
+ <slot v-else />
+ </label>
+ <input
+ :id="path"
+ class="string-input"
+ :disabled="shouldBeDisabled"
+ :value="realDraftMode ? draft : state"
+ @change="update"
+ >
+ {{ ' ' }}
+ <ModifiedIndicator
+ :changed="isChanged"
+ :onclick="reset"
+ />
+ <ProfileSettingIndicator :is-profile="isProfileSetting" />
+ <DraftButtons />
+ <p
+ v-if="backendDescriptionDescription"
+ class="setting-description"
+ :class="{ 'faint': shouldBeDisabled }"
+ >
+ {{ backendDescriptionDescription + ' ' }}
+ </p>
+ </label>
+</template>
+
+<script src="./string_setting.js"></script>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 0a72dca1..ff58f2c3 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, isEqual } from 'lodash'
import {
newImporter,
newExporter
@@ -53,8 +53,16 @@ const SettingsModal = {
Modal,
Popover,
Checkbox,
- SettingsModalContent: getResettableAsyncComponent(
- () => import('./settings_modal_content.vue'),
+ SettingsModalUserContent: getResettableAsyncComponent(
+ () => import('./settings_modal_user_content.vue'),
+ {
+ loadingComponent: PanelLoading,
+ errorComponent: AsyncComponentError,
+ delay: 0
+ }
+ ),
+ SettingsModalAdminContent: getResettableAsyncComponent(
+ () => import('./settings_modal_admin_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
@@ -147,6 +155,12 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION
]
return clone
+ },
+ resetAdminDraft () {
+ this.$store.commit('resetAdminDraft')
+ },
+ pushAdminDraft () {
+ this.$store.dispatch('pushAdminDraft')
}
},
computed: {
@@ -156,8 +170,14 @@ const SettingsModal = {
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
- modalOpenedOnce () {
- return this.$store.state.interface.settingsModalLoaded
+ modalMode () {
+ return this.$store.state.interface.settingsModalMode
+ },
+ modalOpenedOnceUser () {
+ return this.$store.state.interface.settingsModalLoadedUser
+ },
+ modalOpenedOnceAdmin () {
+ return this.$store.state.interface.settingsModalLoadedAdmin
},
modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized'
@@ -167,9 +187,14 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0
},
set (value) {
- console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
+ },
+ adminDraftAny () {
+ return !isEqual(
+ this.$store.state.adminSettings.config,
+ this.$store.state.adminSettings.draft
+ )
}
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index f5861229..49ef83e0 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -17,6 +17,12 @@
}
}
+ .setting-description {
+ margin-top: 0.2em;
+ margin-bottom: 2em;
+ font-size: 70%;
+ }
+
.settings-modal-panel {
overflow: hidden;
transition: transform;
@@ -37,7 +43,9 @@
.btn {
min-height: 2em;
- min-width: 10em;
+ }
+
+ .btn:not(.dropdown-button) {
padding: 0 2em;
}
}
@@ -45,6 +53,8 @@
.settings-footer {
display: flex;
+ flex-wrap: wrap;
+ line-height: 2;
>* {
margin-right: 0.5em;
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 7b457371..4e7fd931 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -8,7 +8,7 @@
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
- {{ $t('settings.settings') }}
+ {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
</span>
<transition name="fade">
<div
@@ -42,10 +42,12 @@
</button>
</div>
<div class="panel-body">
- <SettingsModalContent v-if="modalOpenedOnce" />
+ <SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
+ <SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
</div>
- <div class="panel-footer settings-footer">
+ <div class="panel-footer settings-footer -flexible-height">
<Popover
+ v-if="modalMode === 'user'"
class="export"
trigger="click"
placement="top"
@@ -107,10 +109,42 @@
>
{{ $t("settings.expert_mode") }}
</Checkbox>
+ <span v-if="modalMode === 'admin'">
+ <i18n-t keypath="admin_dash.wip_notice">
+ <template #adminFeLink>
+ <a
+ href="/pleroma/admin/#/login-pleroma"
+ target="_blank"
+ >
+ {{ $t("admin_dash.old_ui_link") }}
+ </a>
+ </template>
+ </i18n-t>
+ </span>
<span
id="unscrolled-content"
class="extra-content"
/>
+ <span
+ v-if="modalMode === 'admin'"
+ class="admin-buttons"
+ >
+ <button
+ class="button-default btn"
+ :disabled="!adminDraftAny"
+ @click="resetAdminDraft"
+ >
+ {{ $t("admin_dash.reset_all") }}
+ </button>
+ {{ ' ' }}
+ <button
+ class="button-default btn"
+ :disabled="!adminDraftAny"
+ @click="pushAdminDraft"
+ >
+ {{ $t("admin_dash.commit_all") }}
+ </button>
+ </span>
</div>
</div>
</Modal>
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
new file mode 100644
index 00000000..f94721ec
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -0,0 +1,93 @@
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import InstanceTab from './admin_tabs/instance_tab.vue'
+import LimitsTab from './admin_tabs/limits_tab.vue'
+import FrontendsTab from './admin_tabs/frontends_tab.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+)
+
+const SettingsModalAdminContent = {
+ components: {
+ TabSwitcher,
+
+ InstanceTab,
+ LimitsTab,
+ FrontendsTab
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ isLoggedIn () {
+ return !!this.$store.state.users.currentUser
+ },
+ open () {
+ return this.$store.state.interface.settingsModalState !== 'hidden'
+ },
+ bodyLock () {
+ return this.$store.state.interface.settingsModalState === 'visible'
+ },
+ adminDbLoaded () {
+ return this.$store.state.adminSettings.loaded
+ },
+ adminDescriptionsLoaded () {
+ return this.$store.state.adminSettings.descriptions !== null
+ },
+ noDb () {
+ return this.$store.state.adminSettings.dbConfigEnabled === false
+ }
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadAdminStuff')
+ }
+ },
+ methods: {
+ onOpen () {
+ const targetTab = this.$store.state.interface.settingsModalTargetTab
+ // We're being told to open in specific tab
+ if (targetTab) {
+ const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
+ return elm.props && elm.props['data-tab-name'] === targetTab
+ })
+ if (tabIndex >= 0) {
+ this.$refs.tabSwitcher.setTab(tabIndex)
+ }
+ }
+ // Clear the state of target tab, so that next time settings is opened
+ // it doesn't force it.
+ this.$store.dispatch('clearSettingsModalTargetTab')
+ }
+ },
+ mounted () {
+ this.onOpen()
+ },
+ watch: {
+ open: function (value) {
+ if (value) this.onOpen()
+ }
+ }
+}
+
+export default SettingsModalAdminContent
diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss
index 87df7982..c984d703 100644
--- a/src/components/settings_modal/settings_modal_content.scss
+++ b/src/components/settings_modal/settings_modal_admin_content.scss
@@ -48,9 +48,5 @@
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
-
- .number-input {
- max-width: 6em;
- }
}
}
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
new file mode 100644
index 00000000..a7a2ac9a
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -0,0 +1,68 @@
+<template>
+ <tab-switcher
+ v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
+ ref="tabSwitcher"
+ class="settings_tab-switcher"
+ :side-tab-bar="true"
+ :scrollable-tabs="true"
+ :render-only-focused="true"
+ :body-scroll-lock="bodyLock"
+ >
+ <div
+ v-if="noDb"
+ :label="$t('admin_dash.tabs.nodb')"
+ icon="exclamation-triangle"
+ data-tab-name="nodb-notice"
+ >
+ <div :label="$t('admin_dash.tabs.nodb')">
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.nodb.heading') }}</h2>
+ <i18n-t keypath="admin_dash.nodb.text">
+ <template #documentation>
+ <a
+ href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
+ target="_blank"
+ >
+ {{ $t("admin_dash.nodb.documentation") }}
+ </a>
+ </template>
+ <template #property>
+ <code>config :pleroma, configurable_from_database</code>
+ </template>
+ <template #value>
+ <code>true</code>
+ </template>
+ </i18n-t>
+ <p>{{ $t('admin_dash.nodb.text2') }}</p>
+ </div>
+ </div>
+ </div>
+ <div
+ v-if="adminDbLoaded"
+ :label="$t('admin_dash.tabs.instance')"
+ icon="wrench"
+ data-tab-name="general"
+ >
+ <InstanceTab />
+ </div>
+ <div
+ v-if="adminDbLoaded"
+ :label="$t('admin_dash.tabs.limits')"
+ icon="hand"
+ data-tab-name="limits"
+ >
+ <LimitsTab />
+ </div>
+ <div
+ :label="$t('admin_dash.tabs.frontends')"
+ icon="laptop-code"
+ data-tab-name="frontends"
+ >
+ <FrontendsTab />
+ </div>
+ </tab-switcher>
+</template>
+
+<script src="./settings_modal_admin_content.js"></script>
+
+<style src="./settings_modal_admin_content.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_user_content.js
index 9ac0301f..9ac0301f 100644
--- a/src/components/settings_modal/settings_modal_content.js
+++ b/src/components/settings_modal/settings_modal_user_content.js
diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss
new file mode 100644
index 00000000..c984d703
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_user_content.scss
@@ -0,0 +1,52 @@
+@import "src/variables";
+
+.settings_tab-switcher {
+ height: 100%;
+
+ .setting-item {
+ border-bottom: 2px solid var(--fg, $fallback--fg);
+ margin: 1em 1em 1.4em;
+ padding-bottom: 1.4em;
+
+ > div,
+ > label {
+ display: block;
+ margin-bottom: 0.5em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .select-multiple {
+ display: flex;
+
+ .option-list {
+ margin: 0;
+ padding-left: 0.5em;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ margin-bottom: 1em;
+ }
+
+ select {
+ min-width: 10em;
+ }
+
+ textarea {
+ width: 100%;
+ max-width: 100%;
+ height: 100px;
+ }
+
+ .unavailable,
+ .unavailable svg {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+ }
+}
diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_user_content.vue
index 0be76d22..0221cccb 100644
--- a/src/components/settings_modal/settings_modal_content.vue
+++ b/src/components/settings_modal/settings_modal_user_content.vue
@@ -78,6 +78,6 @@
</tab-switcher>
</template>
-<script src="./settings_modal_content.js"></script>
+<script src="./settings_modal_user_content.js"></script>
-<style src="./settings_modal_content.scss" lang="scss"></style>
+<style src="./settings_modal_user_content.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 97046ff0..41d1b54f 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -7,13 +7,11 @@
<BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideWordFilteredPosts"
>
{{ $t('settings.hide_wordfiltered_statuses') }}
@@ -22,7 +20,8 @@
<li>
<BooleanSetting
v-if="user"
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideMutedThreads"
>
{{ $t('settings.hide_muted_threads') }}
@@ -31,7 +30,8 @@
<li>
<BooleanSetting
v-if="user"
- :disabled="hideFilteredStatuses"
+ parent-path="hideFilteredStatuses"
+ :parent-invert="true"
path="hideMutedPosts"
>
{{ $t('settings.hide_muted_posts') }}
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index ea24d6ad..3f2bcb13 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -2,11 +2,12 @@ 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'
import SharedComputedObject from '../helpers/shared_computed_object.js'
-import ServerSideIndicator from '../helpers/server_side_indicator.vue'
+import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
@@ -62,10 +63,11 @@ const GeneralTab = {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
+ FloatSetting,
SizeSetting,
InterfaceLanguageSwitcher,
ScopeSelector,
- ServerSideIndicator
+ ProfileSettingIndicator
},
computed: {
horizontalUnits () {
@@ -108,7 +110,7 @@ const GeneralTab = {
},
methods: {
changeDefaultScope (value) {
- this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
+ this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
}
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 703e94a0..f56fa8e0 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -29,14 +29,11 @@
<BooleanSetting path="streaming">
{{ $t('settings.streaming') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
path="pauseOnUnfocused"
- :disabled="!streaming"
+ parent-path="streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</BooleanSetting>
@@ -213,7 +210,7 @@
</ChoiceSetting>
</li>
<ul
- v-if="conversationDisplay !== 'linear'"
+ v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
@@ -265,12 +262,22 @@
<li>
<BooleanSetting
v-if="user"
- path="serverSide_stripRichContent"
+ source="profile"
+ path="stripRichContent"
expert="1"
>
{{ $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
@@ -290,7 +297,7 @@
<BooleanSetting
path="preloadImage"
expert="1"
- :disabled="!hideNsfw"
+ parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
@@ -299,7 +306,7 @@
<BooleanSetting
path="useOneClickNsfw"
expert="1"
- :disabled="!hideNsfw"
+ parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
@@ -312,15 +319,13 @@
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !streaming}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
- :disabled="!loopVideo || !loopSilentAvailable"
+ parent-path="loopVideo"
+ :disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
@@ -418,18 +423,18 @@
<ul class="setting-list">
<li>
<label for="default-vis">
- {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
+ {{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
- :user-default="serverSide_defaultScope"
- :initial-scope="serverSide_defaultScope"
+ :user-default="$store.state.profileConfig.defaultScope"
+ :initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
/>
</label>
</li>
<li>
- <!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
+ <!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
@@ -501,6 +506,14 @@
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
+ <li>
+ <BooleanSetting
+ path="autocompleteSelect"
+ expert="1"
+ >
+ {{ $t('settings.autocomplete_select_first') }}
+ </BooleanSetting>
+ </li>
</ul>
</div>
</div>
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
index 6cfeea35..51974f9f 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
import Checkbox from 'src/components/checkbox/checkbox.vue'
-const BlockList = withSubscription({
+const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+ destroy: () => {},
childPropName: 'items'
})(SelectableList)
-const MuteList = withSubscription({
+const MuteList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+ destroy: () => {},
childPropName: 'items'
})(SelectableList)
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index dd3806ed..fcb92135 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -4,7 +4,10 @@
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list">
<li>
- <BooleanSetting path="serverSide_blockNotificationsFromStrangers">
+ <BooleanSetting
+ source="profile"
+ path="blockNotificationsFromStrangers"
+ >
{{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting>
</li>
@@ -67,7 +70,8 @@
</li>
<li>
<BooleanSetting
- path="serverSide_webPushHideContents"
+ source="profile"
+ path="webPushHideContents"
expert="1"
>
{{ $t('settings.notification_setting_hide_notification_contents') }}
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 72d27083..eeacad48 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
+import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -261,6 +262,9 @@ const ProfileTab = {
messageArgs: [error.message],
level: 'error'
})
+ },
+ propsToNative (props) {
+ return propsToNative(props)
}
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index faa05bcc..1cc850cb 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -8,11 +8,14 @@
enable-emoji-picker
:suggest="emojiSuggestor"
>
- <input
- id="username"
- v-model="newName"
- class="name-changer"
- >
+ <template #default="inputProps">
+ <input
+ id="username"
+ v-model="newName"
+ class="name-changer"
+ v-bind="propsToNative(inputProps)"
+ >
+ </template>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
@@ -20,10 +23,13 @@
enable-emoji-picker
:suggest="emojiUserSuggestor"
>
- <textarea
- v-model="newBio"
- class="bio resize-height"
- />
+ <template #default="inputProps">
+ <textarea
+ v-model="newBio"
+ class="bio resize-height"
+ v-bind="propsToNative(inputProps)"
+ />
+ </template>
</EmojiInput>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
@@ -60,10 +66,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
- <input
- v-model="newFields[i].name"
- :placeholder="$t('settings.profile_fields.name')"
- >
+ <template #default="inputProps">
+ <input
+ v-model="newFields[i].name"
+ :placeholder="$t('settings.profile_fields.name')"
+ v-bind="propsToNative(inputProps)"
+ >
+ </template>
</EmojiInput>
<EmojiInput
v-model="newFields[i].value"
@@ -71,10 +80,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
- <input
- v-model="newFields[i].value"
- :placeholder="$t('settings.profile_fields.value')"
- >
+ <template #default="inputProps">
+ <input
+ v-model="newFields[i].value"
+ :placeholder="$t('settings.profile_fields.value')"
+ v-bind="propsToNative(inputProps)"
+ >
+ </template>
</EmojiInput>
<button
class="delete-field button-unstyled -hover-highlight"
@@ -242,37 +254,50 @@
<h2>{{ $t('settings.account_privacy') }}</h2>
<ul class="setting-list">
<li>
- <BooleanSetting path="serverSide_locked">
+ <BooleanSetting
+ source="profile"
+ path="locked"
+ >
{{ $t('settings.lock_account_description') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_discoverable">
+ <BooleanSetting
+ source="profile"
+ path="discoverable"
+ >
{{ $t('settings.discoverable') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_allowFollowingMove">
+ <BooleanSetting
+ source="profile"
+ path="allowFollowingMove"
+ >
{{ $t('settings.allow_following_move') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_hideFavorites">
+ <BooleanSetting
+ source="profile"
+ path="hideFavorites"
+ >
{{ $t('settings.hide_favorites_description') }}
</BooleanSetting>
</li>
<li>
- <BooleanSetting path="serverSide_hideFollowers">
+ <BooleanSetting
+ source="profile"
+ path="hideFollowers"
+ >
{{ $t('settings.hide_followers_description') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !serverSide_hideFollowers}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- path="serverSide_hideFollowersCount"
- :disabled="!serverSide_hideFollowers"
+ source="profile"
+ path="hideFollowersCount"
+ parent-path="hideFollowers"
>
{{ $t('settings.hide_followers_count_description') }}
</BooleanSetting>
@@ -280,17 +305,18 @@
</ul>
</li>
<li>
- <BooleanSetting path="serverSide_hideFollows">
+ <BooleanSetting
+ source="profile"
+ path="hideFollows"
+ >
{{ $t('settings.hide_follows_description') }}
</BooleanSetting>
- <ul
- class="setting-list suboptions"
- :class="[{disabled: !serverSide_hideFollows}]"
- >
+ <ul class="setting-list suboptions">
<li>
<BooleanSetting
- path="serverSide_hideFollowsCount"
- :disabled="!serverSide_hideFollows"
+ source="profile"
+ path="hideFollowsCount"
+ parent-path="hideFollows"
>
{{ $t('settings.hide_follows_count_description') }}
</BooleanSetting>
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
index 6e03bef4..d36d478f 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.vue
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -143,8 +143,8 @@
/>
</div>
<div>
- <i18n
- path="settings.new_alias_target"
+ <i18n-t
+ keypath="settings.new_alias_target"
tag="p"
>
<code
@@ -152,7 +152,7 @@
>
foo@example.org
</code>
- </i18n>
+ </i18n-t>
<input
v-model="addAliasTarget"
>
@@ -175,16 +175,16 @@
<h2>{{ $t('settings.move_account') }}</h2>
<p>{{ $t('settings.move_account_notes') }}</p>
<div>
- <i18n
- path="settings.move_account_target"
+ <i18n-t
+ keypath="settings.move_account_target"
tag="p"
>
- <code
- place="example"
- >
- foo@example.org
- </code>
- </i18n>
+ <template #example>
+ <code>
+ foo@example.org
+ </code>
+ </template>
+ </i18n-t>
<input
v-model="moveAccountTarget"
>
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index 7546535d..1f3c26aa 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -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
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 27019577..81c5a612 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -115,7 +115,10 @@ const SideDrawer = {
GestureService.updateSwipe(e, this.closeGesture)
},
openSettingsModal () {
- this.$store.dispatch('openSettingsModal')
+ this.$store.dispatch('openSettingsModal', 'user')
+ },
+ openAdminModal () {
+ this.$store.dispatch('openSettingsModal', 'admin')
}
}
}
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 994ac953..09588767 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -180,16 +180,16 @@
v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer"
>
- <a
- href="/pleroma/admin/#/login-pleroma"
- target="_blank"
+ <button
+ class="button-unstyled -link -fullwidth"
+ @click.stop="openAdminModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
/> {{ $t("nav.administration") }}
- </a>
+ </button>
</li>
<li
v-if="currentUser && supportsAnnouncements"
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
index c8d390bc..b444da43 100644
--- a/src/components/tab_switcher/tab_switcher.jsx
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -60,13 +60,7 @@ export default {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
- },
- settingsModalVisible () {
- return this.settingsModalState === 'visible'
- },
- ...mapState({
- settingsModalState: state => state.interface.settingsModalState
- })
+ }
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
@@ -117,6 +111,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 +126,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 +163,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}
>