Merge branch 'improve_settings_reusability' into 'develop'

AdminFE functionality in PleromaFE

See merge request pleroma/pleroma-fe!1800
This commit is contained in:
HJ 2023-05-24 18:55:20 +00:00
commit c730c9b6d0
63 changed files with 2371 additions and 422 deletions

1
changelog.d/adminfe.add Normal file
View File

@ -0,0 +1 @@
Implemented a very basic instance administration screen

View File

@ -645,6 +645,20 @@ option {
} }
} }
.cards-list {
list-style: none;
display: grid;
grid-auto-flow: row dense;
grid-template-columns: 1fr 1fr;
li {
border: 1px solid var(--border);
border-radius: var(--inputRadius);
padding: 0.5em;
margin: 0.25em;
}
}
.btn-block { .btn-block {
display: block; display: block;
width: 100%; width: 100%;
@ -655,16 +669,19 @@ option {
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
button { button,
.button-dropdown {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
&:not(:last-child) { &:not(:last-child),
&:not(:last-child) .button-default {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
&:not(:first-child) { &:not(:first-child),
&:not(:first-child) .button-default {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<label <label
class="checkbox" class="checkbox"
:class="{ disabled, indeterminate }" :class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
> >
<input <input
type="checkbox" type="checkbox"
@ -14,6 +14,7 @@
<i <i
class="checkbox-indicator" class="checkbox-indicator"
:aria-hidden="true" :aria-hidden="true"
@transitionend.capture="onTransitionEnd"
/> />
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"
@ -31,7 +32,24 @@ export default {
'indeterminate', 'indeterminate',
'disabled' 'disabled'
], ],
emits: ['update:modelValue'] emits: ['update:modelValue'],
data: (vm) => ({
indeterminateTransitionFix: vm.indeterminate
}),
watch: {
indeterminate (e) {
if (e) {
this.indeterminateTransitionFix = true
}
}
},
methods: {
onTransitionEnd (e) {
if (!this.indeterminate) {
this.indeterminateTransitionFix = false
}
}
}
} }
</script> </script>
@ -98,6 +116,12 @@ export default {
} }
} }
&.indeterminate-fix {
input[type="checkbox"] + .checkbox-indicator::before {
content: "";
}
}
& > span { & > span {
margin-left: 0.5em; margin-left: 0.5em;
} }

View File

@ -107,7 +107,10 @@ export default {
this.searchBarHidden = hidden this.searchBarHidden = hidden
}, },
openSettingsModal () { openSettingsModal () {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
} }
} }
} }

View File

@ -48,20 +48,19 @@
icon="cog" icon="cog"
/> />
</button> </button>
<a <button
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma" class="button-unstyled nav-icon"
class="nav-icon"
target="_blank" target="_blank"
:title="$t('nav.administration')" :title="$t('nav.administration')"
@click.stop @click.stop="openAdminModal"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="tachometer-alt" icon="tachometer-alt"
/> />
</a> </button>
<span class="spacer" /> <span class="spacer" />
<button <button
v-if="currentUser" v-if="currentUser"

View File

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

View File

