diff options
Diffstat (limited to 'src')
44 files changed, 1261 insertions, 381 deletions
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 dc8bbfd3..49382f8e 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -48,20 +48,19 @@ icon="cog" /> </button> - <a + <button v-if="currentUser && currentUser.role === 'admin'" - href="/pleroma/admin/#/login-pleroma" - class="nav-icon" + class="button-unstyled nav-icon" target="_blank" :title="$t('nav.administration')" - @click.stop + @click.stop="openAdminModal" > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="tachometer-alt" /> - </a> + </button> <span class="spacer" /> <button v-if="currentUser" diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index a57e8761..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> diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 4d801c5e..13e7f9b8 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -162,8 +162,8 @@ </router-link> <button class="button-unstyled expand-icon" - :aria-expanded="statusExpanded" :title="$t('tool_tip.toggle_expand')" + :aria-expanded="statusExpanded" @click.prevent="toggleStatusExpanded" > <FAIcon 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..4a261659 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_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 InstanceTab = { + data () {}, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting + }, + 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..ad271293 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -0,0 +1,119 @@ +<template> + <div :label="$t('admin_dash.instance')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance') }}</h2> + <ul class="setting-list"> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:name" + draft-mode + > + NAME + </StringSetting> + </li> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:email" + draft-mode + > + ADMIN EMAIL + </StringSetting> + </li> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:description" + draft-mode + > + DESCRIPTION + </StringSetting> + </li> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:short_description" + draft-mode + > + SHORT DESCRIPTION + </StringSetting> + </li> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:instance_thumbnail" + draft-mode + > + INSTANCE THUMBNAIL + </StringSetting> + </li> + <li> + <StringSetting + source="admin" + path=":pleroma.:instance.:background_image" + draft-mode + > + BACKGROUND IMAGE + </StringSetting> + </li> + <li> + <BooleanSetting + source="admin" + path=":pleroma.:instance.:public" + draft-mode + > + PUBLIC + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('admin_dash.registrations') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting + source="admin" + path=":pleroma.:instance.:registrations_open" + draft-mode + > + REGISTRATIONS OPEN + </BooleanSetting> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + source="admin" + path=":pleroma.:instance.:invites_enabled" + parent-path=":pleroma.:instance.:registrations_open" + :parent-invert="true" + draft-mode + > + INVITES ENABLED + </BooleanSetting> + </li> + </ul> + </li> + <li> + <BooleanSetting + source="admin" + path=":pleroma.:instance.:account_activation_required" + draft-mode + > + ACTIVATION REQUIRED + </BooleanSetting> + </li> + <li> + <BooleanSetting + source="admin" + path=":pleroma.:instance.:account_approval_required" + draft-mode + > + APPROVAL REQUIRED + </BooleanSetting> + </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..3f07c554 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/limits_tab.vue @@ -0,0 +1,152 @@ +<template> + <div :label="$t('admin_dash.instance')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.arbitrary_limits') }}</h2> + <ul class="setting-list"> + <li> + <h3>{{ $t('admin_dash.limits.posts') }}</h3> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:limit" + draft-mode + > + POST LIMIT + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:remote_limit" + draft-mode + > + POST LIMIT (remote) + </IntegerSetting> + </li> + <li> + <h3>{{ $t('admin_dash.limits.uploads') }}</h3> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:description_limit" + draft-mode + > + IMAGE DESCRIPTION LIMIT + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:upload_limit" + draft-mode + > + UPLOAD LIMIT KiB + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_media_attachments" + draft-mode + > + MAX ATTACHMENTS + </IntegerSetting> + </li> + <li> + <h3>{{ $t('admin_dash.limits.users') }}</h3> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_bio_length" + draft-mode + > + BIO LENGTH + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_name_length" + draft-mode + > + NAME LENGTH + </IntegerSetting> + </li> + <li> + <h4>{{ $t('admin_dash.limits.profile_fields') }}</h4> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_account_fields" + draft-mode + > + MAX ACCOUNT FIELDS + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_remote_account_fields" + draft-mode + > + MAX ACCOUNT FIELDS (remote) + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_name_length" + draft-mode + > + MAX ACCOUNT FIELD NAME + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_value_length" + draft-mode + > + MAX ACCOUNT VALUE NAME + </IntegerSetting> + </li> + <li> + <h4>{{ $t('admin_dash.limits.user_uploads') }}</h4> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:avatar_upload_limit" + draft-mode + > + MAX AVATAR SIZE KiB + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:banner_upload_limit" + draft-mode + > + MAX BANNER SIZE KiB + </IntegerSetting> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_pinned_statuses" + draft-mode + > + MAX PINNED POSTS + </IntegerSetting> + </li> + </ul> + </div> + </div> +</template> + +<script src="./limits_tab.js"></script> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js index 2e6992cb..0df01d31 100644 --- a/src/components/settings_modal/helpers/boolean_setting.js +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -1,56 +1,16 @@ -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, components: { - Checkbox, - ModifiedIndicator, - ServerSideIndicator - }, - 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.components, + Checkbox }, 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) }) - } - }, - reset () { - set(this.$parent, this.path, this.defaultState) + ...Setting.methods, + getValue (e) { + return e } } } diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index 41142966..aedbf23e 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -4,22 +4,34 @@ class="BooleanSetting" > <Checkbox - :model-value="state" - :disabled="disabled" + :model-value="draftMode ? draft :state" + :disabled="shouldBeDisabled" @update:modelValue="update" > <span v-if="!!$slots.default" class="label" > - <slot /> + <template v-if="backendDescription"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else> + <slot /> + </template> </span> {{ ' ' }} <ModifiedIndicator :changed="isChanged" :onclick="reset" /> - <ServerSideIndicator :server-side="isServerSide" /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + > + {{ backendDescriptionDescription + ' ' }} + </p> </Checkbox> </label> </template> diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index 3da559fe..a3c3bf44 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -1,51 +1,17 @@ -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('.') - }, - 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 - } - }, - methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) + props: { + ...Setting.props, + options: { + type: Array, + required: true } } } diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index 8fdbb5d3..4c4cdefe 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -23,7 +23,7 @@ :changed="isChanged" :onclick="reset" /> - <ServerSideIndicator :server-side="isServerSide" /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> </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..53ddc9b4 --- /dev/null +++ b/src/components/settings_modal/helpers/draft_buttons.vue @@ -0,0 +1,102 @@ +<!-- this is a helper exclusive to Setting components --> +<!-- TODO make it reusable --> +<template> + <span + class="DraftButtons" + > + <Popover + trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }" + > + <template #trigger> + + <button + v-if="$parent.isDirty" + class="button button-default btn" + type="button" + :title="$t('settings.commit_value')" + @click="$parent.commitDraft" + > + {{ $t('settings.commit_value') }} + </button> + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.commit_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }" + > + <template #trigger> + + <button + v-if="$parent.isDirty" + class="button button-default btn" + type="button" + :title="$t('settings.reset_value')" + @click="$parent.reset" + > + {{ $t('settings.reset_value') }} + </button> + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.reset_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }" + > + <template #trigger> + + <button + v-if="$parent.canHardReset" + class="button button-default btn" + type="button" + :title="$t('settings.hard_reset_value')" + @click="$parent.hardReset" + > + {{ $t('settings.hard_reset_value') }} + </button> + </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; +} + +.draft-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; +} +</style> diff --git a/src/components/settings_modal/helpers/number_setting.js b/src/components/settings_modal/helpers/number_setting.js index 73c39948..676a0d22 100644 --- a/src/components/settings_modal/helpers/number_setting.js +++ b/src/components/settings_modal/helpers/number_setting.js @@ -1,56 +1,24 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' +import Setting from './setting.js' + export default { - components: { - ModifiedIndicator - }, + ...Setting, props: { - path: String, - disabled: Boolean, - min: Number, - step: Number, - truncate: Number, - expert: [Number, String] - }, - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - parent () { - return this.$parent.$parent - }, - 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 + ...Setting.props, + truncate: { + type: Number, + required: false, + default: 1 } }, methods: { - truncateValue (value) { - if (!this.truncate) { - return value + ...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 Math.trunc(value / this.truncate) * this.truncate - }, - update (e) { - set(this.parent, this.path, this.truncateValue(parseFloat(e.target.value))) - }, - reset () { - set(this.parent, this.path, this.defaultState) + 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 index 3eab5178..81335762 100644 --- a/src/components/settings_modal/helpers/number_setting.vue +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -13,7 +13,7 @@ :step="step || 1" :disabled="disabled" :min="min || 0" - :value="state" + :value="draftMode ? draft :state" @change="update" > {{ ' ' }} 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..8c7074a5 --- /dev/null +++ b/src/components/settings_modal/helpers/setting.js @@ -0,0 +1,161 @@ +import Checkbox from 'src/components/checkbox/checkbox.vue' +import ModifiedIndicator from './modified_indicator.vue' +import ProfileSettingIndicator from './profile_setting_indicator.vue' +import DraftButtons from './draft_buttons.vue' +import { get, set } from 'lodash' + +export default { + components: { + Checkbox, + ModifiedIndicator, + DraftButtons, + ProfileSettingIndicator + }, + props: { + path: { + type: String, + required: true + }, + disabled: { + type: Boolean, + default: false + }, + parentPath: { + type: String + }, + parentInvert: { + type: Boolean, + default: false + }, + expert: { + type: [Number, String], + default: 0 + }, + source: { + type: String, + default: 'default' + }, + draftMode: { + type: Boolean, + default: false + } + }, + data () { + return { + draft: null + } + }, + created () { + if (this.draftMode) { + this.draft = this.state + } + }, + computed: { + state () { + const value = get(this.configSource, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + backendDescription () { + return get(this.$store.state.adminSettings.descriptions, this.path) + }, + backendDescriptionLabel () { + return this.backendDescription?.label + }, + backendDescriptionDescription () { + return this.backendDescription?.description + }, + 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.source) { + case 'profile': + return this.$store.state.profileConfig + case 'admin': + return this.$store.state.adminSettings.config + default: + return this.$store.getters.mergedConfig + } + }, + configSink () { + switch (this.source) { + 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.source) { + case 'profile': + return {} + default: + return get(this.$store.getters.defaultConfig, this.path) + } + }, + isProfileSetting () { + return this.source === 'profile' + }, + isChanged () { + switch (this.source) { + case 'profile': + case 'admin': + return false + default: + return this.state !== this.defaultState + } + }, + isDirty () { + return this.draftMode && this.draft !== this.state + }, + canHardReset () { + return this.source === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.path) + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 + } + }, + methods: { + getValue (e) { + return e.target.value + }, + update (e) { + if (this.draftMode) { + this.draft = this.getValue(e) + } else { + this.configSink(this.path, this.getValue(e)) + } + }, + commitDraft () { + if (this.draftMode) { + this.configSink(this.path, this.draft) + } + }, + reset () { + console.log('reset') + if (this.draftMode) { + console.log(this.draft) + console.log(this.state) + this.draft = this.state + } else { + set(this.$store.getters.mergedConfig, this.path, this.defaultState) + } + }, + hardReset () { + switch (this.source) { + 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..912999ce 100644 --- a/src/components/settings_modal/helpers/shared_computed_object.js +++ b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,52 +1,12 @@ -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 } }) 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/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..91a4afa4 --- /dev/null +++ b/src/components/settings_modal/helpers/string_setting.vue @@ -0,0 +1,38 @@ +<template> + <label + v-if="matchesExpertLevel" + class="StringSetting" + > + <label :for="path"> + <template v-if="backendDescription"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else> + <slot /> + </template> + </label> + <input + :id="path" + class="string-input" + step="1" + :disabled="disabled" + :value="draftMode ? draft :state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + > + {{ 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..e033d999 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -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, @@ -156,8 +164,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,7 +181,6 @@ const SettingsModal = { return this.$store.state.config.expertLevel > 0 }, set (value) { - console.log(value) this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) } } diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index f5861229..4cce6099 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; diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 7b457371..73e1524a 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -42,7 +42,8 @@ </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"> <Popover 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..c6c8837f --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -0,0 +1,78 @@ +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' + +import DataImportExportTab from './tabs/data_import_export_tab.vue' +import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue' +import InstanceTab from './admin_tabs/instance_tab.vue' +import LimitsTab from './admin_tabs/limits_tab.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +) + +const SettingsModalAdminContent = { + components: { + TabSwitcher, + + DataImportExportTab, + MutesAndBlocksTab, + InstanceTab, + LimitsTab + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + open () { + return this.$store.state.interface.settingsModalState !== 'hidden' + }, + bodyLock () { + return this.$store.state.interface.settingsModalState === 'visible' + } + }, + 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..87df7982 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_admin_content.scss 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..16b55828 --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -0,0 +1,28 @@ +<template> + <tab-switcher + ref="tabSwitcher" + class="settings_tab-switcher" + :side-tab-bar="true" + :scrollable-tabs="true" + :body-scroll-lock="bodyLock" + > + <div + :label="$t('admin_dash.instance')" + icon="wrench" + data-tab-name="general" + > + <InstanceTab /> + </div> + <div + :label="$t('admin_dash.limits')" + icon="wrench" + data-tab-name="limits" + > + <LimitsTab /> + </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..87df7982 --- /dev/null +++ b/src/components/settings_modal/settings_modal_user_content.scss @@ -0,0 +1,56 @@ +@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; + } + + .number-input { + max-width: 6em; + } + } +} 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 be97710f..3f2bcb13 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -7,7 +7,7 @@ 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 @@ -67,7 +67,7 @@ const GeneralTab = { SizeSetting, InterfaceLanguageSwitcher, ScopeSelector, - ServerSideIndicator + ProfileSettingIndicator }, computed: { horizontalUnits () { @@ -110,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 21e2d855..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,7 +262,8 @@ <li> <BooleanSetting v-if="user" - path="serverSide_stripRichContent" + source="profile" + path="stripRichContent" expert="1" > {{ $t('settings.no_rich_text_description') }} @@ -299,7 +297,7 @@ <BooleanSetting path="preloadImage" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.preload_images') }} </BooleanSetting> @@ -308,7 +306,7 @@ <BooleanSetting path="useOneClickNsfw" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} </BooleanSetting> @@ -321,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> @@ -427,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> 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.vue b/src/components/settings_modal/tabs/profile_tab.vue index 6a5b478a..1cc850cb 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -254,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> @@ -292,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/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index a7ef8560..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] diff --git a/src/main.js b/src/main.js index d3e60a0f..71f19a82 100644 --- a/src/main.js +++ b/src/main.js @@ -10,8 +10,9 @@ import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' -import serverSideConfigModule from './modules/serverSideConfig.js' +import profileConfigModule from './modules/profileConfig.js' import serverSideStorageModule from './modules/serverSideStorage.js' +import adminSettingsModule from './modules/adminSettings.js' import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' @@ -80,8 +81,9 @@ const persistedStateOptions = { lists: listsModule, api: apiModule, config: configModule, - serverSideConfig: serverSideConfigModule, + profileConfig: profileConfigModule, serverSideStorage: serverSideStorageModule, + adminSettings: adminSettingsModule, shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js new file mode 100644 index 00000000..44a4d409 --- /dev/null +++ b/src/modules/adminSettings.js @@ -0,0 +1,115 @@ +import { set, cloneDeep } from 'lodash' + +export const defaultState = { + needsReboot: null, + config: null, + modifiedPaths: null, + descriptions: null +} + +export const newUserFlags = { + ...defaultState.flagStorage +} + +const adminSettingsStorage = { + state: { + ...cloneDeep(defaultState) + }, + mutations: { + updateAdminSettings (state, { config, modifiedPaths }) { + state.config = config + state.modifiedPaths = modifiedPaths + }, + updateAdminDescriptions (state, { descriptions }) { + state.descriptions = descriptions + } + }, + actions: { + setInstanceAdminSettings ({ state, commit, dispatch }, { backendDbConfig }) { + const config = state.config || {} + const modifiedPaths = state.modifiedPaths || new Set() + backendDbConfig.configs.forEach(c => { + const path = c.group + '.' + c.key + if (c.db) { + c.db.forEach(x => modifiedPaths.add(path + '.' + x)) + } + const convert = (value) => { + if (Array.isArray(value) && value.length > 0 && value[0].tuple) { + return value.reduce((acc, c) => { + return { ...acc, [c.tuple[0]]: convert(c.tuple[1]) } + }, {}) + } else { + return value + } + } + set(config, path, convert(c.value)) + }) + console.log(config[':pleroma'][':welcome']) + commit('updateAdminSettings', { config, modifiedPaths }) + }, + setInstanceAdminDescriptions ({ state, commit, dispatch }, { backendDescriptions }) { + const convert = ({ children, description, label, key = '<ROOT>', group, suggestions }, path, acc) => { + const newPath = group ? group + '.' + key : key + const obj = { description, label, suggestions } + if (Array.isArray(children)) { + children.forEach(c => { + convert(c, '.' + newPath, obj) + }) + } + set(acc, newPath, obj) + } + + const descriptions = {} + backendDescriptions.forEach(d => convert(d, '', descriptions)) + commit('updateAdminDescriptions', { descriptions }) + }, + pushAdminSetting ({ rootState, state, commit, dispatch }, { path, value }) { + const [group, key, ...rest] = path.split(/\./g) + const clone = {} // not actually cloning the entire thing to avoid excessive writes + set(clone, rest.join('.'), value) + + // TODO cleanup paths in modifiedPaths + const convert = (value) => { + if (typeof value !== 'object') { + return value + } else if (Array.isArray(value)) { + return value.map(convert) + } else { + return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] })) + } + } + + rootState.api.backendInteractor.pushInstanceDBConfig({ + payload: { + configs: [{ + group, + key, + value: convert(clone) + }] + } + }) + .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig()) + .then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig })) + }, + resetAdminSetting ({ rootState, state, commit, dispatch }, { path }) { + const [group, key, subkey] = path.split(/\./g) + + state.modifiedPaths.delete(path) + + return rootState.api.backendInteractor.pushInstanceDBConfig({ + payload: { + configs: [{ + group, + key, + delete: true, + subkeys: [subkey] + }] + } + }) + .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig()) + .then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig })) + } + } +} + +export default adminSettingsStorage diff --git a/src/modules/config.js b/src/modules/config.js index 7597886e..56f8cba5 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,6 +1,7 @@ import Cookies from 'js-cookie' import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' +import { set } from 'lodash' import localeService from '../services/locale/locale.service.js' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' @@ -148,7 +149,7 @@ const config = { }, mutations: { setOption (state, { name, value }) { - state[name] = value + set(state, name, value) }, setHighlight (state, { user, color, type }) { const data = this.state.config.highlight[user] @@ -178,32 +179,52 @@ const config = { commit('setHighlight', { user, color, type }) }, setOption ({ commit, dispatch, state }, { name, value }) { - commit('setOption', { name, value }) - switch (name) { - case 'theme': - setPreset(value) - break - case 'sidebarColumnWidth': - case 'contentColumnWidth': - case 'notifsColumnWidth': - case 'emojiReactionsScale': - applyConfig(state) - break - case 'customTheme': - case 'customThemeSource': - applyTheme(value) - break - case 'interfaceLanguage': - messages.setLanguage(this.getters.i18n, value) - dispatch('loadUnicodeEmojiData', value) - Cookies.set( - BACKEND_LANGUAGE_COOKIE_NAME, - localeService.internalToBackendLocaleMulti(value) - ) - break - case 'thirdColumnMode': - dispatch('setLayoutWidth', undefined) - break + const exceptions = new Set([ + 'useStreamingApi' + ]) + + if (exceptions.has(name)) { + switch (name) { + case 'useStreamingApi': { + const action = value ? 'enableMastoSockets' : 'disableMastoSockets' + + dispatch(action).then(() => { + commit('setOption', { name: 'useStreamingApi', value }) + }).catch((e) => { + console.error('Failed starting MastoAPI Streaming socket', e) + dispatch('disableMastoSockets') + dispatch('setOption', { name: 'useStreamingApi', value: false }) + }) + } + } + } else { + commit('setOption', { name, value }) + switch (name) { + case 'theme': + setPreset(value) + break + case 'sidebarColumnWidth': + case 'contentColumnWidth': + case 'notifsColumnWidth': + case 'emojiReactionsScale': + applyConfig(state) + break + case 'customTheme': + case 'customThemeSource': + applyTheme(value) + break + case 'interfaceLanguage': + messages.setLanguage(this.getters.i18n, value) + dispatch('loadUnicodeEmojiData', value) + Cookies.set( + BACKEND_LANGUAGE_COOKIE_NAME, + localeService.internalToBackendLocaleMulti(value) + ) + break + case 'thirdColumnMode': + dispatch('setLayoutWidth', undefined) + break + } } } } diff --git a/src/modules/interface.js b/src/modules/interface.js index a86193ea..2d012f1b 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,7 +1,9 @@ const defaultState = { settingsModalState: 'hidden', - settingsModalLoaded: false, + settingsModalLoadedUser: false, + settingsModalLoadedAdmin: false, settingsModalTargetTab: null, + settingsModalMode: 'user', settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -54,10 +56,17 @@ const interfaceMod = { throw new Error('Illegal minimization state of settings modal') } }, - openSettingsModal (state) { + openSettingsModal (state, value) { + state.settingsModalMode = value state.settingsModalState = 'visible' - if (!state.settingsModalLoaded) { - state.settingsModalLoaded = true + if (value === 'user') { + if (!state.settingsModalLoadedUser) { + state.settingsModalLoadedUser = true + } + } else if (value === 'admin') { + if (!state.settingsModalLoadedAdmin) { + state.settingsModalLoadedAdmin = true + } } }, setSettingsModalTargetTab (state, value) { @@ -92,8 +101,8 @@ const interfaceMod = { closeSettingsModal ({ commit }) { commit('closeSettingsModal') }, - openSettingsModal ({ commit }) { - commit('openSettingsModal') + openSettingsModal ({ commit }, value = 'user') { + commit('openSettingsModal', value) }, togglePeekSettingsModal ({ commit }) { commit('togglePeekSettingsModal') diff --git a/src/modules/serverSideConfig.js b/src/modules/profileConfig.js index 476263bc..2cb2014a 100644 --- a/src/modules/serverSideConfig.js +++ b/src/modules/profileConfig.js @@ -22,9 +22,9 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { .updateNotificationSettings({ settings }) .then(result => { if (result.status === 'success') { - commit('confirmServerSideOption', { name, value }) + commit('confirmProfileOption', { name, value }) } else { - commit('confirmServerSideOption', { name, value: oldValue }) + commit('confirmProfileOption', { name, value: oldValue }) } }) } @@ -94,16 +94,16 @@ export const settingsMap = { export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null])) -const serverSideConfig = { +const profileConfig = { state: { ...defaultState }, mutations: { - confirmServerSideOption (state, { name, value }) { + confirmProfileOption (state, { name, value }) { set(state, name, value) }, - wipeServerSideOption (state, { name }) { + wipeProfileOption (state, { name }) { set(state, name, null) }, - wipeAllServerSideOptions (state) { + wipeAllProfileOptions (state) { Object.keys(settingsMap).forEach(key => { set(state, key, null) }) @@ -118,23 +118,23 @@ const serverSideConfig = { } }, actions: { - setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) { + setProfileOption ({ rootState, state, commit, dispatch }, { name, value }) { const oldValue = get(state, name) const map = settingsMap[name] if (!map) throw new Error('Invalid server-side setting') const { set: path = map, api = defaultApi } = map - commit('wipeServerSideOption', { name }) + commit('wipeProfileOption', { name }) api({ rootState, commit }, { path, value, oldValue }) .catch((e) => { console.warn('Error setting server-side option:', e) - commit('confirmServerSideOption', { name, value: oldValue }) + commit('confirmProfileOption', { name, value: oldValue }) }) }, logout ({ commit }) { - commit('wipeAllServerSideOptions') + commit('wipeAllProfileOptions') } } } -export default serverSideConfig +export default profileConfig diff --git a/src/modules/users.js b/src/modules/users.js index a1316ba2..45cba334 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -551,6 +551,7 @@ const users = { loginUser (store, accessToken) { return new Promise((resolve, reject) => { const commit = store.commit + const dispatch = store.dispatch commit('beginLogin') store.rootState.api.backendInteractor.verifyCredentials(accessToken) .then((data) => { @@ -563,59 +564,65 @@ const users = { user.domainMutes = [] commit('setCurrentUser', user) commit('setServerSideStorage', user) + if (user.rights.admin) { + store.rootState.api.backendInteractor.fetchInstanceDBConfig() + .then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig })) + store.rootState.api.backendInteractor.fetchInstanceConfigDescriptions() + .then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions })) + } commit('addNewUsers', [user]) - store.dispatch('fetchEmoji') + dispatch('fetchEmoji') getNotificationPermission() .then(permission => commit('setNotificationPermission', permission)) // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) - store.dispatch('pushServerSideStorage') + dispatch('pushServerSideStorage') if (user.token) { - store.dispatch('setWsToken', user.token) + dispatch('setWsToken', user.token) // Initialize the shout socket. - store.dispatch('initializeSocket') + dispatch('initializeSocket') } const startPolling = () => { // Start getting fresh posts. - store.dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingTimeline', { timeline: 'friends' }) // Start fetching notifications - store.dispatch('startFetchingNotifications') + dispatch('startFetchingNotifications') // Start fetching chats - store.dispatch('startFetchingChats') + dispatch('startFetchingChats') } - store.dispatch('startFetchingLists') + dispatch('startFetchingLists') if (user.locked) { - store.dispatch('startFetchingFollowRequests') + dispatch('startFetchingFollowRequests') } if (store.getters.mergedConfig.useStreamingApi) { - store.dispatch('fetchTimeline', { timeline: 'friends', since: null }) - store.dispatch('fetchNotifications', { since: null }) - store.dispatch('enableMastoSockets', true).catch((error) => { + dispatch('fetchTimeline', { timeline: 'friends', since: null }) + dispatch('fetchNotifications', { since: null }) + dispatch('enableMastoSockets', true).catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) }).then(() => { - store.dispatch('fetchChats', { latest: true }) - setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) + dispatch('fetchChats', { latest: true }) + setTimeout(() => dispatch('setNotificationsSilence', false), 10000) }) } else { startPolling() } // Get user mutes - store.dispatch('fetchMutes') + dispatch('fetchMutes') - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) + dispatch('setLayoutWidth', windowWidth()) + dispatch('setLayoutHeight', windowHeight()) // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index b8c10b21..073f40a3 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -108,6 +108,9 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` +const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' +const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' + const oldfetch = window.fetch const fetch = (url, options) => { @@ -1659,6 +1662,58 @@ const setReportState = ({ id, state, credentials }) => { }) } +// ADMIN STUFF // EXPERIMENTAL +const fetchInstanceDBConfig = ({ credentials }) => { + return fetch(PLEROMA_ADMIN_CONFIG_URL, { + headers: authHeaders(credentials) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const fetchInstanceConfigDescriptions = ({ credentials }) => { + return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, { + headers: authHeaders(credentials) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const pushInstanceDBConfig = ({ credentials, payload }) => { + return fetch(PLEROMA_ADMIN_CONFIG_URL, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...authHeaders(credentials) + }, + method: 'POST', + body: JSON.stringify(payload) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1772,7 +1827,10 @@ const apiService = { postAnnouncement, editAnnouncement, deleteAnnouncement, - adminFetchAnnouncements + adminFetchAnnouncements, + fetchInstanceDBConfig, + fetchInstanceConfigDescriptions, + pushInstanceDBConfig } export default apiService |
