+
{{ $t('settings.setting_server_side') }}
@@ -33,17 +33,17 @@ library.add(
export default {
components: { Popover },
- props: ['serverSide']
+ props: ['isProfile']
}
diff --git a/src/components/settings_modal/helpers/string_setting.js b/src/components/settings_modal/helpers/string_setting.js
new file mode 100644
index 0000000000..b368cfc8c0
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.js
@@ -0,0 +1,5 @@
+import Setting from './setting.js'
+
+export default {
+ ...Setting
+}
diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue
new file mode 100644
index 0000000000..0cfa61ce77
--- /dev/null
+++ b/src/components/settings_modal/helpers/string_setting.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 0a72dca1e2..ff58f2c38e 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, isEqual } from 'lodash'
import {
newImporter,
newExporter
@@ -53,8 +53,16 @@ const SettingsModal = {
Modal,
Popover,
Checkbox,
- SettingsModalContent: getResettableAsyncComponent(
- () => import('./settings_modal_content.vue'),
+ SettingsModalUserContent: getResettableAsyncComponent(
+ () => import('./settings_modal_user_content.vue'),
+ {
+ loadingComponent: PanelLoading,
+ errorComponent: AsyncComponentError,
+ delay: 0
+ }
+ ),
+ SettingsModalAdminContent: getResettableAsyncComponent(
+ () => import('./settings_modal_admin_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
@@ -147,6 +155,12 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION
]
return clone
+ },
+ resetAdminDraft () {
+ this.$store.commit('resetAdminDraft')
+ },
+ pushAdminDraft () {
+ this.$store.dispatch('pushAdminDraft')
}
},
computed: {
@@ -156,8 +170,14 @@ const SettingsModal = {
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
- modalOpenedOnce () {
- return this.$store.state.interface.settingsModalLoaded
+ modalMode () {
+ return this.$store.state.interface.settingsModalMode
+ },
+ modalOpenedOnceUser () {
+ return this.$store.state.interface.settingsModalLoadedUser
+ },
+ modalOpenedOnceAdmin () {
+ return this.$store.state.interface.settingsModalLoadedAdmin
},
modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized'
@@ -167,9 +187,14 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0
},
set (value) {
- console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
+ },
+ adminDraftAny () {
+ return !isEqual(
+ this.$store.state.adminSettings.config,
+ this.$store.state.adminSettings.draft
+ )
}
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index f58612299d..49ef83e014 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -17,6 +17,12 @@
}
}
+ .setting-description {
+ margin-top: 0.2em;
+ margin-bottom: 2em;
+ font-size: 70%;
+ }
+
.settings-modal-panel {
overflow: hidden;
transition: transform;
@@ -37,7 +43,9 @@
.btn {
min-height: 2em;
- min-width: 10em;
+ }
+
+ .btn:not(.dropdown-button) {
padding: 0 2em;
}
}
@@ -45,6 +53,8 @@
.settings-footer {
display: flex;
+ flex-wrap: wrap;
+ line-height: 2;
>* {
margin-right: 0.5em;
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 7b45737119..4e7fd931a5 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -8,7 +8,7 @@
- {{ $t('settings.settings') }}
+ {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
-
+
+
-
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
new file mode 100644
index 0000000000..f94721ec78
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -0,0 +1,93 @@
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import InstanceTab from './admin_tabs/instance_tab.vue'
+import LimitsTab from './admin_tabs/limits_tab.vue'
+import FrontendsTab from './admin_tabs/frontends_tab.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faWrench,
+ faHand,
+ faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo
+)
+
+const SettingsModalAdminContent = {
+ components: {
+ TabSwitcher,
+
+ InstanceTab,
+ LimitsTab,
+ FrontendsTab
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ isLoggedIn () {
+ return !!this.$store.state.users.currentUser
+ },
+ open () {
+ return this.$store.state.interface.settingsModalState !== 'hidden'
+ },
+ bodyLock () {
+ return this.$store.state.interface.settingsModalState === 'visible'
+ },
+ adminDbLoaded () {
+ return this.$store.state.adminSettings.loaded
+ },
+ adminDescriptionsLoaded () {
+ return this.$store.state.adminSettings.descriptions !== null
+ },
+ noDb () {
+ return this.$store.state.adminSettings.dbConfigEnabled === false
+ }
+ },
+ created () {
+ if (this.user.rights.admin) {
+ this.$store.dispatch('loadAdminStuff')
+ }
+ },
+ methods: {
+ onOpen () {
+ const targetTab = this.$store.state.interface.settingsModalTargetTab
+ // We're being told to open in specific tab
+ if (targetTab) {
+ const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
+ return elm.props && elm.props['data-tab-name'] === targetTab
+ })
+ if (tabIndex >= 0) {
+ this.$refs.tabSwitcher.setTab(tabIndex)
+ }
+ }
+ // Clear the state of target tab, so that next time settings is opened
+ // it doesn't force it.
+ this.$store.dispatch('clearSettingsModalTargetTab')
+ }
+ },
+ mounted () {
+ this.onOpen()
+ },
+ watch: {
+ open: function (value) {
+ if (value) this.onOpen()
+ }
+ }
+}
+
+export default SettingsModalAdminContent
diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss
similarity index 94%
rename from src/components/settings_modal/settings_modal_content.scss
rename to src/components/settings_modal/settings_modal_admin_content.scss
index 87df798219..c984d703ab 100644
--- a/src/components/settings_modal/settings_modal_content.scss
+++ b/src/components/settings_modal/settings_modal_admin_content.scss
@@ -48,9 +48,5 @@
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
-
- .number-input {
- max-width: 6em;
- }
}
}
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
new file mode 100644
index 0000000000..a7a2ac9ad0
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_user_content.js
similarity index 100%
rename from src/components/settings_modal/settings_modal_content.js
rename to src/components/settings_modal/settings_modal_user_content.js
diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss
new file mode 100644
index 0000000000..c984d703ab
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_user_content.scss
@@ -0,0 +1,52 @@
+@import "src/variables";
+
+.settings_tab-switcher {
+ height: 100%;
+
+ .setting-item {
+ border-bottom: 2px solid var(--fg, $fallback--fg);
+ margin: 1em 1em 1.4em;
+ padding-bottom: 1.4em;
+
+ > div,
+ > label {
+ display: block;
+ margin-bottom: 0.5em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .select-multiple {
+ display: flex;
+
+ .option-list {
+ margin: 0;
+ padding-left: 0.5em;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ margin-bottom: 1em;
+ }
+
+ select {
+ min-width: 10em;
+ }
+
+ textarea {
+ width: 100%;
+ max-width: 100%;
+ height: 100px;
+ }
+
+ .unavailable,
+ .unavailable svg {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+ }
+}
diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_user_content.vue
similarity index 92%
rename from src/components/settings_modal/settings_modal_content.vue
rename to src/components/settings_modal/settings_modal_user_content.vue
index 0be76d22c4..0221cccb63 100644
--- a/src/components/settings_modal/settings_modal_content.vue
+++ b/src/components/settings_modal/settings_modal_user_content.vue
@@ -78,6 +78,6 @@
-
+
-
+
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 97046ff0c9..41d1b54f6d 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -7,13 +7,11 @@
{{ $t('settings.hide_filtered_statuses') }}
-
+
-
{{ $t('settings.hide_wordfiltered_statuses') }}
@@ -22,7 +20,8 @@
-
{{ $t('settings.hide_muted_threads') }}
@@ -31,7 +30,8 @@
-
{{ $t('settings.hide_muted_posts') }}
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index be97710f23..3f2bcb13ba 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -7,7 +7,7 @@ import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
-import ServerSideIndicator from '../helpers/server_side_indicator.vue'
+import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
@@ -67,7 +67,7 @@ const GeneralTab = {
SizeSetting,
InterfaceLanguageSwitcher,
ScopeSelector,
- ServerSideIndicator
+ ProfileSettingIndicator
},
computed: {
horizontalUnits () {
@@ -110,7 +110,7 @@ const GeneralTab = {
},
methods: {
changeDefaultScope (value) {
- this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
+ this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
}
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 21e2d855a6..f56fa8e0e2 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -29,14 +29,11 @@
{{ $t('settings.streaming') }}
-
+
-
{{ $t('settings.pause_on_unfocused') }}
@@ -213,7 +210,7 @@
-
@@ -265,7 +262,8 @@
-
{{ $t('settings.no_rich_text_description') }}
@@ -299,7 +297,7 @@
{{ $t('settings.preload_images') }}
@@ -308,7 +306,7 @@
{{ $t('settings.use_one_click_nsfw') }}
@@ -321,15 +319,13 @@
>
{{ $t('settings.loop_video') }}
-
+
-
{{ $t('settings.loop_video_silent_only') }}
@@ -427,18 +423,18 @@
-
- {{ $t('settings.default_vis') }}
+ {{ $t('settings.default_vis') }}
-
-
+
{{ $t('settings.sensitive_by_default') }}
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index dd3806ed1a..fcb92135e8 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -4,7 +4,10 @@
{{ $t('settings.notification_setting_filters') }}
-
-
+
{{ $t('settings.notification_setting_block_from_strangers') }}
@@ -67,7 +70,8 @@
-
{{ $t('settings.notification_setting_hide_notification_contents') }}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 6a5b478a1f..1cc850cbfa 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -254,37 +254,50 @@
{{ $t('settings.account_privacy') }}
-
-
+
{{ $t('settings.lock_account_description') }}
-
-
+
{{ $t('settings.discoverable') }}
-
-
+
{{ $t('settings.allow_following_move') }}
-
-
+
{{ $t('settings.hide_favorites_description') }}
-
-
+
{{ $t('settings.hide_followers_description') }}
-
+
-
{{ $t('settings.hide_followers_count_description') }}
@@ -292,17 +305,18 @@
-
-
+
{{ $t('settings.hide_follows_description') }}
-
+
-
{{ $t('settings.hide_follows_count_description') }}
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
index 6e03bef463..d36d478fb8 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.vue
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -143,8 +143,8 @@
/>
-
foo@example.org
-
+
@@ -175,16 +175,16 @@
{{ $t('settings.move_account') }}
{{ $t('settings.move_account_notes') }}
-
-
- foo@example.org
-
-
+
+
+ foo@example.org
+
+
+
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 2701957750..81c5a6121b 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -115,7 +115,10 @@ const SideDrawer = {
GestureService.updateSwipe(e, this.closeGesture)
},
openSettingsModal () {
- this.$store.dispatch('openSettingsModal')
+ this.$store.dispatch('openSettingsModal', 'user')
+ },
+ openAdminModal () {
+ this.$store.dispatch('openSettingsModal', 'admin')
}
}
}
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 994ac953d9..0958876799 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -180,16 +180,16 @@
v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer"
>
-
{{ $t("nav.administration") }}
-
+
slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
- },
- settingsModalVisible () {
- return this.settingsModalState === 'visible'
- },
- ...mapState({
- settingsModalState: state => state.interface.settingsModalState
- })
+ }
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 01309d0ef4..a7ab451f4c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -519,6 +519,8 @@
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos in a popup frame",
+ "url": "URL",
+ "preview": "Preview",
"file_export_import": {
"backup_restore": "Settings backup",
"backup_settings": "Backup settings to file",
@@ -830,6 +832,98 @@
"title": "Version",
"backend_version": "Backend 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": {
diff --git a/src/main.js b/src/main.js
index 9ca7eb91d5..0b7c7674a1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,8 +10,9 @@ import listsModule from './modules/lists.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
import configModule from './modules/config.js'
-import serverSideConfigModule from './modules/serverSideConfig.js'
+import profileConfigModule from './modules/profileConfig.js'
import serverSideStorageModule from './modules/serverSideStorage.js'
+import adminSettingsModule from './modules/adminSettings.js'
import shoutModule from './modules/shout.js'
import oauthModule from './modules/oauth.js'
import authFlowModule from './modules/auth_flow.js'
@@ -80,8 +81,9 @@ const persistedStateOptions = {
lists: listsModule,
api: apiModule,
config: configModule,
- serverSideConfig: serverSideConfigModule,
+ profileConfig: profileConfigModule,
serverSideStorage: serverSideStorageModule,
+ adminSettings: adminSettingsModule,
shout: shoutModule,
oauth: oauthModule,
authFlow: authFlowModule,
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js
new file mode 100644
index 0000000000..cad9c0ca4d
--- /dev/null
+++ b/src/modules/adminSettings.js
@@ -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 = '', 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
diff --git a/src/modules/config.js b/src/modules/config.js
index 7597886e06..56f8cba55c 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,6 +1,7 @@
import Cookies from 'js-cookie'
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
+import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
@@ -148,7 +149,7 @@ const config = {
},
mutations: {
setOption (state, { name, value }) {
- state[name] = value
+ set(state, name, value)
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
@@ -178,32 +179,52 @@ const config = {
commit('setHighlight', { user, color, type })
},
setOption ({ commit, dispatch, state }, { name, value }) {
- commit('setOption', { name, value })
- switch (name) {
- case 'theme':
- setPreset(value)
- break
- case 'sidebarColumnWidth':
- case 'contentColumnWidth':
- case 'notifsColumnWidth':
- case 'emojiReactionsScale':
- applyConfig(state)
- break
- case 'customTheme':
- case 'customThemeSource':
- applyTheme(value)
- break
- case 'interfaceLanguage':
- messages.setLanguage(this.getters.i18n, value)
- dispatch('loadUnicodeEmojiData', value)
- Cookies.set(
- BACKEND_LANGUAGE_COOKIE_NAME,
- localeService.internalToBackendLocaleMulti(value)
- )
- break
- case 'thirdColumnMode':
- dispatch('setLayoutWidth', undefined)
- break
+ const exceptions = new Set([
+ 'useStreamingApi'
+ ])
+
+ if (exceptions.has(name)) {
+ switch (name) {
+ case 'useStreamingApi': {
+ const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
+
+ dispatch(action).then(() => {
+ commit('setOption', { name: 'useStreamingApi', value })
+ }).catch((e) => {
+ console.error('Failed starting MastoAPI Streaming socket', e)
+ dispatch('disableMastoSockets')
+ dispatch('setOption', { name: 'useStreamingApi', value: false })
+ })
+ }
+ }
+ } else {
+ commit('setOption', { name, value })
+ switch (name) {
+ case 'theme':
+ setPreset(value)
+ break
+ case 'sidebarColumnWidth':
+ case 'contentColumnWidth':
+ case 'notifsColumnWidth':
+ case 'emojiReactionsScale':
+ applyConfig(state)
+ break
+ case 'customTheme':
+ case 'customThemeSource':
+ applyTheme(value)
+ break
+ case 'interfaceLanguage':
+ messages.setLanguage(this.getters.i18n, value)
+ dispatch('loadUnicodeEmojiData', value)
+ Cookies.set(
+ BACKEND_LANGUAGE_COOKIE_NAME,
+ localeService.internalToBackendLocaleMulti(value)
+ )
+ break
+ case 'thirdColumnMode':
+ dispatch('setLayoutWidth', undefined)
+ break
+ }
}
}
}
diff --git a/src/modules/interface.js b/src/modules/interface.js
index a86193eaff..2d012f1ba8 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,7 +1,9 @@
const defaultState = {
settingsModalState: 'hidden',
- settingsModalLoaded: false,
+ settingsModalLoadedUser: false,
+ settingsModalLoadedAdmin: false,
settingsModalTargetTab: null,
+ settingsModalMode: 'user',
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
@@ -54,10 +56,17 @@ const interfaceMod = {
throw new Error('Illegal minimization state of settings modal')
}
},
- openSettingsModal (state) {
+ openSettingsModal (state, value) {
+ state.settingsModalMode = value
state.settingsModalState = 'visible'
- if (!state.settingsModalLoaded) {
- state.settingsModalLoaded = true
+ if (value === 'user') {
+ if (!state.settingsModalLoadedUser) {
+ state.settingsModalLoadedUser = true
+ }
+ } else if (value === 'admin') {
+ if (!state.settingsModalLoadedAdmin) {
+ state.settingsModalLoadedAdmin = true
+ }
}
},
setSettingsModalTargetTab (state, value) {
@@ -92,8 +101,8 @@ const interfaceMod = {
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
- openSettingsModal ({ commit }) {
- commit('openSettingsModal')
+ openSettingsModal ({ commit }, value = 'user') {
+ commit('openSettingsModal', value)
},
togglePeekSettingsModal ({ commit }) {
commit('togglePeekSettingsModal')
diff --git a/src/modules/serverSideConfig.js b/src/modules/profileConfig.js
similarity index 85%
rename from src/modules/serverSideConfig.js
rename to src/modules/profileConfig.js
index 476263bc8b..2cb2014acc 100644
--- a/src/modules/serverSideConfig.js
+++ b/src/modules/profileConfig.js
@@ -22,9 +22,9 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
.updateNotificationSettings({ settings })
.then(result => {
if (result.status === 'success') {
- commit('confirmServerSideOption', { name, value })
+ commit('confirmProfileOption', { name, value })
} else {
- commit('confirmServerSideOption', { name, value: oldValue })
+ commit('confirmProfileOption', { name, value: oldValue })
}
})
}
@@ -94,16 +94,16 @@ export const settingsMap = {
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
-const serverSideConfig = {
+const profileConfig = {
state: { ...defaultState },
mutations: {
- confirmServerSideOption (state, { name, value }) {
+ confirmProfileOption (state, { name, value }) {
set(state, name, value)
},
- wipeServerSideOption (state, { name }) {
+ wipeProfileOption (state, { name }) {
set(state, name, null)
},
- wipeAllServerSideOptions (state) {
+ wipeAllProfileOptions (state) {
Object.keys(settingsMap).forEach(key => {
set(state, key, null)
})
@@ -118,23 +118,23 @@ const serverSideConfig = {
}
},
actions: {
- setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
+ setProfileOption ({ rootState, state, commit, dispatch }, { name, value }) {
const oldValue = get(state, name)
const map = settingsMap[name]
if (!map) throw new Error('Invalid server-side setting')
const { set: path = map, api = defaultApi } = map
- commit('wipeServerSideOption', { name })
+ commit('wipeProfileOption', { name })
api({ rootState, commit }, { path, value, oldValue })
.catch((e) => {
console.warn('Error setting server-side option:', e)
- commit('confirmServerSideOption', { name, value: oldValue })
+ commit('confirmProfileOption', { name, value: oldValue })
})
},
logout ({ commit }) {
- commit('wipeAllServerSideOptions')
+ commit('wipeAllProfileOptions')
}
}
}
-export default serverSideConfig
+export default profileConfig
diff --git a/src/modules/users.js b/src/modules/users.js
index 7b41fab6f3..e976d87536 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -577,6 +577,7 @@ const users = {
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
+ const dispatch = store.dispatch
commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((data) => {
@@ -591,57 +592,57 @@ const users = {
commit('setServerSideStorage', user)
commit('addNewUsers', [user])
- store.dispatch('fetchEmoji')
+ dispatch('fetchEmoji')
getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission))
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
- store.dispatch('pushServerSideStorage')
+ dispatch('pushServerSideStorage')
if (user.token) {
- store.dispatch('setWsToken', user.token)
+ dispatch('setWsToken', user.token)
// Initialize the shout socket.
- store.dispatch('initializeSocket')
+ dispatch('initializeSocket')
}
const startPolling = () => {
// Start getting fresh posts.
- store.dispatch('startFetchingTimeline', { timeline: 'friends' })
+ dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
- store.dispatch('startFetchingNotifications')
+ dispatch('startFetchingNotifications')
// Start fetching chats
- store.dispatch('startFetchingChats')
+ dispatch('startFetchingChats')
}
- store.dispatch('startFetchingLists')
+ dispatch('startFetchingLists')
if (user.locked) {
- store.dispatch('startFetchingFollowRequests')
+ dispatch('startFetchingFollowRequests')
}
if (store.getters.mergedConfig.useStreamingApi) {
- store.dispatch('fetchTimeline', { timeline: 'friends', since: null })
- store.dispatch('fetchNotifications', { since: null })
- store.dispatch('enableMastoSockets', true).catch((error) => {
+ dispatch('fetchTimeline', { timeline: 'friends', since: null })
+ dispatch('fetchNotifications', { since: null })
+ dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error)
}).then(() => {
- store.dispatch('fetchChats', { latest: true })
- setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
+ dispatch('fetchChats', { latest: true })
+ setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
})
} else {
startPolling()
}
// Get user mutes
- store.dispatch('fetchMutes')
+ dispatch('fetchMutes')
- store.dispatch('setLayoutWidth', windowWidth())
- store.dispatch('setLayoutHeight', windowHeight())
+ dispatch('setLayoutWidth', windowWidth())
+ dispatch('setLayoutHeight', windowHeight())
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index e90723a1e9..ac715678b8 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -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_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 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 = {
verifyCredentials,
fetchTimeline,
@@ -1781,7 +1874,12 @@ const apiService = {
postAnnouncement,
editAnnouncement,
deleteAnnouncement,
- adminFetchAnnouncements
+ adminFetchAnnouncements,
+ fetchInstanceDBConfig,
+ fetchInstanceConfigDescriptions,
+ fetchAvailableFrontends,
+ pushInstanceDBConfig,
+ installFrontend
}
export default apiService
diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js
index 5182ecd18b..b92c6c6446 100644
--- a/src/services/file_type/file_type.service.js
+++ b/src/services/file_type/file_type.service.js
@@ -1,7 +1,7 @@
// 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
// on mimetypes and not files. Currently the naming is confusing.
-const fileType = mimetype => {
+export const fileType = mimetype => {
if (mimetype.match(/flash/)) {
return 'flash'
}
@@ -25,11 +25,25 @@ const fileType = mimetype => {
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)
const fileTypeService = {
fileType,
+ fileTypeExt,
fileMatchesSomeType
}