@ -23,6 +23,11 @@ const mediaUpload = {
} }
}, },
methods: { methods: {
onClick () {
if (this.uploadReady) {
this.$refs.input.click()
}
},
uploadFile (file) { uploadFile (file) {
const self = this const self = this
const store = this.$store const store = this.$store
@ -69,10 +74,15 @@ const mediaUpload = {
this.multiUpload(target.files) this.multiUpload(target.files)
} }
}, },
props: [ props: {
'dropFiles', dropFiles: Object,
'disabled' disabled: Boolean,
], normalButton: Boolean,
acceptTypes: {
type: String,
default: '*/*'
}
},
watch: { watch: {
dropFiles: function (fileInfos) { dropFiles: function (fileInfos) {
if (!this.uploading) { if (!this.uploading) {

View File

@ -1,8 +1,9 @@
<template> <template>
<label <button
class="media-upload" class="media-upload"
:class="{ disabled: disabled }" :class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]"
:title="$t('tool_tip.media_upload')" :title="$t('tool_tip.media_upload')"
@click="onClick"
> >
<FAIcon <FAIcon
v-if="uploading" v-if="uploading"
@ -15,15 +16,21 @@
class="new-icon" class="new-icon"
icon="upload" icon="upload"
/> />
<template v-if="normalButton">
{{ ' ' }}
{{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }}
</template>
<input <input
v-if="uploadReady" v-if="uploadReady"
ref="input"
class="hidden-input-file" class="hidden-input-file"
:disabled="disabled" :disabled="disabled"
type="file" type="file"
multiple="true" multiple="true"
:accept="acceptTypes"
@change="change" @change="change"
> >
</label> </button>
</template> </template>
<script src="./media_upload.js"></script> <script src="./media_upload.js"></script>
@ -32,10 +39,12 @@
@import "../../variables"; @import "../../variables";
.media-upload { .media-upload {
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
.hidden-input-file { .hidden-input-file {
display: none; display: none;
} }
} }
</style>
label.media-upload {
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
}
</style>

View File

@ -163,8 +163,8 @@
</router-link> </router-link>
<button <button
class="button-unstyled expand-icon" class="button-unstyled expand-icon"
:aria-expanded="statusExpanded"
:title="$t('tool_tip.toggle_expand')" :title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded"
@click.prevent="toggleStatusExpanded" @click.prevent="toggleStatusExpanded"
> >
<FAIcon <FAIcon

View File

@ -45,6 +45,9 @@ const Popover = {
// Lets hover popover stay when clicking inside of it // Lets hover popover stay when clicking inside of it
stayOnClick: Boolean, stayOnClick: Boolean,
// Use styled button (to avoid nested buttons)
normalButton: Boolean,
triggerAttrs: { triggerAttrs: {
type: Object, type: Object,
default: {} default: {}

View File

@ -5,7 +5,8 @@
> >
<button <button
ref="trigger" ref="trigger"
class="button-unstyled popover-trigger-button" class="popover-trigger-button"
:class="normalButton ? 'button-default btn' : 'button-unstyled'"
type="button" type="button"
v-bind="triggerAttrs" v-bind="triggerAttrs"
@click="onClick" @click="onClick"

View File

@ -0,0 +1,64 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import Popover from 'src/components/popover/popover.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const FrontendsTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
GroupSetting,
Popover
},
created () {
if (this.user.rights.admin) {
this.$store.dispatch('loadFrontendsStuff')
}
},
computed: {
frontends () {
return this.$store.state.adminSettings.frontends
},
...SharedComputedObject()
},
methods: {
update (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const { name } = frontend
const payload = { name, ref }
this.$store.state.api.backendInteractor.installFrontend({ payload })
.then((externalUser) => {
this.$store.dispatch('loadFrontendsStuff')
})
},
setDefault (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const { name } = frontend
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
}
}
}
export default FrontendsTab

View File

@ -0,0 +1,13 @@
.frontends-tab {
.cards-list {
padding: 0;
}
dd {
text-overflow: ellipsis;
word-wrap: nowrap;
white-space: nowrap;
overflow-x: hidden;
max-width: 10em;
}
}

View File

@ -0,0 +1,184 @@
<template>
<div
class="frontends-tab"
:label="$t('admin_dash.tabs.frontends')"
>
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
<ul class="setting-list">
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
<p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:frontends.:primary.name" />
</li>
<li>
<StringSetting path=":pleroma.:frontends.:primary.ref" />
</li>
<li>
<GroupSetting path=":pleroma.:frontends.:primary" />
</li>
</ul>
</li>
</ul>
<div class="setting-list">
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
<ul class="cards-list">
<li
v-for="frontend in frontends"
:key="frontend.name"
>
<strong>{{ frontend.name }}</strong>
{{ ' ' }}
<span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name">
<i18n-t
v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
v-else
keypath="admin_dash.frontend.is_default_custom"
>
<template #version>
<code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
</template>
</i18n-t>
</span>
<dl>
<dt>{{ $t('admin_dash.frontend.repository') }}</dt>
<dd>
<a
:href="frontend.git"
target="_blank"
>{{ frontend.git }}</a>
</dd>
<template v-if="expertLevel">
<dt>{{ $t('admin_dash.frontend.versions') }}</dt>
<dd
v-for="ref in frontend.refs"
:key="ref"
>
<code>{{ ref }}</code>
</dd>
</template>
<dt v-if="expertLevel">
{{ $t('admin_dash.frontend.build_url') }}
</dt>
<dd v-if="expertLevel">
<a
:href="frontend.build_url"
target="_blank"
>{{ frontend.build_url }}</a>
</dd>
</dl>
<div>
<span class="btn-group">
<button
class="button button-default btn"
type="button"
@click="update(frontend)"
>
{{
frontend.installed
? $t('admin_dash.frontend.reinstall')
: $t('admin_dash.frontend.install')
}}
</button>
<Popover
v-if="frontend.refs.length > 1"
trigger="click"
class="button-dropdown"
placement="bottom"
>
<template #content>
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs"
:key="ref"
class="button-default dropdown-item"
@click="update(frontend, ref)"
>
<i18n-t keypath="admin_dash.frontend.install_version">
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</button>
</div>
</template>
<template #trigger>
<button
class="button button-default btn dropdown-button"
type="button"
:title="$t('admin_dash.frontend.more_install_options')"
>
<FAIcon icon="chevron-down" />
</button>
</template>
</Popover>
</span>
<span
v-if="frontend.installed && frontend.name !== 'admin-fe'"
class="btn-group"
>
<button
class="button button-default btn"
type="button"
:disabled="
adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]
"
@click="setDefault(frontend)"
>
{{
$t('admin_dash.frontend.set_default')
}}
</button>
{{ ' ' }}
<Popover
v-if="frontend.refs.length > 1"
trigger="click"
class="button-dropdown"
placement="bottom"
>
<template #content>
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs.slice(1)"
:key="ref"
class="button-default dropdown-item"
@click="setDefault(frontend, ref)"
>
<i18n-t keypath="admin_dash.frontend.set_default_version">
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</button>
</div>
</template>
<template #trigger>
<button
class="button button-default btn dropdown-button"
type="button"
:title="$t('admin_dash.frontend.more_default_options')"
>
<FAIcon icon="chevron-down" />
</button>
</template>
</Popover>
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script src="./frontends_tab.js"></script>
<style lang="scss" src="./frontends_tab.scss"></style>

View File

@ -0,0 +1,38 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const InstanceTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
GroupSetting
},
computed: {
...SharedComputedObject()
}
}
export default InstanceTab

View File

@ -0,0 +1,196 @@
<template>
<div :label="$t('admin_dash.tabs.instance')">
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:instance.:name" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:email" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:description" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:short_description" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:background_image" />
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:registrations_open" />
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path=":pleroma.:instance.:invites_enabled"
parent-path=":pleroma.:instance.:registrations_open"
parent-invert
/>
</li>
</ul>
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:birthday_required" />
<ul class="setting-list suboptions">
<li>
<IntegerSetting
path=":pleroma.:instance.:birthday_min_age"
parent-path=":pleroma.:instance.:birthday_required"
/>
</li>
</ul>
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:account_activation_required" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:account_approval_required" />
</li>
<li>
<h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
<ul class="setting-list suboptions">
<li>
<ChoiceSetting
:path="[':pleroma', 'Pleroma.Captcha', ':method']"
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
:option-label-map="{
'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
}"
/>
<IntegerSetting
:path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
/>
</li>
<li
v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
>
<h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
<ul class="setting-list">
<li>
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.access') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting
override-backend-description
override-backend-description-label
path=":pleroma.:instance.:public"
/>
</li>
<li>
<ChoiceSetting
override-backend-description
override-backend-description-label
path=":pleroma.:instance.:limit_to_local_content"
/>
</li>
<li v-if="expertLevel">
<h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
<p>
{{ $t('admin_dash.instance.restrict.description') }}
</p>
<ul class="setting-list">
<li>
<h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:timelines.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:profiles.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:activities.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:activities.:remote"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./instance_tab.js"></script>

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,136 @@
<template>
<div :label="$t('admin_dash.tabs.limits')">
<div class="setting-item">
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
<ul class="setting-list">
<li>
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:remote_limit"
expert="1"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:description_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:upload_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_media_attachments"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.users') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_pinned_statuses"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:user_bio_length"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:user_name_length"
draft-mode
/>
</li>
<li>
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_account_fields"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_remote_account_fields"
draft-mode
expert="1"
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:account_field_name_length"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:account_field_value_length"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:avatar_upload_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:banner_upload_limit"
draft-mode
/>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./limits_tab.js"></script>

View File

@ -0,0 +1,43 @@
import Setting from './setting.js'
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
import MediaUpload from 'src/components/media_upload/media_upload.vue'
import Attachment from 'src/components/attachment/attachment.vue'
export default {
...Setting,
props: {
...Setting.props,
acceptTypes: {
type: String,
required: false,
default: 'image/*'
}
},
components: {
...Setting.components,
MediaUpload,
Attachment
},
computed: {
...Setting.computed,
attachment () {
const path = this.realDraftMode ? this.draft : this.state
// The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
const url = path.includes('://') ? path : this.$store.state.instance.server + path
return {
mimetype: fileTypeExt(url),
url
}
}
},
methods: {
...Setting.methods,
setMediaFile (fileInfo) {
if (this.realDraftMode) {
this.draft = fileInfo.url
} else {
this.configSink(this.path, fileInfo.url)
}
}
}
}

View File

@ -0,0 +1,96 @@
<template>
<span
v-if="matchesExpertLevel"
class="AttachmentSetting"
>
<label
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
<div class="attachment-input">
<div>{{ $t('settings.url') }}</div>
<div class="controls">
<input
:id="path"
class="string-input"
:disabled="shouldBeDisabled"
:value="realDraftMode ? draft : state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
</div>
<div>{{ $t('settings.preview') }}</div>
<Attachment
class="attachment"
:compact="compact"
:attachment="attachment"
size="small"
hide-description
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
<div class="controls">
<MediaUpload
ref="mediaUpload"
class="media-upload-icon"
:drop-files="dropFiles"
normal-button
:accept-types="acceptTypes"
@uploaded="setMediaFile"
@upload-failed="uploadFailed"
/>
</div>
</div>
<DraftButtons />
</span>
</template>
<script src="./attachment_setting.js"></script>
<style lang="scss">
.AttachmentSetting {
.attachment {
display: block;
width: 100%;
height: 15em;
margin-bottom: 0.5em;
}
.attachment-input {
margin-left: 1em;
display: flex;
flex-direction: column;
width: 20em;
}
.controls {
margin-bottom: 0.5em;
input,
button {
width: 100%;
}
}
}
</style>

View File

@ -1,56 +1,31 @@
import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import ModifiedIndicator from './modified_indicator.vue' import Setting from './setting.js'
import ServerSideIndicator from './server_side_indicator.vue'
export default { export default {
components: { ...Setting,
Checkbox, props: {
ModifiedIndicator, ...Setting.props,
ServerSideIndicator indeterminateState: [String, Object]
},
components: {
...Setting.components,
Checkbox
}, },
props: [
'path',
'disabled',
'expert'
],
computed: { computed: {
pathDefault () { ...Setting.computed,
const [firstSegment, ...rest] = this.path.split('.') isIndeterminate () {
return [firstSegment + 'DefaultValue', ...rest].join('.') return this.visibleState === this.indeterminateState
},
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: { methods: {
update (e) { ...Setting.methods,
const [firstSegment, ...rest] = this.path.split('.') getValue (e) {
set(this.$parent, this.path, e) // Basic tri-state toggle implementation
// Updating nested properties does not trigger update on its parent. if (!!this.indeterminateState && !e && this.visibleState === true) {
// probably still not as reliable, but works for depth=1 at least // If we have indeterminate state, switching from true to false first goes through indeterminate
if (rest.length > 0) { return this.indeterminateState
set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
} }
}, return e
reset () {
set(this.$parent, this.path, this.defaultState)
} }
} }
} }

View File

@ -4,23 +4,37 @@
class="BooleanSetting" class="BooleanSetting"
> >
<Checkbox <Checkbox
:model-value="state" :model-value="visibleState"
:disabled="disabled" :disabled="shouldBeDisabled"
:indeterminate="isIndeterminate"
@update:modelValue="update" @update:modelValue="update"
> >
<span <span
v-if="!!$slots.default"
class="label" class="label"
:class="{ 'faint': shouldBeDisabled }"
> >
<slot /> <template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</span> </span>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ServerSideIndicator :server-side="isServerSide" />
</Checkbox> </Checkbox>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label> </label>
</template> </template>

View File

@ -1,51 +1,41 @@
import { get, set } from 'lodash'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import ModifiedIndicator from './modified_indicator.vue' import Setting from './setting.js'
import ServerSideIndicator from './server_side_indicator.vue'
export default { export default {
...Setting,
components: { components: {
Select, ...Setting.components,
ModifiedIndicator, Select
ServerSideIndicator },
props: {
...Setting.props,
options: {
type: Array,
required: false
},
optionLabelMap: {
type: Object,
required: false,
default: {}
}
}, },
props: [
'path',
'disabled',
'options',
'expert'
],
computed: { computed: {
pathDefault () { ...Setting.computed,
const [firstSegment, ...rest] = this.path.split('.') realOptions () {
return [firstSegment + 'DefaultValue', ...rest].join('.') if (this.realSource === 'admin') {
}, return this.backendDescriptionSuggestions.map(x => ({
state () { key: x,
const value = get(this.$parent, this.path) value: x,
if (value === undefined) { label: this.optionLabelMap[x] || x
return this.defaultState }))
} else {
return value
} }
}, return this.options
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: { methods: {
update (e) { ...Setting.methods,
set(this.$parent, this.path, e) getValue (e) {
}, return e
reset () {
set(this.$parent, this.path, this.defaultState)
} }
} }
} }

View File

@ -3,15 +3,20 @@
v-if="matchesExpertLevel" v-if="matchesExpertLevel"
class="ChoiceSetting" class="ChoiceSetting"
> >
<slot /> <template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else>
<slot />
</template>
{{ ' ' }} {{ ' ' }}
<Select <Select
:model-value="state" :model-value="realDraftMode ? draft :state"
:disabled="disabled" :disabled="disabled"
@update:modelValue="update" @update:modelValue="update"
> >
<option <option
v-for="option in options" v-for="option in realOptions"
:key="option.key" :key="option.key"
:value="option.value" :value="option.value"
> >
@ -23,7 +28,14 @@
:changed="isChanged" :changed="isChanged"
:onclick="reset" :onclick="reset"
/> />
<ServerSideIndicator :server-side="isServerSide" /> <ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label> </label>
</template> </template>

View File

@ -0,0 +1,88 @@
<!-- this is a helper exclusive to Setting components -->
<!-- TODO make it reusable -->
<template>
<span
class="DraftButtons"
>
<Popover
v-if="$parent.isDirty"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
@click="$parent.commitDraft"
>
<template #trigger>
{{ $t('settings.commit_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.commit_value_tooltip') }}
</div>
</template>
</Popover>
<Popover
v-if="$parent.isDirty"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
@click="$parent.reset"
>
<template #trigger>
{{ $t('settings.reset_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.reset_value_tooltip') }}
</div>
</template>
</Popover>
<Popover
v-if="$parent.canHardReset"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
@click="$parent.hardReset"
>
<template #trigger>
{{ $t('settings.hard_reset_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.hard_reset_value_tooltip') }}
</div>
</template>
</Popover>
</span>
</template>
<script>
import Popover from 'src/components/popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faWrench } from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench
)
export default {
components: { Popover },
props: ['changed']
}
</script>
<style lang="scss">
.DraftButtons {
display: inline-block;
position: relative;
.button-default {
margin-left: 0.5em;
}
}
.draft-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
}
</style>

View File

@ -0,0 +1,13 @@
import { isEqual } from 'lodash'
import Setting from './setting.js'
export default {
...Setting,
computed: {
...Setting.computed,
isDirty () {
return !isEqual(this.state, this.draft)
}
}
}

View File

@ -0,0 +1,15 @@
<template>
<span
v-if="matchesExpertLevel"
class="GroupSetting"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
</span>
</template>
<script src="./group_setting.js"></script>

View File

@ -1,56 +1,24 @@
import { get, set } from 'lodash' import Setting from './setting.js'
import ModifiedIndicator from './modified_indicator.vue'
export default { export default {
components: { ...Setting,
ModifiedIndicator
},
props: { props: {
path: String, ...Setting.props,
disabled: Boolean, truncate: {
min: Number, type: Number,
step: Number, required: false,
truncate: Number, default: 1
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
} }
}, },
methods: { methods: {
truncateValue (value) { ...Setting.methods,
if (!this.truncate) { getValue (e) {
return value if (!this.truncate === 1) {
return parseInt(e.target.value)
} else if (this.truncate > 1) {
return Math.trunc(e.target.value / this.truncate) * this.truncate
} }
return parseFloat(e.target.value)
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)
} }
} }
} }

View File

@ -3,17 +3,26 @@
v-if="matchesExpertLevel" v-if="matchesExpertLevel"
class="NumberSetting" class="NumberSetting"
> >
<label :for="path"> <label
<slot /> :for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label> </label>
<input <input
:id="path" :id="path"
class="number-input" class="number-input"
type="number" type="number"
:step="step || 1" :step="step || 1"
:disabled="disabled" :disabled="shouldBeDisabled"
:min="min || 0" :min="min || 0"
:value="state" :value="realDraftMode ? draft :state"
@change="update" @change="update"
> >
{{ ' ' }} {{ ' ' }}
@ -21,6 +30,15 @@
:changed="isChanged" :changed="isChanged"
:onclick="reset" :onclick="reset"
/> />
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</span> </span>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<span <span
v-if="serverSide" v-if="isProfile"
class="ServerSideIndicator" class="ProfileSettingIndicator"
> >
<Popover <Popover
trigger="hover" trigger="hover"
@ -14,7 +14,7 @@
/> />
</template> </template>
<template #content> <template #content>
<div class="serverside-tooltip"> <div class="profilesetting-tooltip">
{{ $t('settings.setting_server_side') }} {{ $t('settings.setting_server_side') }}
</div> </div>
</template> </template>
@ -33,17 +33,17 @@ library.add(
export default { export default {
components: { Popover }, components: { Popover },
props: ['serverSide'] props: ['isProfile']
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.ServerSideIndicator { .ProfileSettingIndicator {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }
.serverside-tooltip { .profilesetting-tooltip {
margin: 0.5em 1em; margin: 0.5em 1em;
min-width: 10em; min-width: 10em;
text-align: center; text-align: center;

View File

@ -0,0 +1,237 @@
import ModifiedIndicator from './modified_indicator.vue'
import ProfileSettingIndicator from './profile_setting_indicator.vue'
import DraftButtons from './draft_buttons.vue'
import { get, set, cloneDeep } from 'lodash'
export default {
components: {
ModifiedIndicator,
DraftButtons,
ProfileSettingIndicator
},
props: {
path: {
type: [String, Array],
required: true
},
disabled: {
type: Boolean,
default: false
},
parentPath: {
type: [String, Array]
},
parentInvert: {
type: Boolean,
default: false
},
expert: {
type: [Number, String],
default: 0
},
source: {
type: String,
default: undefined
},
hideDescription: {
type: Boolean
},
swapDescriptionAndLabel: {
type: Boolean
},
overrideBackendDescription: {
type: Boolean
},
overrideBackendDescriptionLabel: {
type: Boolean
},
draftMode: {
type: Boolean,
default: undefined
}
},
inject: {
defaultSource: {
default: 'default'
},
defaultDraftMode: {
default: false
}
},
data () {
return {
localDraft: null
}
},
created () {
if (this.realDraftMode && this.realSource !== 'admin') {
this.draft = this.state
}
},
computed: {
draft: {
// TODO allow passing shared draft object?
get () {
if (this.realSource === 'admin') {
return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
},
set (value) {
if (this.realSource === 'admin') {
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
} else {
this.localDraft = value
}
}
},
state () {
const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
visibleState () {
return this.realDraftMode ? this.draft : this.state
},
realSource () {
return this.source || this.defaultSource
},
realDraftMode () {
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
},
backendDescription () {
return get(this.$store.state.adminSettings.descriptions, this.path)
},
backendDescriptionLabel () {
if (this.realSource !== 'admin') return ''
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
return this.$t([
'admin_dash',
'temp_overrides',
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
'label'
].join('.'))
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.description
: this.backendDescription?.label
}
},
backendDescriptionDescription () {
if (this.realSource !== 'admin') return ''
if (this.hideDescription) return null
if (!this.backendDescription || this.overrideBackendDescription) {
return this.$t([
'admin_dash',
'temp_overrides',
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
'description'
].join('.'))
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.label
: this.backendDescription?.description
}
},
backendDescriptionSuggestions () {
return this.backendDescription?.suggestions
},
shouldBeDisabled () {
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
},
configSource () {
switch (this.realSource) {
case 'profile':
return this.$store.state.profileConfig
case 'admin':
return this.$store.state.adminSettings.config
default:
return this.$store.getters.mergedConfig
}
},
configSink () {
switch (this.realSource) {
case 'profile':
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
default:
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
}
},
defaultState () {
switch (this.realSource) {
case 'profile':
return {}
default:
return get(this.$store.getters.defaultConfig, this.path)
}
},
isProfileSetting () {
return this.realSource === 'profile'
},
isChanged () {
switch (this.realSource) {
case 'profile':
case 'admin':
return false
default:
return this.state !== this.defaultState
}
},
canonPath () {
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
isDirty () {
if (this.realSource === 'admin' && this.canonPath.length > 3) {
return false // should not show draft buttons for "grouped" values
} else {
return this.realDraftMode && this.draft !== this.state
}
},
canHardReset () {
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
}
},
methods: {
getValue (e) {
return e.target.value
},
update (e) {
if (this.realDraftMode) {
this.draft = this.getValue(e)
} else {
this.configSink(this.path, this.getValue(e))
}
},
commitDraft () {
if (this.realDraftMode) {
this.configSink(this.path, this.draft)
}
},
reset () {
if (this.realDraftMode) {
this.draft = cloneDeep(this.state)
} else {
set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
}
},
hardReset () {
switch (this.realSource) {
case 'admin':
return this.$store.dispatch('resetAdminSetting', { path: this.path })
.then(() => { this.draft = this.state })
default:
console.warn('Hard reset not implemented yet!')
}
}
}
}

View File

@ -1,52 +1,18 @@
import { defaultState as configDefaultState } from 'src/modules/config.js'
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
const SharedComputedObject = () => ({ const SharedComputedObject = () => ({
user () { user () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
// Getting values for default properties expertLevel () {
...Object.keys(configDefaultState) return this.$store.getters.mergedConfig.expertLevel > 0
.map(key => [ },
key + 'DefaultValue', mergedConfig () {
function () { return this.$store.getters.mergedConfig
return this.$store.getters.defaultConfig[key] },
} adminConfig () {
]) return this.$store.state.adminSettings.config
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), },
// Generating computed values for vuex properties adminDraft () {
...Object.keys(configDefaultState) return this.$store.state.adminSettings.draft
.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 })
})
}
} }
}) })

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 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 allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
export const defaultHorizontalUnits = ['px', 'rem', 'vw'] export const defaultHorizontalUnits = ['px', 'rem', 'vw']
export const defaultVerticalUnits = ['px', 'rem', 'vh'] export const defaultVerticalUnits = ['px', 'rem', 'vh']
export default { export default {
...Setting,
components: { components: {
ModifiedIndicator, ...Setting.components,
Select Select
}, },
props: { props: {
path: String, ...Setting.props,
disabled: Boolean,
min: Number, min: Number,
units: { units: {
type: [String], type: Array,
default: () => allCssUnits default: () => allCssUnits
}, }
expert: [Number, String]
}, },
computed: { computed: {
pathDefault () { ...Setting.computed,
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
stateUnit () { stateUnit () {
return (this.state || '').replace(/\d+/, '') return this.state.replace(/\d+/, '')
}, },
stateValue () { stateValue () {
return (this.state || '').replace(/\D+/, '') 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
} }
}, },
methods: { methods: {
update (e) { ...Setting.methods,
set(this.$parent, this.path, e)
},
reset () {
set(this.$parent, this.path, this.defaultState)
},
updateValue (e) { 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) { updateUnit (e) {
set(this.$parent, this.path, this.stateValue + e.target.value) this.configSink(this.path, this.stateValue + e.target.value)
} }
} }
} }

View File

@ -45,11 +45,18 @@
<script src="./size_setting.js"></script> <script src="./size_setting.js"></script>
<style lang="scss"> <style lang="scss">
.css-unit-input, .SizeSetting {
.css-unit-input select { .number-input {
margin-left: 0.5em; max-width: 6.5em;
width: 4em; }
max-width: 4em;
min-width: 4em; .css-unit-input,
.css-unit-input select {
margin-left: 0.5em;
width: 4em;
max-width: 4em;
min-width: 4em;
}
} }
</style> </style>

View File

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

View File

@ -0,0 +1,42 @@
<template>
<label
v-if="matchesExpertLevel"
class="StringSetting"
>
<label
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<input
:id="path"
class="string-input"
:disabled="shouldBeDisabled"
:value="realDraftMode ? draft : state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>
<script src="./string_setting.js"></script>

View File

@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { cloneDeep } from 'lodash' import { cloneDeep, isEqual } from 'lodash'
import { import {
newImporter, newImporter,
newExporter newExporter
@ -53,8 +53,16 @@ const SettingsModal = {
Modal, Modal,
Popover, Popover,
Checkbox, Checkbox,
SettingsModalContent: getResettableAsyncComponent( SettingsModalUserContent: getResettableAsyncComponent(
() => import('./settings_modal_content.vue'), () => import('./settings_modal_user_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
delay: 0
}
),
SettingsModalAdminContent: getResettableAsyncComponent(
() => import('./settings_modal_admin_content.vue'),
{ {
loadingComponent: PanelLoading, loadingComponent: PanelLoading,
errorComponent: AsyncComponentError, errorComponent: AsyncComponentError,
@ -147,6 +155,12 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION PLEROMAFE_SETTINGS_MINOR_VERSION
] ]
return clone return clone
},
resetAdminDraft () {
this.$store.commit('resetAdminDraft')
},
pushAdminDraft () {
this.$store.dispatch('pushAdminDraft')
} }
}, },
computed: { computed: {
@ -156,8 +170,14 @@ const SettingsModal = {
modalActivated () { modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden' return this.$store.state.interface.settingsModalState !== 'hidden'
}, },
modalOpenedOnce () { modalMode () {
return this.$store.state.interface.settingsModalLoaded return this.$store.state.interface.settingsModalMode
},
modalOpenedOnceUser () {
return this.$store.state.interface.settingsModalLoadedUser
},
modalOpenedOnceAdmin () {
return this.$store.state.interface.settingsModalLoadedAdmin
}, },
modalPeeked () { modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized' return this.$store.state.interface.settingsModalState === 'minimized'
@ -167,9 +187,14 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0 return this.$store.state.config.expertLevel > 0
}, },
set (value) { set (value) {
console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
} }
},
adminDraftAny () {
return !isEqual(
this.$store.state.adminSettings.config,
this.$store.state.adminSettings.draft
)
} }
} }
} }

View File

@ -17,6 +17,12 @@
} }
} }
.setting-description {
margin-top: 0.2em;
margin-bottom: 2em;
font-size: 70%;
}
.settings-modal-panel { .settings-modal-panel {
overflow: hidden; overflow: hidden;
transition: transform; transition: transform;
@ -37,7 +43,9 @@
.btn { .btn {
min-height: 2em; min-height: 2em;
min-width: 10em; }
.btn:not(.dropdown-button) {
padding: 0 2em; padding: 0 2em;
} }
} }
@ -45,6 +53,8 @@
.settings-footer { .settings-footer {
display: flex; display: flex;
flex-wrap: wrap;
line-height: 2;
>* { >* {
margin-right: 0.5em; margin-right: 0.5em;

View File

@ -8,7 +8,7 @@
<div class="settings-modal-panel panel"> <div class="settings-modal-panel panel">
<div class="panel-heading"> <div class="panel-heading">
<span class="title"> <span class="title">
{{ $t('settings.settings') }} {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
</span> </span>
<transition name="fade"> <transition name="fade">
<div <div
@ -42,10 +42,12 @@
</button> </button>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<SettingsModalContent v-if="modalOpenedOnce" /> <SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
<SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
</div> </div>
<div class="panel-footer settings-footer"> <div class="panel-footer settings-footer -flexible-height">
<Popover <Popover
v-if="modalMode === 'user'"
class="export" class="export"
trigger="click" trigger="click"
placement="top" placement="top"
@ -107,10 +109,42 @@
> >
{{ $t("settings.expert_mode") }} {{ $t("settings.expert_mode") }}
</Checkbox> </Checkbox>
<span v-if="modalMode === 'admin'">
<i18n-t keypath="admin_dash.wip_notice">
<template #adminFeLink>
<a
href="/pleroma/admin/#/login-pleroma"
target="_blank"
>
{{ $t("admin_dash.old_ui_link") }}
</a>
</template>
</i18n-t>
</span>
<span <span
id="unscrolled-content" id="unscrolled-content"
class="extra-content" class="extra-content"
/> />
<span
v-if="modalMode === 'admin'"
class="admin-buttons"
>
<button
class="button-default btn"
:disabled="!adminDraftAny"
@click="resetAdminDraft"
>
{{ $t("admin_dash.reset_all") }}
</button>
{{ ' ' }}
<button
class="button-default btn"
:disabled="!adminDraftAny"
@click="pushAdminDraft"
>
{{ $t("admin_dash.commit_all") }}
</button>
</span>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@ -0,0 +1,93 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
import FrontendsTab from './admin_tabs/frontends_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faHand,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
} from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench,
faHand,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
)
const SettingsModalAdminContent = {
components: {
TabSwitcher,
InstanceTab,
LimitsTab,
FrontendsTab
},
computed: {
user () {
return this.$store.state.users.currentUser
},
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
open () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
},
adminDbLoaded () {
return this.$store.state.adminSettings.loaded
},
adminDescriptionsLoaded () {
return this.$store.state.adminSettings.descriptions !== null
},
noDb () {
return this.$store.state.adminSettings.dbConfigEnabled === false
}
},
created () {
if (this.user.rights.admin) {
this.$store.dispatch('loadAdminStuff')
}
},
methods: {
onOpen () {
const targetTab = this.$store.state.interface.settingsModalTargetTab
// We're being told to open in specific tab
if (targetTab) {
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
return elm.props && elm.props['data-tab-name'] === targetTab
})
if (tabIndex >= 0) {
this.$refs.tabSwitcher.setTab(tabIndex)
}
}
// Clear the state of target tab, so that next time settings is opened
// it doesn't force it.
this.$store.dispatch('clearSettingsModalTargetTab')
}
},
mounted () {
this.onOpen()
},
watch: {
open: function (value) {
if (value) this.onOpen()
}
}
}
export default SettingsModalAdminContent

