Merge branch 'improve_settings_reusability' into shigusegubu-vue3

* improve_settings_reusability:
  ChoiceSetting support added, added captcha settings
  limits tab, backend descriptions
  remove obsolete files
  lint
  setting admin settings works now. also now we have draftable settings
  initial admin settings prototype (WIP)
  initial implementation of an admin settings module
  lint
  fixes for stuff i missed
  minimize the rest of the sharedcomputedobject
  move websocket connection logic into module
  serverSideConfig renamed into profileSettingConfig to avoid confusion with serverSideStorage, reduced overall need for SharedComputedObject in settings tabs, moved copypaste code of "setting" type of helpers into a separate file.
This commit is contained in:
Henry Jameson 2023-03-20 23:48:39 +02:00
commit a010dd73e9
44 changed files with 1368 additions and 380 deletions

View file

@ -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')
}
}
}

View file

@ -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"

View file

@ -36,7 +36,9 @@
<button
class="button-default btn"
@click="addLanguage"
>{{ $t('settings.add_language') }}</button>
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</div>

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,170 @@
<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>
<li>
<h3>{{ $t('admin_dash.captcha.header') }}</h3>
</li>
<li>
<BooleanSetting
source="admin"
:path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
draft-mode
>
CAPTCHA
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<ChoiceSetting
source="admin"
: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')
}"
draft-mode
>
CAPTCHA TYPE
</ChoiceSetting>
<IntegerSetting
source="admin"
:path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
draft-mode
>
VALID
</IntegerSetting>
</li>
</ul>
<ul
v-if="adminConfig[':pleroma']['Pleroma.Captcha'][':enabled'] && adminConfig[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
class="setting-list suboptions"
>
<h4>{{ $t('admin_dash.kocaptcha') }}</h4>
<li>
<StringSetting
source="admin"
:path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']"
draft-mode
>
cockAPTCHA ENDPOINT
</StringSetting>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./instance_tab.js"></script>

View file

@ -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

View file

@ -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>

View file

@ -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
}
}
}

View file

@ -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"
>
<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>

View file

@ -1,51 +1,42 @@
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: {}
}
},
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
computed: {
...Setting.computed,
realOptions () {
if (this.source === 'admin') {
console.log(this.backendDescriptionSuggestions)
return this.backendDescriptionSuggestions.map(x => ({
key: x,
value: x,
label: this.optionLabelMap[x] || x
}))
}
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
}
}
}

View file

@ -3,15 +3,20 @@
v-if="matchesExpertLevel"
class="ChoiceSetting"
>
<template v-if="backendDescription">
{{ backendDescriptionLabel }}
</template>
<template v-else>
<slot />
</template>
{{ ' ' }}
<Select
:model-value="state"
:model-value="draftMode ? 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>

View file

@ -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>
&nbsp;
<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>
&nbsp;
<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>
&nbsp;
<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>

View file

@ -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)
}
}
}

View file

@ -4,7 +4,12 @@
class="NumberSetting"
>
<label :for="path">
<template v-if="backendDescription">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else>
<slot />
</template>
</label>
<input
:id="path"
@ -13,7 +18,7 @@
:step="step || 1"
:disabled="disabled"
:min="min || 0"
:value="state"
:value="draftMode ? draft :state"
@change="update"
>
{{ ' ' }}
@ -21,6 +26,14 @@
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</span>
</template>

View file

@ -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;

View file

@ -0,0 +1,164 @@
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, 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: '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
},
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.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!')
}
}
}
}

View file

@ -1,52 +1,15 @@
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
}
})

View file

@ -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('.')
},
stateUnit () {
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)
computed: {
...Setting.computed,
stateUnit () {
return this.state.replace(/\d+/, '')
},
isChanged () {
return this.state !== this.defaultState
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$parent.expertLevel
stateValue () {
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)
}
}
}

View file

@ -0,0 +1,5 @@
import Setting from './setting.js'
export default {
...Setting
}

View file

@ -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>

View file

@ -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 })
}
}

View file

@ -17,6 +17,12 @@
}
}
.setting-description {
margin-top: 0.2em;
margin-bottom: 2em;
font-size: 70%;
}
.settings-modal-panel {
overflow: hidden;
transition: transform;

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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') }}

View file

@ -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 })
}
}
}

View file

@ -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>

View file

@ -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') }}

View file

@ -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>

View file

@ -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"
>
<template #example>
<code>
foo@example.org
</code>
</i18n>
</template>
</i18n-t>
<input
v-model="moveAccountTarget"
>

View file

@ -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]

View file

@ -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,

View file

@ -0,0 +1,116 @@
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'])
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))
console.log(descriptions[':pleroma']['Pleroma.Captcha'])
commit('updateAdminDescriptions', { descriptions })
},
pushAdminSetting ({ rootState, state, commit, dispatch }, { path, value }) {
const [group, key, ...rest] = Array.isArray(path) ? path : path.split(/\./g)
const clone = {} // not actually cloning the entire thing to avoid excessive writes
set(clone, rest, 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

View file

@ -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,6 +179,25 @@ const config = {
commit('setHighlight', { user, color, type })
},
setOption ({ commit, dispatch, state }, { name, value }) {
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':
@ -207,6 +227,7 @@ const config = {
}
}
}
}
}
export default config

View file

@ -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')

View file

@ -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

View file

@ -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 })

View file

@ -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