View File

@ -48,9 +48,5 @@
color: var(--cRed, $fallback--cRed); color: var(--cRed, $fallback--cRed);
color: $fallback--cRed; color: $fallback--cRed;
} }
.number-input {
max-width: 6em;
}
} }
} }

View File

@ -0,0 +1,68 @@
<template>
<tab-switcher
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
ref="tabSwitcher"
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:render-only-focused="true"
:body-scroll-lock="bodyLock"
>
<div
v-if="noDb"
:label="$t('admin_dash.tabs.nodb')"
icon="exclamation-triangle"
data-tab-name="nodb-notice"
>
<div :label="$t('admin_dash.tabs.nodb')">
<div class="setting-item">
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
<i18n-t keypath="admin_dash.nodb.text">
<template #documentation>
<a
href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
target="_blank"
>
{{ $t("admin_dash.nodb.documentation") }}
</a>
</template>
<template #property>
<code>config :pleroma, configurable_from_database</code>
</template>
<template #value>
<code>true</code>
</template>
</i18n-t>
<p>{{ $t('admin_dash.nodb.text2') }}</p>
</div>
</div>
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.instance')"
icon="wrench"
data-tab-name="general"
>
<InstanceTab />
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.limits')"
icon="hand"
data-tab-name="limits"
>
<LimitsTab />
</div>
<div
:label="$t('admin_dash.tabs.frontends')"
icon="laptop-code"
data-tab-name="frontends"
>
<FrontendsTab />
</div>
</tab-switcher>
</template>
<script src="./settings_modal_admin_content.js"></script>
<style src="./settings_modal_admin_content.scss" lang="scss"></style>

View File

@ -0,0 +1,52 @@
@import "src/variables";
.settings_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
display: block;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: 0.5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
}
}

View File

@ -78,6 +78,6 @@
</tab-switcher> </tab-switcher>
</template> </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"> <BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.hide_filtered_statuses') }}
</BooleanSetting> </BooleanSetting>
<ul <ul class="setting-list suboptions">
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li> <li>
<BooleanSetting <BooleanSetting
:disabled="hideFilteredStatuses" parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideWordFilteredPosts" path="hideWordFilteredPosts"
> >
{{ $t('settings.hide_wordfiltered_statuses') }} {{ $t('settings.hide_wordfiltered_statuses') }}
@ -22,7 +20,8 @@
<li> <li>
<BooleanSetting <BooleanSetting
v-if="user" v-if="user"
:disabled="hideFilteredStatuses" parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideMutedThreads" path="hideMutedThreads"
> >
{{ $t('settings.hide_muted_threads') }} {{ $t('settings.hide_muted_threads') }}
@ -31,7 +30,8 @@
<li> <li>
<BooleanSetting <BooleanSetting
v-if="user" v-if="user"
:disabled="hideFilteredStatuses" parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideMutedPosts" path="hideMutedPosts"
> >
{{ $t('settings.hide_muted_posts') }} {{ $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 InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faGlobe faGlobe
@ -67,7 +67,7 @@ const GeneralTab = {
SizeSetting, SizeSetting,
InterfaceLanguageSwitcher, InterfaceLanguageSwitcher,
ScopeSelector, ScopeSelector,
ServerSideIndicator ProfileSettingIndicator
}, },
computed: { computed: {
horizontalUnits () { horizontalUnits () {
@ -110,7 +110,7 @@ const GeneralTab = {
}, },
methods: { methods: {
changeDefaultScope (value) { 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"> <BooleanSetting path="streaming">
{{ $t('settings.streaming') }} {{ $t('settings.streaming') }}
</BooleanSetting> </BooleanSetting>
<ul <ul class="setting-list suboptions">
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li> <li>
<BooleanSetting <BooleanSetting
path="pauseOnUnfocused" path="pauseOnUnfocused"
:disabled="!streaming" parent-path="streaming"
> >
{{ $t('settings.pause_on_unfocused') }} {{ $t('settings.pause_on_unfocused') }}
</BooleanSetting> </BooleanSetting>
@ -213,7 +210,7 @@
</ChoiceSetting> </ChoiceSetting>
</li> </li>
<ul <ul
v-if="conversationDisplay !== 'linear'" v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions" class="setting-list suboptions"
> >
<li> <li>
@ -265,7 +262,8 @@
<li> <li>
<BooleanSetting <BooleanSetting
v-if="user" v-if="user"
path="serverSide_stripRichContent" source="profile"
path="stripRichContent"
expert="1" expert="1"
> >
{{ $t('settings.no_rich_text_description') }} {{ $t('settings.no_rich_text_description') }}
@ -299,7 +297,7 @@
<BooleanSetting <BooleanSetting
path="preloadImage" path="preloadImage"
expert="1" expert="1"
:disabled="!hideNsfw" parent-path="hideNsfw"
> >
{{ $t('settings.preload_images') }} {{ $t('settings.preload_images') }}
</BooleanSetting> </BooleanSetting>
@ -308,7 +306,7 @@
<BooleanSetting <BooleanSetting
path="useOneClickNsfw" path="useOneClickNsfw"
expert="1" expert="1"
:disabled="!hideNsfw" parent-path="hideNsfw"
> >
{{ $t('settings.use_one_click_nsfw') }} {{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting> </BooleanSetting>
@ -321,15 +319,13 @@
> >
{{ $t('settings.loop_video') }} {{ $t('settings.loop_video') }}
</BooleanSetting> </BooleanSetting>
<ul <ul class="setting-list suboptions">
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li> <li>
<BooleanSetting <BooleanSetting
path="loopVideoSilentOnly" path="loopVideoSilentOnly"
expert="1" expert="1"
:disabled="!loopVideo || !loopSilentAvailable" parent-path="loopVideo"
:disabled="!loopSilentAvailable"
> >
{{ $t('settings.loop_video_silent_only') }} {{ $t('settings.loop_video_silent_only') }}
</BooleanSetting> </BooleanSetting>
@ -427,18 +423,18 @@
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<label for="default-vis"> <label for="default-vis">
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" /> {{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector <ScopeSelector
class="scope-selector" class="scope-selector"
:show-all="true" :show-all="true"
:user-default="serverSide_defaultScope" :user-default="$store.state.profileConfig.defaultScope"
:initial-scope="serverSide_defaultScope" :initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope" :on-scope-change="changeDefaultScope"
/> />
</label> </label>
</li> </li>
<li> <li>
<!-- <BooleanSetting path="serverSide_defaultNSFW"> --> <!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault"> <BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }} {{ $t('settings.sensitive_by_default') }}
</BooleanSetting> </BooleanSetting>

View File

@ -4,7 +4,10 @@
<h2>{{ $t('settings.notification_setting_filters') }}</h2> <h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="serverSide_blockNotificationsFromStrangers"> <BooleanSetting
source="profile"
path="blockNotificationsFromStrangers"
>
{{ $t('settings.notification_setting_block_from_strangers') }} {{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
@ -67,7 +70,8 @@
</li> </li>
<li> <li>
<BooleanSetting <BooleanSetting
path="serverSide_webPushHideContents" source="profile"
path="webPushHideContents"
expert="1" expert="1"
> >
{{ $t('settings.notification_setting_hide_notification_contents') }} {{ $t('settings.notification_setting_hide_notification_contents') }}

View File

@ -254,37 +254,50 @@
<h2>{{ $t('settings.account_privacy') }}</h2> <h2>{{ $t('settings.account_privacy') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<BooleanSetting path="serverSide_locked"> <BooleanSetting
source="profile"
path="locked"
>
{{ $t('settings.lock_account_description') }} {{ $t('settings.lock_account_description') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_discoverable"> <BooleanSetting
source="profile"
path="discoverable"
>
{{ $t('settings.discoverable') }} {{ $t('settings.discoverable') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_allowFollowingMove"> <BooleanSetting
source="profile"
path="allowFollowingMove"
>
{{ $t('settings.allow_following_move') }} {{ $t('settings.allow_following_move') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_hideFavorites"> <BooleanSetting
source="profile"
path="hideFavorites"
>
{{ $t('settings.hide_favorites_description') }} {{ $t('settings.hide_favorites_description') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_hideFollowers"> <BooleanSetting
source="profile"
path="hideFollowers"
>
{{ $t('settings.hide_followers_description') }} {{ $t('settings.hide_followers_description') }}
</BooleanSetting> </BooleanSetting>
<ul <ul class="setting-list suboptions">
class="setting-list suboptions"
:class="[{disabled: !serverSide_hideFollowers}]"
>
<li> <li>
<BooleanSetting <BooleanSetting
path="serverSide_hideFollowersCount" source="profile"
:disabled="!serverSide_hideFollowers" path="hideFollowersCount"
parent-path="hideFollowers"
> >
{{ $t('settings.hide_followers_count_description') }} {{ $t('settings.hide_followers_count_description') }}
</BooleanSetting> </BooleanSetting>
@ -292,17 +305,18 @@
</ul> </ul>
</li> </li>
<li> <li>
<BooleanSetting path="serverSide_hideFollows"> <BooleanSetting
source="profile"
path="hideFollows"
>
{{ $t('settings.hide_follows_description') }} {{ $t('settings.hide_follows_description') }}
</BooleanSetting> </BooleanSetting>
<ul <ul class="setting-list suboptions">
class="setting-list suboptions"
:class="[{disabled: !serverSide_hideFollows}]"
>
<li> <li>
<BooleanSetting <BooleanSetting
path="serverSide_hideFollowsCount" source="profile"
:disabled="!serverSide_hideFollows" path="hideFollowsCount"
parent-path="hideFollows"
> >
{{ $t('settings.hide_follows_count_description') }} {{ $t('settings.hide_follows_count_description') }}
</BooleanSetting> </BooleanSetting>

View File

@ -143,8 +143,8 @@
/> />
</div> </div>
<div> <div>
<i18n <i18n-t
path="settings.new_alias_target" keypath="settings.new_alias_target"
tag="p" tag="p"
> >
<code <code
@ -152,7 +152,7 @@
> >
foo@example.org foo@example.org
</code> </code>
</i18n> </i18n-t>
<input <input
v-model="addAliasTarget" v-model="addAliasTarget"
> >
@ -175,16 +175,16 @@
<h2>{{ $t('settings.move_account') }}</h2> <h2>{{ $t('settings.move_account') }}</h2>
<p>{{ $t('settings.move_account_notes') }}</p> <p>{{ $t('settings.move_account_notes') }}</p>
<div> <div>
<i18n <i18n-t
path="settings.move_account_target" keypath="settings.move_account_target"
tag="p" tag="p"
> >
<code <template #example>
place="example" <code>
> foo@example.org
foo@example.org </code>
</code> </template>
</i18n> </i18n-t>
<input <input
v-model="moveAccountTarget" v-model="moveAccountTarget"
> >

View File

@ -115,7 +115,10 @@ const SideDrawer = {
GestureService.updateSwipe(e, this.closeGesture) GestureService.updateSwipe(e, this.closeGesture)
}, },
openSettingsModal () { openSettingsModal () {
this.$store.dispatch('openSettingsModal') this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
} }
} }
} }

View File

@ -180,16 +180,16 @@
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer" @click="toggleDrawer"
> >
<a <button
href="/pleroma/admin/#/login-pleroma" class="button-unstyled -link -fullwidth"
target="_blank" @click.stop="openAdminModal"
> >
<FAIcon <FAIcon
fixed-width fixed-width
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="tachometer-alt" icon="tachometer-alt"
/> {{ $t("nav.administration") }} /> {{ $t("nav.administration") }}
</a> </button>
</li> </li>
<li <li
v-if="currentUser && supportsAnnouncements" v-if="currentUser && supportsAnnouncements"

View File

@ -60,13 +60,7 @@ export default {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex return this.$slots.default().findIndex(isWanted) === this.activeIndex
} }
}, }
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
})
}, },
beforeUpdate () { beforeUpdate () {
const currentSlot = this.slots()[this.active] const currentSlot = this.slots()[this.active]

View File

@ -519,6 +519,8 @@
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes", "mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos in a popup frame", "play_videos_in_modal": "Play videos in a popup frame",
"url": "URL",
"preview": "Preview",
"file_export_import": { "file_export_import": {
"backup_restore": "Settings backup", "backup_restore": "Settings backup",
"backup_settings": "Backup settings to file", "backup_settings": "Backup settings to file",
@ -830,6 +832,98 @@
"title": "Version", "title": "Version",
"backend_version": "Backend version", "backend_version": "Backend version",
"frontend_version": "Frontend version" "frontend_version": "Frontend version"
},
"commit_value": "Save",
"commit_value_tooltip": "Value is not saved, press this button to commit your changes",
"reset_value": "Reset",
"reset_value_tooltip": "Reset draft",
"hard_reset_value": "Hard reset",
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value"
},
"admin_dash": {
"window_title": "Administration",
"wip_notice": "This admin dashboard is experimental and WIP, {adminFeLink}.",
"old_ui_link": "old admin UI available here",
"reset_all": "Reset all",
"commit_all": "Save all",
"tabs": {
"nodb": "No DB Config",
"instance": "Instance",
"limits": "Limits",
"frontends": "Front-ends"
},
"nodb": {
"heading": "Database config is disabled",
"text": "You need to change backend config files so that {property} is set to {value}, see more in {documentation}.",
"documentation": "documentation",
"text2": "Most configuration options will be unavailable."
},
"captcha": {
"native": "Native",
"kocaptcha": "KoCaptcha"
},
"instance": {
"instance": "Instance information",
"registrations": "User sign-ups",
"captcha_header": "CAPTCHA",
"kocaptcha": "KoCaptcha settings",
"access": "Instance access",
"restrict": {
"header": "Restrict access for anonymous visitors",
"description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.",
"timelines": "Timelines access",
"profiles": "User profiles access",
"activities": "Statues/activities access"
}
},
"limits": {
"arbitrary_limits": "Arbitrary limits",
"posts": "Post limits",
"uploads": "Attachments limits",
"users": "User profile limits",
"profile_fields": "Profile fields limits",
"user_uploads": "Profile media limits"
},
"frontend": {
"repository": "Repository link",
"versions": "Available versions",
"build_url": "Build URL",
"reinstall": "Reinstall",
"is_default": "(Default)",
"is_default_custom": "(Default, version: {version})",
"install": "Install",
"install_version": "Install version {version}",
"more_install_options": "More install options",
"more_default_options": "More default setting options",
"set_default": "Set default",
"set_default_version": "Set version {version} as default",
"wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
"default_frontend": "Default front-end",
"default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
"default_frontend_tip2": "WIP: Since Pleroma backend doesn't properly list all installed frontends you'll have to enter name and reference manually. List below provides shortcuts to fill the values.",
"available_frontends": "Available for install"
},
"temp_overrides": {
":pleroma": {
":instance": {
":public": {
"label": "Instance is public",
"description": "Disabling this will make all API accessible only for logged-in users, this will make Public and Federated timelines inaccessible to anonymous visitors."
},
":limit_to_local_content": {
"label": "Limit search to local content",
"description": "Disables global network search for unauthenticated (default), all users or none"
},
":description_limit": {
"label": "Limit",
"description": "Character limit for attachment descriptions"
},
":background_image": {
"label": "Background image",
"description": "Background image (primarily used by PleromaFE)"
}
}
}
} }
}, },
"time": { "time": {

View File

@ -10,8 +10,9 @@ import listsModule from './modules/lists.js'
import usersModule from './modules/users.js' import usersModule from './modules/users.js'
import apiModule from './modules/api.js' import apiModule from './modules/api.js'
import configModule from './modules/config.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 serverSideStorageModule from './modules/serverSideStorage.js'
import adminSettingsModule from './modules/adminSettings.js'
import shoutModule from './modules/shout.js' import shoutModule from './modules/shout.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import authFlowModule from './modules/auth_flow.js' import authFlowModule from './modules/auth_flow.js'
@ -80,8 +81,9 @@ const persistedStateOptions = {
lists: listsModule, lists: listsModule,
api: apiModule, api: apiModule,
config: configModule, config: configModule,
serverSideConfig: serverSideConfigModule, profileConfig: profileConfigModule,
serverSideStorage: serverSideStorageModule, serverSideStorage: serverSideStorageModule,
adminSettings: adminSettingsModule,
shout: shoutModule, shout: shoutModule,
oauth: oauthModule, oauth: oauthModule,
authFlow: authFlowModule, authFlow: authFlowModule,

View File

@ -0,0 +1,230 @@
import { set, get, cloneDeep, differenceWith, isEqual, flatten } from 'lodash'
export const defaultState = {
frontends: [],
loaded: false,
needsReboot: null,
config: null,
modifiedPaths: null,
descriptions: null,
draft: null,
dbConfigEnabled: null
}
export const newUserFlags = {
...defaultState.flagStorage
}
const adminSettingsStorage = {
state: {
...cloneDeep(defaultState)
},
mutations: {
setInstanceAdminNoDbConfig (state) {
state.loaded = false
state.dbConfigEnabled = false
},
setAvailableFrontends (state, { frontends }) {
state.frontends = frontends.map(f => {
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
} else {
f.refs = [f.ref]
}
return f
})
},
updateAdminSettings (state, { config, modifiedPaths }) {
state.loaded = true
state.dbConfigEnabled = true
state.config = config
state.modifiedPaths = modifiedPaths
},
updateAdminDescriptions (state, { descriptions }) {
state.descriptions = descriptions
},
updateAdminDraft (state, { path, value }) {
const [group, key, subkey] = path
const parent = [group, key, subkey]
set(state.draft, path, value)
// force-updating grouped draft to trigger refresh of group settings
if (path.length > parent.length) {
set(state.draft, parent, cloneDeep(get(state.draft, parent)))
}
},
resetAdminDraft (state) {
state.draft = cloneDeep(state.config)
}
},
actions: {
loadFrontendsStuff ({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor.fetchAvailableFrontends()
.then(frontends => commit('setAvailableFrontends', { frontends }))
},
loadAdminStuff ({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor.fetchInstanceDBConfig()
.then(backendDbConfig => {
if (backendDbConfig.error) {
if (backendDbConfig.error.status === 400) {
backendDbConfig.error.json().then(errorJson => {
if (/configurable_from_database/.test(errorJson.error)) {
commit('setInstanceAdminNoDbConfig')
}
})
}
} else {
dispatch('setInstanceAdminSettings', { backendDbConfig })
}
})
if (state.descriptions === null) {
rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
.then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
}
},
setInstanceAdminSettings ({ state, commit, dispatch }, { backendDbConfig }) {
const config = state.config || {}
const modifiedPaths = new Set()
backendDbConfig.configs.forEach(c => {
const path = [c.group, c.key]
if (c.db) {
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
// Using strings for modified paths for easier searching
c.db.forEach(x => modifiedPaths.add([...path, x].join(' -> ')))
}
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 })
commit('resetAdminDraft')
},
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 })
},
// This action takes draft state, diffs it with live config state and then pushes
// only differences between the two. Difference detection only work up to subkey (third) level.
pushAdminDraft ({ rootState, state, commit, dispatch }) {
// 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] }))
}
}
// Getting all group-keys used in config
const allGroupKeys = flatten(
Object
.entries(state.config)
.map(
([group, lv1data]) => Object
.keys(lv1data)
.map((key) => ({ group, key }))
)
)
// Only using group-keys where there are changes detected
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
return !isEqual(state.config[group][key], state.draft[group][key])
})
// Here we take all changed group-keys and get all changed subkeys
const changed = changedGroupKeys.map(({ group, key }) => {
const config = state.config[group][key]
const draft = state.draft[group][key]
// We convert group-key value into entries arrays
const eConfig = Object.entries(config)
const eDraft = Object.entries(draft)
// Then those entries array we diff so only changed subkey entries remain
// We use the diffed array to reconstruct the object and then shove it into convert()
return ({ group, key, value: convert(Object.fromEntries(differenceWith(eDraft, eConfig, isEqual))) })
})
rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: changed
}
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
},
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 Cookies from 'js-cookie'
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js' import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages' import messages from '../i18n/messages'
import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js' import localeService from '../services/locale/locale.service.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
@ -148,7 +149,7 @@ const config = {
}, },
mutations: { mutations: {
setOption (state, { name, value }) { setOption (state, { name, value }) {
state[name] = value set(state, name, value)
}, },
setHighlight (state, { user, color, type }) { setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user] const data = this.state.config.highlight[user]
@ -178,32 +179,52 @@ const config = {
commit('setHighlight', { user, color, type }) commit('setHighlight', { user, color, type })
}, },
setOption ({ commit, dispatch, state }, { name, value }) { setOption ({ commit, dispatch, state }, { name, value }) {
commit('setOption', { name, value }) const exceptions = new Set([
switch (name) { 'useStreamingApi'
case 'theme': ])
setPreset(value)
break if (exceptions.has(name)) {
case 'sidebarColumnWidth': switch (name) {
case 'contentColumnWidth': case 'useStreamingApi': {
case 'notifsColumnWidth': const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
case 'emojiReactionsScale':
applyConfig(state) dispatch(action).then(() => {
break commit('setOption', { name: 'useStreamingApi', value })
case 'customTheme': }).catch((e) => {
case 'customThemeSource': console.error('Failed starting MastoAPI Streaming socket', e)
applyTheme(value) dispatch('disableMastoSockets')
break dispatch('setOption', { name: 'useStreamingApi', value: false })
case 'interfaceLanguage': })
messages.setLanguage(this.getters.i18n, value) }
dispatch('loadUnicodeEmojiData', value) }
Cookies.set( } else {
BACKEND_LANGUAGE_COOKIE_NAME, commit('setOption', { name, value })
localeService.internalToBackendLocaleMulti(value) switch (name) {
) case 'theme':
break setPreset(value)
case 'thirdColumnMode': break
dispatch('setLayoutWidth', undefined) case 'sidebarColumnWidth':
break 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
}
} }
} }
} }

View File

@ -1,7 +1,9 @@
const defaultState = { const defaultState = {
settingsModalState: 'hidden', settingsModalState: 'hidden',
settingsModalLoaded: false, settingsModalLoadedUser: false,
settingsModalLoadedAdmin: false,
settingsModalTargetTab: null, settingsModalTargetTab: null,
settingsModalMode: 'user',
settings: { settings: {
currentSaveStateNotice: null, currentSaveStateNotice: null,
noticeClearTimeout: null, noticeClearTimeout: null,
@ -54,10 +56,17 @@ const interfaceMod = {
throw new Error('Illegal minimization state of settings modal') throw new Error('Illegal minimization state of settings modal')
} }
}, },
openSettingsModal (state) { openSettingsModal (state, value) {
state.settingsModalMode = value
state.settingsModalState = 'visible' state.settingsModalState = 'visible'
if (!state.settingsModalLoaded) { if (value === 'user') {
state.settingsModalLoaded = true if (!state.settingsModalLoadedUser) {
state.settingsModalLoadedUser = true
}
} else if (value === 'admin') {
if (!state.settingsModalLoadedAdmin) {
state.settingsModalLoadedAdmin = true
}
} }
}, },
setSettingsModalTargetTab (state, value) { setSettingsModalTargetTab (state, value) {
@ -92,8 +101,8 @@ const interfaceMod = {
closeSettingsModal ({ commit }) { closeSettingsModal ({ commit }) {
commit('closeSettingsModal') commit('closeSettingsModal')
}, },
openSettingsModal ({ commit }) { openSettingsModal ({ commit }, value = 'user') {
commit('openSettingsModal') commit('openSettingsModal', value)
}, },
togglePeekSettingsModal ({ commit }) { togglePeekSettingsModal ({ commit }) {
commit('togglePeekSettingsModal') commit('togglePeekSettingsModal')

View File

@ -22,9 +22,9 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
.updateNotificationSettings({ settings }) .updateNotificationSettings({ settings })
.then(result => { .then(result => {
if (result.status === 'success') { if (result.status === 'success') {
commit('confirmServerSideOption', { name, value }) commit('confirmProfileOption', { name, value })
} else { } 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])) export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
const serverSideConfig = { const profileConfig = {
state: { ...defaultState }, state: { ...defaultState },
mutations: { mutations: {
confirmServerSideOption (state, { name, value }) { confirmProfileOption (state, { name, value }) {
set(state, name, value) set(state, name, value)
}, },
wipeServerSideOption (state, { name }) { wipeProfileOption (state, { name }) {
set(state, name, null) set(state, name, null)
}, },
wipeAllServerSideOptions (state) { wipeAllProfileOptions (state) {
Object.keys(settingsMap).forEach(key => { Object.keys(settingsMap).forEach(key => {
set(state, key, null) set(state, key, null)
}) })
@ -118,23 +118,23 @@ const serverSideConfig = {
} }
}, },
actions: { actions: {
setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) { setProfileOption ({ rootState, state, commit, dispatch }, { name, value }) {
const oldValue = get(state, name) const oldValue = get(state, name)
const map = settingsMap[name] const map = settingsMap[name]
if (!map) throw new Error('Invalid server-side setting') if (!map) throw new Error('Invalid server-side setting')
const { set: path = map, api = defaultApi } = map const { set: path = map, api = defaultApi } = map
commit('wipeServerSideOption', { name }) commit('wipeProfileOption', { name })
api({ rootState, commit }, { path, value, oldValue }) api({ rootState, commit }, { path, value, oldValue })
.catch((e) => { .catch((e) => {
console.warn('Error setting server-side option:', e) console.warn('Error setting server-side option:', e)
commit('confirmServerSideOption', { name, value: oldValue }) commit('confirmProfileOption', { name, value: oldValue })
}) })
}, },
logout ({ commit }) { logout ({ commit }) {
commit('wipeAllServerSideOptions') commit('wipeAllProfileOptions')
} }
} }
} }
export default serverSideConfig export default profileConfig

View File

@ -577,6 +577,7 @@ const users = {
loginUser (store, accessToken) { loginUser (store, accessToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const commit = store.commit const commit = store.commit
const dispatch = store.dispatch
commit('beginLogin') commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken) store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((data) => { .then((data) => {
@ -591,57 +592,57 @@ const users = {
commit('setServerSideStorage', user) commit('setServerSideStorage', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
store.dispatch('fetchEmoji') dispatch('fetchEmoji')
getNotificationPermission() getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission)) .then(permission => commit('setNotificationPermission', permission))
// Set our new backend interactor // Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken)) commit('setBackendInteractor', backendInteractorService(accessToken))
store.dispatch('pushServerSideStorage') dispatch('pushServerSideStorage')
if (user.token) { if (user.token) {
store.dispatch('setWsToken', user.token) dispatch('setWsToken', user.token)
// Initialize the shout socket. // Initialize the shout socket.
store.dispatch('initializeSocket') dispatch('initializeSocket')
} }
const startPolling = () => { const startPolling = () => {
// Start getting fresh posts. // Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications // Start fetching notifications
store.dispatch('startFetchingNotifications') dispatch('startFetchingNotifications')
// Start fetching chats // Start fetching chats
store.dispatch('startFetchingChats') dispatch('startFetchingChats')
} }
store.dispatch('startFetchingLists') dispatch('startFetchingLists')
if (user.locked) { if (user.locked) {
store.dispatch('startFetchingFollowRequests') dispatch('startFetchingFollowRequests')
} }
if (store.getters.mergedConfig.useStreamingApi) { if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('fetchTimeline', { timeline: 'friends', since: null }) dispatch('fetchTimeline', { timeline: 'friends', since: null })
store.dispatch('fetchNotifications', { since: null }) dispatch('fetchNotifications', { since: null })
store.dispatch('enableMastoSockets', true).catch((error) => { dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error) console.error('Failed initializing MastoAPI Streaming socket', error)
}).then(() => { }).then(() => {
store.dispatch('fetchChats', { latest: true }) dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
}) })
} else { } else {
startPolling() startPolling()
} }
// Get user mutes // Get user mutes
store.dispatch('fetchMutes') dispatch('fetchMutes')
store.dispatch('setLayoutWidth', windowWidth()) dispatch('setLayoutWidth', windowWidth())
store.dispatch('setLayoutHeight', windowHeight()) dispatch('setLayoutHeight', windowHeight())
// Fetch our friends // Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

View File

@ -108,6 +108,11 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` 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_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 PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
const oldfetch = window.fetch const oldfetch = window.fetch
const fetch = (url, options) => { const fetch = (url, options) => {
@ -1668,6 +1673,94 @@ 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 fetchAvailableFrontends = ({ credentials }) => {
return fetch(PLEROMA_ADMIN_FRONTENDS_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 installFrontend = ({ credentials, payload }) => {
return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_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 = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -1781,7 +1874,12 @@ const apiService = {
postAnnouncement, postAnnouncement,
editAnnouncement, editAnnouncement,
deleteAnnouncement, deleteAnnouncement,
adminFetchAnnouncements adminFetchAnnouncements,
fetchInstanceDBConfig,
fetchInstanceConfigDescriptions,
fetchAvailableFrontends,
pushInstanceDBConfig,
installFrontend
} }
export default apiService export default apiService

View File

@ -1,7 +1,7 @@
// TODO this func might as well take the entire file and use its mimetype // TODO this func might as well take the entire file and use its mimetype
// or the entire service could be just mimetype service that only operates // or the entire service could be just mimetype service that only operates
// on mimetypes and not files. Currently the naming is confusing. // on mimetypes and not files. Currently the naming is confusing.
const fileType = mimetype => { export const fileType = mimetype => {
if (mimetype.match(/flash/)) { if (mimetype.match(/flash/)) {
return 'flash' return 'flash'
} }
@ -25,11 +25,25 @@ const fileType = mimetype => {
return 'unknown' return 'unknown'
} }
const fileMatchesSomeType = (types, file) => export const fileTypeExt = url => {
if (url.match(/\.(png|jpe?g|gif|webp|avif)$/)) {
return 'image'
}
if (url.match(/\.(ogv|mp4|webm|mov)$/)) {
return 'video'
}
if (url.match(/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/)) {
return 'audio'
}
return 'unknown'
}
export const fileMatchesSomeType = (types, file) =>
types.some(type => fileType(file.mimetype) === type) types.some(type => fileType(file.mimetype) === type)
const fileTypeService = { const fileTypeService = {
fileType, fileType,
fileTypeExt,
fileMatchesSomeType fileMatchesSomeType
} }