+
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
index cb2436806..fa87c9094 100644
--- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
+++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
@@ -43,7 +43,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
@@ -70,7 +70,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
@@ -97,7 +97,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
@@ -124,7 +124,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
@@ -151,7 +151,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
@@ -178,7 +178,7 @@
>
| {{ entry.instance }} |
- {{ $t("about.mrf.simple.not_applicable") }}
+ {{ $t("general.not_applicable") }}
|
{{ entry.reason }}
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index 83520dc22..a9418cffa 100644
--- a/src/components/mute_card/mute_card.vue
+++ b/src/components/mute_card/mute_card.vue
@@ -1,5 +1,5 @@
-
+
-
+
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
index a166e2196..7dfaa28a5 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -96,6 +96,11 @@
content: "✓";
}
+ &.menu-checkbox-indeterminate::after {
+ font-size: 1.25em;
+ content: "–";
+ }
+
&.-radio {
border-radius: 9999px;
@@ -103,6 +108,11 @@
font-size: 2em;
content: "•";
}
+
+ &.menu-checkbox-indeterminate::after {
+ font-size: 2em;
+ content: "–";
+ }
}
}
}
diff --git a/src/components/selectable_list/selectable_list.js b/src/components/selectable_list/selectable_list.js
deleted file mode 100644
index b82b3d40c..000000000
--- a/src/components/selectable_list/selectable_list.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import Checkbox from 'src/components/checkbox/checkbox.vue'
-import List from 'src/components/list/list.vue'
-
-const SelectableList = {
- components: {
- List,
- Checkbox,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- },
- getKey: {
- type: Function,
- default: (item) => item.id,
- },
- },
- data() {
- return {
- selected: [],
- }
- },
- computed: {
- allKeys() {
- return this.items.map(this.getKey)
- },
- filteredSelected() {
- return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
- },
- allSelected() {
- return this.filteredSelected.length === this.items.length
- },
- noneSelected() {
- return this.filteredSelected.length === 0
- },
- someSelected() {
- return !this.allSelected && !this.noneSelected
- },
- },
- methods: {
- isSelected(item) {
- return this.filteredSelected.indexOf(this.getKey(item)) !== -1
- },
- toggle(checked, item) {
- const key = this.getKey(item)
- const oldChecked = this.isSelected(key)
- if (checked !== oldChecked) {
- if (checked) {
- this.selected.push(key)
- } else {
- this.selected.splice(this.selected.indexOf(key), 1)
- }
- }
- },
- toggleAll(value) {
- if (value) {
- this.selected = this.allKeys.slice(0)
- } else {
- this.selected = []
- }
- },
- },
-}
-
-export default SelectableList
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
deleted file mode 100644
index 3d3a5ff04..000000000
--- a/src/components/selectable_list/selectable_list.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
- toggle(checked, item)"
- @click.stop
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/admin_tabs/admin_card.js b/src/components/settings_modal/admin_tabs/admin_card.js
new file mode 100644
index 000000000..6d9e17b36
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/admin_card.js
@@ -0,0 +1,35 @@
+import { defineAsyncComponent } from 'vue'
+
+import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
+import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
+
+const AdminCard = {
+ props: {
+ userId: {
+ type: String,
+ },
+ },
+ components: {
+ BasicUserCard,
+ ModerationTools,
+ },
+ computed: {
+ user() {
+ return this.$store.getters.findUser(this.userId)
+ },
+ relationship() {
+ return this.$store.getters.relationship(this.userId)
+ },
+ isAdmin() {
+ return this.user.rights.admin
+ },
+ isModerator() {
+ return this.user.rights.moderator
+ },
+ isActivated() {
+ return !this.user.deactivated
+ },
+ },
+}
+
+export default AdminCard
diff --git a/src/components/settings_modal/admin_tabs/admin_card.scss b/src/components/settings_modal/admin_tabs/admin_card.scss
new file mode 100644
index 000000000..9ba9a2dd4
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/admin_card.scss
@@ -0,0 +1,9 @@
+.AdminCard {
+ width: 100%;
+
+ .right-side {
+ align-items: baseline;
+ justify-content: end;
+ display: flex;
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/admin_card.vue b/src/components/settings_modal/admin_tabs/admin_card.vue
new file mode 100644
index 000000000..d0ab596ca
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/admin_card.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('user_card.admin_data.registration_reason') }}
+
+
+ {{ user.adminData.registration_reason }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/admin_tabs/admin_status_card.js b/src/components/settings_modal/admin_tabs/admin_status_card.js
new file mode 100644
index 000000000..ff078a709
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/admin_status_card.js
@@ -0,0 +1,95 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Select from 'src/components/select/select.vue'
+import Status from 'src/components/status/status.vue'
+
+import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js'
+
+const AdminStatusCard = {
+ props: {
+ statusDetails: {
+ type: Object,
+ required: true,
+ validator(u) {
+ return typeof u.id === 'string'
+ },
+ },
+ },
+ data() {
+ return {
+ jsonExpanded: false,
+ statusCache: undefined,
+ }
+ },
+ computed: {
+ isSensitive() {
+ return this.statusDetails.sensitive === true
+ },
+ visibility() {
+ return this.statusDetails.visibility
+ },
+ },
+ methods: {
+ changeSensitivity(v) {
+ this.$store
+ .dispatch('adminChangeStatusScope', {
+ opts: { id: this.statusDetails.id, sensitive: v },
+ })
+ .then((res) => parseStatus(res))
+ .then((s) => (this.statusCache = s))
+ },
+ changeVisibility(v) {
+ this.$store
+ .dispatch('adminChangeStatusScope', {
+ opts: { id: this.statusDetails.id, visibility: v },
+ })
+ .then((res) => parseStatus(res))
+ .then((s) => (this.statusCache = s))
+ },
+ /**
+ * show the confirmation box for bulk actions.
+ * @param {string} box ref name specified for the confirm component
+ */
+ confirmSelection(box) {
+ this.$refs[box].show()
+ this.$refs.dropdown.hidePopover()
+ },
+ /**
+ * called when a bulk action was confirmed
+ * @param {string} action
+ */
+ selectionConfirmed(action, opts) {
+ const restricted = []
+ const s = this.$refs.userList.getSelected()
+ s.forEach((u) => {
+ if (
+ restricted.includes(action) !== false ||
+ u.id !== this.$store.state.users.currentUser.id
+ ) {
+ this.$store.dispatch(action, {
+ id: this.statusDetails.id,
+ ...(opts || {}),
+ })
+ }
+ })
+ this.reset()
+ },
+ },
+ components: {
+ Checkbox,
+ Select,
+ Status,
+ },
+ /**
+ * fetch and cache status info
+ */
+ mounted() {
+ this.$store
+ .dispatch('adminChangeStatusScope', {
+ opts: { id: this.statusDetails.id },
+ })
+ .then((res) => parseStatus(res))
+ .then((s) => (this.statusCache = s))
+ },
+}
+
+export default AdminStatusCard
diff --git a/src/components/settings_modal/admin_tabs/admin_status_card.vue b/src/components/settings_modal/admin_tabs/admin_status_card.vue
new file mode 100644
index 000000000..f56df92e9
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/admin_status_card.vue
@@ -0,0 +1,106 @@
+
+
+ {{ $t('admin_dash.users.title_info') }}:
+
+ -
+ {{ $t('admin_dash.users.status_id') }}: {{ statusDetails.id }}
+
+ -
+ {{ $t('admin_dash.users.created_at') }}: {{ new Date(statusDetails.created_at).toLocaleString() }}
+
+ -
+ {{ $t('admin_dash.users.edited_at') }}: {{ new Date(statusDetails.edited_at).toLocaleString() }}
+
+
+ {{ $t('admin_dash.users.title_content') }}:
+
+ -
+
+
+ action dropdown thingy
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/admin_tabs/auth_tab.js b/src/components/settings_modal/admin_tabs/auth_tab.js
index 627150587..7ccae2ca0 100644
--- a/src/components/settings_modal/admin_tabs/auth_tab.js
+++ b/src/components/settings_modal/admin_tabs/auth_tab.js
@@ -9,6 +9,8 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
import TupleSetting from '../helpers/tuple_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const AuthTab = {
provide() {
return {
@@ -30,9 +32,7 @@ const AuthTab = {
computed: {
...SharedComputedObject(),
LDAPEnabled() {
- return this.$store.state.adminSettings.draft[':pleroma'][':ldap'][
- ':enabled'
- ]
+ return useAdminSettingsStore().draft[':pleroma'][':ldap'][':enabled']
},
},
}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
index b76cd9015..ea7b970fb 100644
--- a/src/components/settings_modal/admin_tabs/frontends_tab.js
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -7,6 +7,7 @@ import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -37,13 +38,13 @@ const FrontendsTab = {
},
created() {
if (this.user.rights.admin) {
- this.$store.dispatch('loadFrontendsStuff')
+ useAdminSettingsStore().loadFrontendsStuff()
}
},
computed: {
...SharedComputedObject(),
frontends() {
- return this.$store.state.adminSettings.frontends
+ return useAdminSettingsStore().frontends
},
},
methods: {
@@ -76,7 +77,7 @@ const FrontendsTab = {
this.working = false
})
.then(async (response) => {
- this.$store.dispatch('loadFrontendsStuff')
+ useAdminSettingsStore().loadFrontendsStuff()
if (response.error) {
const reason = await response.error.json()
useInterfaceStore().pushGlobalNotice({
@@ -104,7 +105,7 @@ const FrontendsTab = {
const ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend
- this.$store.commit('updateAdminDraft', {
+ useAdminSettingsStore.updateAdminDraft({
path: [':pleroma', ':frontends', ':primary'],
value: { name, ref },
})
diff --git a/src/components/settings_modal/admin_tabs/http_tab.js b/src/components/settings_modal/admin_tabs/http_tab.js
index ea76ebe6f..1e45763f9 100644
--- a/src/components/settings_modal/admin_tabs/http_tab.js
+++ b/src/components/settings_modal/admin_tabs/http_tab.js
@@ -12,6 +12,8 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
import TupleSetting from '../helpers/tuple_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const HTTPTab = {
provide() {
return {
@@ -35,7 +37,7 @@ const HTTPTab = {
...SharedComputedObject(),
sslOptions() {
const desc = get(
- this.$store.state.adminSettings.descriptions,
+ useAdminSettingsStore().descriptions,
':pleroma.:http.:adapter.:ssl_options.:versions',
)
return new Set(
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js
index 67d04c303..a428a78f9 100644
--- a/src/components/settings_modal/admin_tabs/instance_tab.js
+++ b/src/components/settings_modal/admin_tabs/instance_tab.js
@@ -12,6 +12,8 @@ import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const InstanceTab = {
provide() {
return {
@@ -34,7 +36,7 @@ const InstanceTab = {
computed: {
...SharedComputedObject(),
providersOptions() {
- const desc = get(this.$store.state.adminSettings.descriptions, [
+ const desc = get(useAdminSettingsStore().descriptions, [
':pleroma',
'Pleroma.Web.Metadata',
':providers',
@@ -47,7 +49,7 @@ const InstanceTab = {
)
},
limitLocalContentOptions() {
- const desc = get(this.$store.state.adminSettings.descriptions, [
+ const desc = get(useAdminSettingsStore().descriptions, [
':pleroma',
':instance',
':limit_to_local_content',
diff --git a/src/components/settings_modal/admin_tabs/links_tab.js b/src/components/settings_modal/admin_tabs/links_tab.js
index b3b5f1f2e..b4563576b 100644
--- a/src/components/settings_modal/admin_tabs/links_tab.js
+++ b/src/components/settings_modal/admin_tabs/links_tab.js
@@ -10,6 +10,8 @@ import ListSetting from '../helpers/list_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const LinksTab = {
provide() {
return {
@@ -30,27 +32,27 @@ const LinksTab = {
computed: {
classIsPresent() {
return (
- this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
+ useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
':class'
] !== false
)
},
relIsPresent() {
return (
- this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
+ useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
':rel'
] !== false
)
},
truncateIsPresent() {
return (
- this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
+ useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
':truncate'
] !== false
)
},
truncateDescription() {
- return get(this.$store.state.adminSettings.descriptions, [
+ return get(useAdminSettingsStore().descriptions, [
':pleroma',
'Pleroma.Formatter',
':truncate',
@@ -58,7 +60,7 @@ const LinksTab = {
},
ttlSettersOptions() {
const desc = get(
- this.$store.state.adminSettings.descriptions,
+ useAdminSettingsStore().descriptions,
':pleroma.:rich_media.:ttl_setters',
)
return new Set(
@@ -70,7 +72,7 @@ const LinksTab = {
},
parsersOptions() {
const desc = get(
- this.$store.state.adminSettings.descriptions,
+ useAdminSettingsStore().descriptions,
':pleroma.:rich_media.:parsers',
)
return new Set(
@@ -97,12 +99,12 @@ const LinksTab = {
]
},
mediaProxyEnabled() {
- return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
+ return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
':enabled'
]
},
mediaInvalidationProvider() {
- return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
+ return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
':invalidation'
][':provider']
},
@@ -110,19 +112,19 @@ const LinksTab = {
},
methods: {
checkRel(e) {
- this.$store.commit('updateAdminDraft', {
+ useAdminSettingsStore.updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
value: e ? '' : false,
})
},
checkClass(e) {
- this.$store.commit('updateAdminDraft', {
+ useAdminSettingsStore.updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':class'],
value: e ? '' : false,
})
},
checkTruncate(e) {
- this.$store.commit('updateAdminDraft', {
+ useAdminSettingsStore.updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
value: e ? 20 : false,
})
diff --git a/src/components/settings_modal/admin_tabs/mailer_tab.js b/src/components/settings_modal/admin_tabs/mailer_tab.js
index 0b909334b..b0750474b 100644
--- a/src/components/settings_modal/admin_tabs/mailer_tab.js
+++ b/src/components/settings_modal/admin_tabs/mailer_tab.js
@@ -7,6 +7,8 @@ import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const MailerTab = {
provide() {
return {
@@ -26,7 +28,7 @@ const MailerTab = {
computed: {
adaptersLabels() {
const prefix = 'Swoosh.Adapters.'
- const descriptions = this.$store.state.adminSettings.descriptions
+ const descriptions = useAdminSettingsStore().descriptions
const options =
descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
.suggestions
@@ -46,20 +48,20 @@ const MailerTab = {
// ]))
},
adapter() {
- return this.$store.state.adminSettings.draft[':pleroma'][
- 'Pleroma.Emails.Mailer'
- ][':adapter']
+ return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
+ ':adapter'
+ ]
},
mailerEnabled() {
- return this.$store.state.adminSettings.draft[':pleroma'][
- 'Pleroma.Emails.Mailer'
- ][':enabled']
+ return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
+ ':enabled'
+ ]
},
...SharedComputedObject(),
},
methods: {
adapterHasKey(key) {
- const descriptions = this.$store.state.adminSettings.descriptions
+ const descriptions = useAdminSettingsStore().descriptions
const mailerStuff = descriptions[':pleroma']['Pleroma.Emails.Mailer']
const adapterStuff = mailerStuff[':subgroup,' + this.adapter]
return Object.hasOwn(adapterStuff, key)
diff --git a/src/components/settings_modal/admin_tabs/media_proxy_tab.js b/src/components/settings_modal/admin_tabs/media_proxy_tab.js
index 6c7231312..9f8a7140e 100644
--- a/src/components/settings_modal/admin_tabs/media_proxy_tab.js
+++ b/src/components/settings_modal/admin_tabs/media_proxy_tab.js
@@ -7,6 +7,8 @@ import ListSetting from '../helpers/list_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const MediaProxyTab = {
provide() {
return {
@@ -25,12 +27,12 @@ const MediaProxyTab = {
},
computed: {
mediaProxyEnabled() {
- return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
+ return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
':enabled'
]
},
mediaInvalidationProvider() {
- return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
+ return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
':invalidation'
][':provider']
},
diff --git a/src/components/settings_modal/admin_tabs/uploads_tab.js b/src/components/settings_modal/admin_tabs/uploads_tab.js
index 760206499..90be43a17 100644
--- a/src/components/settings_modal/admin_tabs/uploads_tab.js
+++ b/src/components/settings_modal/admin_tabs/uploads_tab.js
@@ -4,6 +4,8 @@ import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
const UploadsTab = {
provide() {
return {
@@ -40,9 +42,9 @@ const UploadsTab = {
},
computed: {
uploader() {
- return this.$store.state.adminSettings.draft[':pleroma'][
- 'Pleroma.Upload'
- ][':uploader']
+ return useAdminSettingsStore().draft[':pleroma']['Pleroma.Upload'][
+ ':uploader'
+ ]
},
...SharedComputedObject(),
},
diff --git a/src/components/settings_modal/admin_tabs/users_tab.js b/src/components/settings_modal/admin_tabs/users_tab.js
new file mode 100644
index 000000000..c12111263
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/users_tab.js
@@ -0,0 +1,133 @@
+import { isEmpty } from 'lodash'
+
+import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
+import List from 'src/components/list/list.vue'
+import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import Select from 'src/components/select/select.vue'
+import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
+const UsersTab = {
+ components: {
+ Checkbox,
+ Select,
+ BasicUserCard,
+ List,
+ ProgressButton,
+ AdminCard,
+ TabSwitcher,
+ ModerationTools,
+ GenericConfirm,
+ },
+ provide() {
+ return {
+ defaultDraftMode: true,
+ defaultSource: 'admin',
+ }
+ },
+ data() {
+ return {
+ filtersOrigin: 'local',
+ filtersActivity: 'all',
+ filtersPrivileges: 'all',
+ filtersNeedApproval: false,
+ filtersUnconfirmed: false,
+ filtersQuery: '',
+ filtersName: '',
+ filtersEmail: '',
+ expandedUser: null,
+ }
+ },
+ computed: {
+ /**
+ * do we filter for admins?
+ * @returns {boolean}
+ */
+ filtersIsAdmin() {
+ return (
+ this.filtersPrivileges === 'admin' ||
+ this.filtersPrivileges === 'modsnadmins'
+ )
+ },
+ /**
+ * do we filter for moderators?
+ * @returns {boolean}
+ */
+ filtersIsModerator() {
+ return (
+ this.filtersPrivileges === 'moderator' ||
+ this.filtersPrivileges === 'modsnadmins'
+ )
+ },
+ /**
+ * do we filter for active users?
+ * @returns {boolean}
+ */
+ filtersActive() {
+ return this.filtersActivity === 'active'
+ },
+ /**
+ * do we filter for deactivated users?
+ * @returns {boolean}
+ */
+ filtersDeactivated() {
+ return this.filtersActivity === 'deactivated'
+ },
+ /**
+ * do we filter for local users?
+ * @returns {boolean}
+ */
+ filtersLocal() {
+ return this.filtersOrigin === 'local'
+ },
+ /**
+ * do we filter for external users?
+ * @return {boolean}
+ */
+ filtersExternal() {
+ return this.filtersOrigin === 'external'
+ },
+ fetchOptions() {
+ const filters = {
+ isAdmin: this.filtersIsAdmin,
+ isModerator: this.filtersIsModerator,
+ active: this.filtersActive,
+ deactivated: this.filtersDeactivated,
+ local: this.filtersLocal,
+ external: this.filtersExternal,
+ needApproval: this.filtersNeedApproval,
+ unconfirmed: this.filtersUnconfirmed,
+ }
+
+ return {
+ query: this.filtersQuery,
+ name: this.filtersName,
+ email: this.filtersEmail,
+ pageSize: 50,
+ filters,
+ }
+ },
+ },
+ methods: {
+ fetchUsers(page) {
+ return useAdminSettingsStore()
+ .fetchUsers({
+ ...this.fetchOptions,
+ page,
+ })
+ .then(({ count, users }) => ({ count, items: users }))
+ },
+ },
+ watch: {
+ fetchOptions() {
+ this.$refs.usersList.reset()
+ },
+ },
+}
+
+export default UsersTab
diff --git a/src/components/settings_modal/admin_tabs/users_tab.scss b/src/components/settings_modal/admin_tabs/users_tab.scss
new file mode 100644
index 000000000..537e43d09
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/users_tab.scss
@@ -0,0 +1,30 @@
+.UsersTab {
+ max-height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow-y: hidden;
+
+ .filters-section {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 0.5em 1em;
+
+ > div {
+ flex: 0 1 auto;
+ }
+
+ .filter {
+ display: block;
+ margin-bottom: 0.5em;
+ min-width: 14em;
+
+ .query-label {
+ margin-bottom: 0.5em;
+ }
+
+ > input {
+ width: 100%;
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/users_tab.vue b/src/components/settings_modal/admin_tabs/users_tab.vue
new file mode 100644
index 000000000..1dbd7bcb0
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/users_tab.vue
@@ -0,0 +1,146 @@
+
+
+
+ {{ $t('admin_dash.users.title') }}
+
+
+
+
+
+
+
+ {{ $t('admin_dash.users.labels.origin') }}
+
+
+
+
+
+ {{ $t('admin_dash.users.labels.activity') }}
+
+
+
+
+
+ {{ $t('admin_dash.users.labels.privileges') }}
+
+
+
+
+
+ {{ $t('admin_dash.users.options.only_unapproved') }}
+
+
+
+
+ {{ $t('admin_dash.users.options.only_unconfirmed') }}
+
+
+
+
+
+
+
+
+
+
+
+ loading
+
+
+ no users
+
+
+
+
+
+
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
index af8ebecbd..2202d128c 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -4,6 +4,7 @@ import DraftButtons from './draft_buttons.vue'
import LocalSettingIndicator from './local_setting_indicator.vue'
import ModifiedIndicator from './modified_indicator.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js'
@@ -125,14 +126,14 @@ export default {
draft: {
get() {
if (this.realSource === 'admin' || this.path == null) {
- return get(this.$store.state.adminSettings.draft, this.canonPath)
+ return get(useAdminSettingsStore().draft, this.canonPath)
} else {
return this.localDraft
}
},
set(value) {
if (this.realSource === 'admin' || this.path == null) {
- this.$store.commit('updateAdminDraft', {
+ useAdminSettingsStore.updateAdminDraft({
path: this.canonPath,
value,
})
@@ -164,10 +165,7 @@ export default {
: this.draftMode
},
backendDescription() {
- return get(
- this.$store.state.adminSettings.descriptions,
- this.descriptionPath,
- )
+ return get(useAdminSettingsStore().descriptions, this.descriptionPath)
},
backendDescriptionLabel() {
if (this.realSource !== 'admin') return ''
@@ -221,10 +219,7 @@ export default {
let parentValue = null
if (this.parentPath !== undefined && this.realSource === 'admin') {
if (this.realDraftMode) {
- parentValue = get(
- this.$store.state.adminSettings.draft,
- this.parentPath,
- )
+ parentValue = get(useAdminSettingsStore().draft, this.parentPath)
} else {
parentValue = get(this.configSource, this.parentPath)
}
@@ -243,7 +238,7 @@ export default {
case 'profile':
return this.$store.state.profileConfig
case 'admin':
- return this.$store.state.adminSettings.config
+ return useAdminSettingsStore().config
default:
return useMergedConfigStore().mergedConfig
}
@@ -259,7 +254,7 @@ export default {
this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) =>
- this.$store.dispatch('pushAdminSetting', { path: k, value: v })
+ useAdminSettingsStore.pushAdminSetting({ path: k, value: v })
default:
return (readPath, value) => {
const writePath = `${readPath}`
@@ -372,9 +367,7 @@ export default {
canHardReset() {
return (
this.realSource === 'admin' &&
- this.$store.state.adminSettings.modifiedPaths?.has(
- this.canonPath.join(' -> '),
- )
+ useAdminSettingsStore().modifiedPaths?.has(this.canonPath.join(' -> '))
)
},
matchesExpertLevel() {
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index 39e0b5b9c..4a3b9d4f8 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -1,6 +1,7 @@
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
const SharedComputedObject = () => ({
@@ -8,9 +9,11 @@ const SharedComputedObject = () => ({
...mapPiniaState(useMergedConfigStore, {
expertLevel: (store) => store.mergedConfig.expertLevel,
}),
+ ...mapPiniaState(useAdminSettingsStore, {
+ adminConfig: (store) => store.config,
+ adminDraft: (store) => store.draft,
+ }),
...mapState({
- adminConfig: (state) => state.adminSettings.config,
- adminDraft: (state) => state.adminSettings.draft,
user: (state) => state.users.currentUser,
}),
})
diff --git a/src/components/settings_modal/helpers/vertical_tab_switcher.scss b/src/components/settings_modal/helpers/vertical_tab_switcher.scss
index 67da9305b..4c0e94dff 100644
--- a/src/components/settings_modal/helpers/vertical_tab_switcher.scss
+++ b/src/components/settings_modal/helpers/vertical_tab_switcher.scss
@@ -40,6 +40,7 @@
.tab-slot-wrapper {
flex: 1 1 auto;
+ position: relative;
height: 100%;
padding: 0 1em;
overflow-y: auto;
@@ -56,10 +57,16 @@
}
&.-full-height {
+ height: 100%;
> * {
height: 100%;
}
}
+
+ &.-full-width.-full-height {
+ position: absolute;
+ inset: 0;
+ }
}
}
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index e2df79c26..34e9cad25 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -8,6 +8,7 @@ import Modal from 'src/components/modal/modal.vue'
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import Popover from 'src/components/popover/popover.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
@@ -232,10 +233,10 @@ const SettingsModal = {
return clone
},
resetAdminDraft() {
- this.$store.commit('resetAdminDraft')
+ useAdminSettingsStore.resetAdminDraft()
},
pushAdminDraft() {
- this.$store.dispatch('pushAdminDraft')
+ useAdminSettingsStore.pushAdminDraft()
},
...mapActions(useInterfaceStore, [
'temporaryChangesRevert',
@@ -265,8 +266,8 @@ const SettingsModal = {
},
adminDraftAny() {
return !isEqual(
- this.$store.state.adminSettings.config,
- this.$store.state.adminSettings.draft,
+ useAdminSettingsStore().config,
+ useAdminSettingsStore().draft,
)
},
},
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 7d6e17244..e1405a42f 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -36,6 +36,38 @@
margin-top: 0.75em;
}
+ .toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5em;
+ align-items: end;
+
+ .header-buttons {
+ display: flex;
+ flex: 1 0 auto;
+ justify-content: end;
+ align-items: end;
+
+ &:not(.btn-group) {
+ gap: 0.5em;
+ }
+
+ .header-text {
+ flex: 1 0 auto;
+ }
+ }
+
+ .button-default {
+ flex: 0 0 auto;
+ padding: 0.5em;
+ font-size: 1rem;
+ }
+
+ .popover-wrapper {
+ display: flex;
+ }
+ }
+
p {
line-height: 1.5;
margin-left: 2em;
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
index 6cd760833..fb2d26778 100644
--- a/src/components/settings_modal/settings_modal_admin_content.js
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -1,3 +1,4 @@
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import AuthTab from './admin_tabs/auth_tab.vue'
import EmojiTab from './admin_tabs/emoji_tab.vue'
import FederationTab from './admin_tabs/federation_tab.vue'
@@ -15,27 +16,35 @@ import PostsTab from './admin_tabs/posts_tab.vue'
import RatesTab from './admin_tabs/rates_tab.vue'
import RegistrationsTab from './admin_tabs/registrations_tab.vue'
import UploadsTab from './admin_tabs/uploads_tab.vue'
+import UsersTab from './admin_tabs/users_tab.vue'
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
+ faBell,
faChain,
faChartLine,
faCircleNodes,
faDoorOpen,
+ faDownload,
faEllipsis,
faEnvelope,
+ faEyeSlash,
faGauge,
faGears,
faGlobe,
faHand,
+ faInfo,
faKey,
faLaptopCode,
faMessage,
+ faPaintBrush,
faTowerBroadcast,
faUpload,
+ faUser,
faWrench,
} from '@fortawesome/free-solid-svg-icons'
@@ -45,6 +54,12 @@ library.add(
faChain,
faGlobe,
faLaptopCode,
+ faPaintBrush,
+ faBell,
+ faDownload,
+ faEyeSlash,
+ faInfo,
+ faUser,
faTowerBroadcast,
faEnvelope,
faChartLine,
@@ -63,11 +78,12 @@ const SettingsModalAdminContent = {
VerticalTabSwitcher,
InstanceTab,
+ UsersTab,
+ LimitsTab,
RegistrationsTab,
EmojiTab,
FrontendsTab,
FederationTab,
- LimitsTab,
MailerTab,
UploadsTab,
MediaProxyTab,
@@ -94,18 +110,18 @@ const SettingsModalAdminContent = {
return useInterfaceStore().settingsModalState === 'visible'
},
adminDbLoaded() {
- return this.$store.state.adminSettings.loaded
+ return useAdminSettingsStore().loaded
},
adminDescriptionsLoaded() {
- return this.$store.state.adminSettings.descriptions !== null
+ return useAdminSettingsStore().descriptions !== null
},
noDb() {
- return this.$store.state.adminSettings.dbConfigEnabled === false
+ return useAdminSettingsStore().dbConfigEnabled === false
},
},
created() {
if (this.user.rights.admin) {
- this.$store.dispatch('loadAdminStuff')
+ useAdminSettingsStore().loadAdminStuff()
}
},
methods: {
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
index 94f843871..679cdde42 100644
--- a/src/components/settings_modal/settings_modal_admin_content.vue
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -49,6 +49,17 @@
+
+
+
+
$store.dispatch('fetchBlocks'),
- select: (props, $store) =>
- get($store.state.users.currentUser, 'blockIds', []),
- destroy: () => {
- /* no-op */
- },
- childPropName: 'items',
-})(SelectableList)
-
-const MuteList = withLoadMore({
- fetch: (props, $store) => $store.dispatch('fetchMutes'),
- select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
- destroy: () => {
- /* no-op */
- },
- childPropName: 'items',
-})(SelectableList)
-
-const DomainMuteList = withSubscription({
- fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
- select: (props, $store) =>
- get($store.state.users.currentUser, 'domainMutes', []),
- childPropName: 'items',
-})(SelectableList)
-
const MutesAndBlocks = {
data() {
return {
activeTab: 'profile',
+ mutesLoading: false,
+ mutesError: null,
+ mutesBottomedOut: false,
+ blocksLoading: false,
+ blocksError: null,
+ blocksBottomedOut: false,
+ domainsLoading: false,
+ domainsError: null,
+ domainsBottomedOut: false,
}
},
created() {
@@ -52,12 +33,10 @@ const MutesAndBlocks = {
},
components: {
TabSwitcher,
- BlockList,
- MuteList,
- DomainMuteList,
- BlockCard,
- MuteCard,
DomainMuteCard,
+ BlockCard,
+ List,
+ MuteCard,
ProgressButton,
Autosuggest,
Checkbox,
@@ -69,8 +48,20 @@ const MutesAndBlocks = {
user() {
return this.$store.state.users.currentUser
},
+ blocks() {
+ return get(this.$store.state.users.currentUser, 'blockIds', [])
+ },
+ mutes() {
+ return get(this.$store.state.users.currentUser, 'muteIds', [])
+ },
+ domains() {
+ return get(this.$store.state.users.currentUser, 'domainMutes', [])
+ },
},
methods: {
+ fetchItems(group) {
+ return () => this.$store.dispatch('fetch' + group, this.userId)
+ },
importFollows(file) {
return this.$store.state.api.backendInteractor
.importFollows({ file })
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
index 5fa3a27b1..c07de6be1 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
@@ -1,5 +1,5 @@
.mutes-and-blocks-tab {
- height: 100%;
+ min-height: 100%;
.usersearch-wrapper {
padding: 1em;
@@ -26,4 +26,13 @@
margin-top: 1em;
width: 10em;
}
+
+ .blocks,
+ .mutes {
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+ position: absolute;
+ inset: 0;
+ }
}
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
index ed4b15a49..1bc446ffb 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -1,9 +1,12 @@
-
+ :scrollable-tabs="true"
+ >
+
-
@@ -51,103 +57,106 @@
{{ $t('settings.no_blocks') }}
-
+
-
-
-
-
-
+
+
+
+
+
-
-
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
-
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
-
-
-
-
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
-
-
-
- {{ $t('user_card.unmute') }}
-
- {{ $t('user_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
-
-
-
+
+
+
+
+
-
-
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
-
+
-
-
-
-
- {{ $t('domain_mute_card.unmute') }}
-
- {{ $t('domain_mute_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
-
+
+
+ {{ item }}
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index acd25b512..0107bde3c 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -262,7 +262,9 @@ const Status = {
},
muteFilterHits() {
return muteFilterHits(
- Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters || {}),
+ Object.values(
+ useSyncConfigStore().prefsStorage.simple.muteFilters || {},
+ ),
this.status,
)
},
diff --git a/src/components/status_action_buttons/action_button.js b/src/components/status_action_buttons/action_button.js
index 661d1befd..e3cdf1fb1 100644
--- a/src/components/status_action_buttons/action_button.js
+++ b/src/components/status_action_buttons/action_button.js
@@ -19,6 +19,7 @@ import {
faChevronDown,
faChevronRight,
faExternalLinkAlt,
+ faEye,
faEyeSlash,
faHistory,
faMinus,
@@ -51,6 +52,7 @@ library.add(
faBookmark,
faBookmarkRegular,
faEyeSlash,
+ faEye,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js
index 012844dc8..1f4cfc3f6 100644
--- a/src/components/status_action_buttons/action_button_container.js
+++ b/src/components/status_action_buttons/action_button_container.js
@@ -3,14 +3,30 @@ import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue'
import ActionButton from './action_button.vue'
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import {
+ faEnvelope,
+ faEye,
+ faEyeSlash,
faFolderTree,
faGlobe,
+ faLock,
+ faLockOpen,
faUser,
} from '@fortawesome/free-solid-svg-icons'
-library.add(faUser, faGlobe, faFolderTree)
+library.add(
+ faUser,
+ faGlobe,
+ faFolderTree,
+ faEye,
+ faEyeSlash,
+ faLock,
+ faLockOpen,
+ faEnvelope,
+)
export default {
components: {
@@ -61,8 +77,27 @@ export default {
this.domain,
)
},
+ availableScopes() {
+ return ['private', 'unlisted', 'direct', 'public'].filter((scope) => {
+ return scope !== this.status.visibility
+ })
+ },
},
methods: {
+ visibilityIcon(visibility) {
+ switch (visibility) {
+ case 'private':
+ return 'lock'
+ case 'unlisted':
+ return 'lock-open'
+ case 'direct':
+ return 'envelope'
+ case 'local':
+ return 'igloo'
+ default:
+ return 'globe'
+ }
+ },
unmuteUser() {
return this.$store.dispatch('unmuteUser', this.user.id)
},
@@ -79,6 +114,18 @@ export default {
this.$refs.confirmUser.optionallyPrompt()
}
},
+ setScope(visibility) {
+ return useAdminSettingsStore().changeStatusScope({
+ id: this.status.id,
+ visibility,
+ })
+ },
+ setSensitive(sensitive) {
+ useAdminSettingsStore().changeStatusScope({
+ id: this.status.id,
+ sensitive,
+ })
+ },
toggleConversationMute() {
if (this.conversationIsMuted) {
this.unmuteConversation()
diff --git a/src/components/status_action_buttons/action_button_container.vue b/src/components/status_action_buttons/action_button_container.vue
index da0beed79..919207622 100644
--- a/src/components/status_action_buttons/action_button_container.vue
+++ b/src/components/status_action_buttons/action_button_container.vue
@@ -14,6 +14,58 @@
/>
+
useMergedConfigStore().mergedConfig.modalOnDelete,
@@ -243,6 +243,26 @@ export const BUTTONS = [
return dispatch('deleteStatus', { id: status.id })
},
},
+ {
+ // =========
+ // CHANGE SCOPE
+ // =========
+ name: 'changeScope',
+ icon: 'eye',
+ label: 'status.admin_change_scope',
+ if({ status, loggedIn, currentUser }) {
+ return (
+ loggedIn &&
+ (status.user.id === currentUser.id ||
+ currentUser.privileges.has('messages_delete'))
+ )
+ },
+ toggleable: false,
+ dropdown: true,
+ action({ status, dispatch, emit }) {
+ /* prevent hiding */
+ },
+ },
{
// =========
// SHARE/COPY
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index f788ea30e..30e0bd903 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -49,6 +49,7 @@
.contents.scrollable-tabs {
flex-basis: 0;
+ position: relative;
}
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index d987695dd..acb2c4ea1 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -84,6 +84,12 @@ export default {
default: false,
type: Boolean,
},
+ // Hide action buttons
+ hideButtons: {
+ required: false,
+ default: false,
+ type: Boolean,
+ },
// default - open profile, 'zoom' - zoom, function - call function
avatarAction: {
required: false,
@@ -280,7 +286,7 @@ export default {
},
},
visibleRole() {
- if (!this.newShowRole) {
+ if (!this.user.show_role && !this.user.adminData) {
return
}
const rights = this.user.rights
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
index e676db766..b724c335b 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -22,6 +22,23 @@
}
}
+ summary {
+ font-weight: bold;
+ cursor: pointer;
+ }
+
+ details {
+ margin: 0.5em 0;
+
+ &[open] summary {
+ margin-bottom: 0.5em;
+ }
+
+ ul {
+ margin: 0;
+ }
+ }
+
.input.bio {
height: auto; // override settings default textarea size
}
@@ -56,7 +73,7 @@
.user-card-bio {
text-align: center;
- margin: 0 0.6em;
+ margin: 0.6em;
&.input {
margin: 0 1em;
@@ -81,15 +98,18 @@
--_still-image-label-visibility: hidden;
}
+ .admin-data,
.personal-marks {
- margin: 0.6em;
- padding: 0.6em;
+ margin: 0 0.6em;
+ padding: 0 0.6em;
&:not(:last-child) {
border-bottom: 1px dotted var(--border);
}
.highlighter {
+ margin-bottom: 0.5em;
+
h4 {
margin-top: 0.6em;
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 3eb142954..cdea958af 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -104,7 +104,7 @@
/>
@@ -228,7 +228,7 @@
+
+
+
+ {{ $t('user_card.admin_data.data') }}
+
+
+
+ -
+ {{ $t('admin_dash.users.local_id') }}
+
+ -
+ {{ user.adminData.id }}
+
+
+
+ -
+ {{ $t('admin_dash.users.labels.email') }}
+
+ -
+ {{ user.adminData.email == null ? $t('general.not_available') : user.adminData.email }}
+
+
+
+ -
+ {{ $t('general.role.admin') }}
+
+ -
+ {{ $t('general.' + (user.adminData.roles.admin ? 'yes' : 'no')) }}
+
+
+
+ -
+ {{ $t('general.role.moderator') }}
+
+ -
+ {{ $t('general.' + (user.adminData.roles.moderator ? 'yes' : 'no')) }}
+
+
+
+ -
+ {{ $t('admin_dash.users.indicator.confirmed') }}
+
+ -
+ {{ $t('general.' + (user.adminData.is_confirmed ? 'yes' : 'no')) }}
+
+
+
+ -
+ {{ $t('admin_dash.users.indicator.approved') }}
+
+ -
+ {{ $t('general.' + (user.adminData.is_approved ? 'yes' : 'no')) }}
+
+
+
+ -
+ {{ $t('admin_dash.users.indicator.suggested') }}
+
+ -
+ {{ $t('general.' + (user.adminData.is_suggested ? 'yes' : 'no')) }}
+
+
+
+
+ {{ $t('user_card.admin_data.registration_reason') }}
+
+
+ {{ user.adminData.registration_reason == null ? $t('general.not_available') : user.adminData.registration_reason }}
+
+
+
+
+ {{ $t('user_card.admin_data.tags') }}
+
+
+ -
+ {{ $t('general.none') }}
+
+ -
+
+ {{ tag }}
+
+ {{ ' ' }}
+
+
+
+
+
+
{{ $t('settings.bio') }}
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index d0923d037..d2d766a7f 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,17 +1,15 @@
import { get } from 'lodash'
import { mapState } from 'pinia'
-import Conversation from 'src/components/conversation/conversation.vue'
import FollowCard from 'src/components/follow_card/follow_card.vue'
import List from 'src/components/list/list.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Timeline from 'src/components/timeline/timeline.vue'
import UserCard from 'src/components/user_card/user_card.vue'
-import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { useInstanceStore } from 'src/stores/instance.js'
-import { useInterfaceStore } from 'src/stores/interface.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -19,28 +17,6 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch)
-const FollowerList = withLoadMore({
- fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
- select: (props, $store) =>
- get($store.getters.findUser(props.userId), 'followerIds', []).map((id) =>
- $store.getters.findUser(id),
- ),
- destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
- childPropName: 'items',
- additionalPropNames: ['userId'],
-})(List)
-
-const FriendList = withLoadMore({
- fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
- select: (props, $store) =>
- get($store.getters.findUser(props.userId), 'friendIds', []).map((id) =>
- $store.getters.findUser(id),
- ),
- destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
- childPropName: 'items',
- additionalPropNames: ['userId'],
-})(List)
-
const defaultTabKey = 'statuses'
const UserProfile = {
@@ -64,6 +40,8 @@ const UserProfile = {
unmounted() {
this.stopFetching()
useInterfaceStore().setForeignProfileBackground(null)
+ this.$store.dispatch('clearFollowers', this.userId)
+ this.$store.dispatch('clearFriends', this.userId)
},
computed: {
timeline() {
@@ -104,11 +82,31 @@ const UserProfile = {
compactProfiles() {
return useMergedConfigStore().mergedConfig.compactProfiles
},
+ friends() {
+ return get(
+ this.$store.getters.findUser(this.userId),
+ 'friendIds',
+ [],
+ ).map((id) => this.$store.getters.findUser(id))
+ },
+ followers() {
+ return get(
+ this.$store.getters.findUser(this.userId),
+ 'followerIds',
+ [],
+ ).map((id) => this.$store.getters.findUser(id))
+ },
},
methods: {
setFooterRef(el) {
this.footerRef = el
},
+ fetchUsers(group) {
+ return () =>
+ this.$store
+ .dispatch('fetch' + group, this.userId)
+ .then((result) => ({ items: result }))
+ },
load(userNameOrId) {
const startFetchingTimeline = (timeline, userId) => {
// Clear timeline only if load another user's profile
@@ -203,11 +201,9 @@ const UserProfile = {
components: {
UserCard,
Timeline,
- FollowerList,
- FriendList,
+ List,
FollowCard,
TabSwitcher,
- Conversation,
},
}
diff --git a/src/components/user_profile/user_profile.scss b/src/components/user_profile/user_profile.scss
new file mode 100644
index 000000000..35aa49ca9
--- /dev/null
+++ b/src/components/user_profile/user_profile.scss
@@ -0,0 +1,65 @@
+.user-profile {
+ flex: 2;
+
+ .card-wrapper {
+ border-top-left-radius: var(--roundness);
+ border-top-right-radius: var(--roundness);
+ }
+
+ .panel-footer {
+ border-bottom-left-radius: var(--roundness);
+ border-bottom-right-radius: var(--roundness);
+ }
+
+ // No sticky header on user profile
+ --currentPanelStack: 0;
+
+ .userlist-placeholder {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 2em;
+ }
+
+ .user-info {
+ margin: 1.2em;
+ }
+
+ &.-admin-view {
+ .godmode {
+ padding: 1em;
+ }
+
+ .list-item {
+ padding: 0;
+ border-bottom: 1px solid var(--border);
+
+ .Status{
+ width: 100%
+ }
+ }
+
+ .footer {
+ background: var(--background);
+ }
+ }
+}
+
+.user-profile-placeholder {
+ .panel-body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 7em;
+ }
+
+ .alert {
+ padding: 0.75em 5em;
+ border-width: 2px;
+
+ .error-message {
+ color: var(--text);
+ font-weight: bold;
+ }
+ }
+}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 72515c90d..6ca0c39bd 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -39,14 +39,14 @@
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
-
-
+
-
-
+
-
+
diff --git a/src/components/user_profile/user_profile_admin_view.js b/src/components/user_profile/user_profile_admin_view.js
new file mode 100644
index 000000000..73f9d83d2
--- /dev/null
+++ b/src/components/user_profile/user_profile_admin_view.js
@@ -0,0 +1,69 @@
+import { get } from 'lodash'
+import { mapState } from 'pinia'
+
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import List from 'src/components/list/list.vue'
+import Status from 'src/components/status/status.vue'
+import UserCard from 'src/components/user_card/user_card.vue'
+
+import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
+import { useInterfaceStore } from 'src/stores/interface.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faCircleNotch)
+
+const UserProfileAdminView = {
+ data() {
+ return {
+ userId: null,
+ godmode: false,
+ }
+ },
+ created() {
+ this.userId = this.$route.params.id
+ this.$store.dispatch('fetchUserIfMissing', this.userId)
+ useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
+ },
+ updated() {
+ useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
+ },
+ unmounted() {
+ useInterfaceStore().setForeignProfileBackground(null)
+ },
+ computed: {
+ fetchOptions() {
+ return {
+ pageSize: 20,
+ godmode: this.godmode,
+ id: this.userId,
+ withReblogs: false,
+ }
+ },
+ user() {
+ return this.$store.getters.findUser(this.userId)
+ },
+ },
+ methods: {
+ fetchStatuses(page) {
+ return useAdminSettingsStore().fetchStatuses({
+ ...this.fetchOptions,
+ page,
+ })
+ },
+ },
+ components: {
+ UserCard,
+ List,
+ Status,
+ Checkbox,
+ },
+ watch: {
+ godmode() {
+ this.$refs.list.reset()
+ },
+ },
+}
+
+export default UserProfileAdminView
diff --git a/src/components/user_profile/user_profile_admin_view.vue b/src/components/user_profile/user_profile_admin_view.vue
new file mode 100644
index 000000000..992ce3b4b
--- /dev/null
+++ b/src/components/user_profile/user_profile_admin_view.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+ {{ $t('admin_dash.users.godmode') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/hocs/with_load_more/with_load_more.jsx b/src/hocs/with_load_more/with_load_more.jsx
deleted file mode 100644
index e839f796f..000000000
--- a/src/hocs/with_load_more/with_load_more.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-// eslint-disable-next-line no-unused
-
-import { isEmpty } from 'lodash'
-import { h } from 'vue'
-
-import { getComponentProps } from '../../services/component_utils/component_utils'
-import './with_load_more.scss'
-
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
-import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
-
-library.add(faCircleNotch)
-
-const withLoadMore =
- ({
- fetch, // function to fetch entries and return a promise
- select, // function to select data from store
- unmounted, // function called at "destroyed" lifecycle
- childPropName = 'entries', // name of the prop to be passed into the wrapped component
- additionalPropNames = [], // additional prop name list of the wrapper component
- }) =>
- (WrappedComponent) => {
- const originalProps = Object.keys(getComponentProps(WrappedComponent))
- const props = originalProps
- .filter((v) => v !== childPropName)
- .concat(additionalPropNames)
-
- return {
- props,
- data() {
- return {
- loading: false,
- bottomedOut: false,
- error: false,
- entries: [],
- }
- },
- created() {
- window.addEventListener('scroll', this.scrollLoad)
- if (this.entries.length === 0) {
- this.fetchEntries()
- }
- },
- unmounted() {
- window.removeEventListener('scroll', this.scrollLoad)
- unmounted && unmounted(this.$props, this.$store)
- },
- methods: {
- // Entries is not a computed because computed can't track the dynamic
- // selector for changes and won't trigger after fetch.
- updateEntries() {
- this.entries = select(this.$props, this.$store) || []
- },
- fetchEntries() {
- if (!this.loading) {
- this.loading = true
- this.error = false
- fetch(this.$props, this.$store)
- .then((newEntries) => {
- this.loading = false
- this.bottomedOut = isEmpty(newEntries)
- })
- .catch(() => {
- this.loading = false
- this.error = true
- })
- .finally(() => {
- this.updateEntries()
- })
- }
- },
- scrollLoad(e) {
- const bodyBRect = document.body.getBoundingClientRect()
- const height = Math.max(bodyBRect.height, -bodyBRect.y)
- if (
- this.loading === false &&
- this.bottomedOut === false &&
- this.$el.offsetHeight > 0 &&
- window.innerHeight + window.pageYOffset >= height - 750
- ) {
- this.fetchEntries()
- }
- },
- },
- render() {
- const props = {
- ...this.$props,
- [childPropName]: this.entries,
- }
- const children = this.$slots
- return (
-
- {children}
-
-
- )
- },
- }
- }
-
-export default withLoadMore
diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss
deleted file mode 100644
index 6b007ac2e..000000000
--- a/src/hocs/with_load_more/with_load_more.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.with-load-more {
- &-footer {
- padding: 0.9em;
- text-align: center;
- border-top: 1px solid;
- border-top-color: var(--border);
-
- .error {
- font-size: 1rem;
- }
-
- a {
- cursor: pointer;
- }
- }
-}
diff --git a/src/hocs/with_subscription/with_subscription.jsx b/src/hocs/with_subscription/with_subscription.jsx
deleted file mode 100644
index 7805161c6..000000000
--- a/src/hocs/with_subscription/with_subscription.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-// eslint-disable-next-line no-unused
-
-import { isEmpty } from 'lodash'
-import { h } from 'vue'
-
-import { getComponentProps } from '../../services/component_utils/component_utils'
-import './with_subscription.scss'
-
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
-import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
-
-library.add(faCircleNotch)
-
-const withSubscription =
- ({
- fetch, // function to fetch entries and return a promise
- select, // function to select data from store
- childPropName = 'content', // name of the prop to be passed into the wrapped component
- additionalPropNames = [], // additional prop name list of the wrapper component
- }) =>
- (WrappedComponent) => {
- const originalProps = Object.keys(getComponentProps(WrappedComponent))
- const props = originalProps
- .filter((v) => v !== childPropName)
- .concat(additionalPropNames)
-
- return {
- props: [
- ...props,
- 'refresh', // boolean saying to force-fetch data whenever created
- ],
- data() {
- return {
- loading: false,
- error: false,
- }
- },
- computed: {
- fetchedData() {
- return select(this.$props, this.$store)
- },
- },
- created() {
- if (this.refresh || isEmpty(this.fetchedData)) {
- this.fetchData()
- }
- },
- methods: {
- fetchData() {
- if (!this.loading) {
- this.loading = true
- this.error = false
- fetch(this.$props, this.$store)
- .then(() => {
- this.loading = false
- })
- .catch(() => {
- this.error = true
- this.loading = false
- })
- }
- },
- },
- render() {
- if (!this.error && !this.loading) {
- const props = {
- ...this.$props,
- [childPropName]: this.fetchedData,
- }
- const children = this.$slots
- return (
-
- {children}
-
- )
- } else {
- return (
-
- )
- }
- },
- }
- }
-
-export default withSubscription
diff --git a/src/hocs/with_subscription/with_subscription.scss b/src/hocs/with_subscription/with_subscription.scss
deleted file mode 100644
index 8c0af336c..000000000
--- a/src/hocs/with_subscription/with_subscription.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.with-subscription {
- &-loading {
- padding: 0.7em;
- text-align: center;
-
- .error {
- font-size: 1rem;
- }
- }
-}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e03e21403..e7c03d673 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -86,6 +86,7 @@
"apply": "Apply",
"submit": "Submit",
"more": "More",
+ "no_more": "No more items",
"loading": "Loading…",
"generic_error": "An error occured",
"generic_error_message": "An error occured: {0}",
@@ -105,6 +106,9 @@
"undo": "Undo",
"yes": "Yes",
"no": "No",
+ "none": "None",
+ "not_applicable": "N/A",
+ "not_available": "N/A",
"peek": "Peek",
"scroll_to_top": "Scroll to top",
"role": {
@@ -381,6 +385,9 @@
"selectable_list": {
"select_all": "Select all"
},
+ "page_list": {
+ "load_more": "Load more"
+ },
"settings": {
"invalid_settings_imported": "Error importing settings",
"add_language": "Add fallback language",
@@ -584,6 +591,9 @@
"move_account_error": "Error moving account: {error}",
"discoverable": "Allow discovery of this account in search results and other services",
"domain_mutes": "Domains",
+ "domain_mutes2": "Excluded domains",
+ "user_mutes2": "Muted users",
+ "user_blocks": "Blocked users",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1",
"banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1",
"pad_emoji": "Pad emoji with spaces when adding from picker",
@@ -1156,6 +1166,7 @@
"tabs": {
"nodb": "No DB Config",
"instance": "Instance",
+ "users": "Users",
"limits": "Limits",
"frontends": "Front-ends",
"mailer": "EMails",
@@ -1284,6 +1295,57 @@
"adapter": "Mailing Adapter",
"auth": "Authentication"
},
+ "users": {
+ "title": "Users",
+ "local_id": "Local ID",
+ "godmode": "Show direct messages",
+ "labels": {
+ "query": "Search",
+ "name": "Name",
+ "name_colon": "Name:",
+ "email": "Email",
+ "email_colon": "Email:",
+ "handle_colon": "Handle:",
+ "origin": "Origin",
+ "activity": "Activity",
+ "privileges": "Privileges"
+ },
+ "tags": {
+ "add_new": "Add New Tag",
+ "new_title": "Enter New Tag And Confirm",
+ "yes": "Add",
+ "no": "Abort"
+ },
+ "options": {
+ "all": "All",
+ "only_local": "Only Local",
+ "only_external": "Only External",
+ "only_active": "Only Active",
+ "only_deactivated": "Only Deactivated",
+ "only_admins": "Only Admins",
+ "only_privileged": "Only Privileged",
+ "only_moderators": "Only Moderators",
+ "only_unapproved": "Exclude Approved",
+ "only_unconfirmed": "Exclude Confirmed"
+ },
+ "filters": {
+ "show_direct": "Show Direct Posts",
+ "show_reblogs": "Show Reblogs",
+ "ascending": "Oldest First",
+ "descending": "Newest First"
+ },
+ "indicator": {
+ "admin": "Admin",
+ "moderator": "Moderator",
+ "active": "Active",
+ "deactivated": "Deactivated",
+ "confirmed": "Confirmed",
+ "unconfirmed": "Pending confirmation",
+ "approved": "Approved",
+ "suggested": "Suggested",
+ "unapproved": "Pending approval"
+ }
+ },
"limits": {
"arbitrary_limits": "Arbitrary limits",
"posts": "Post limits",
@@ -1628,7 +1690,10 @@
"invisible_quote": "Quoted status unavailable: {link}",
"more_actions": "More actions on this status",
"loading": "Loading...",
- "load_error": "Unable to load status: {error}"
+ "load_error": "Unable to load status: {error}",
+ "admin_change_scope": "Change visibility",
+ "mark_as_sensitive": "Sensitive",
+ "mark_as_non-sensitive": "Non-sensitive"
},
"user_card": {
"approve": "Approve",
@@ -1716,15 +1781,28 @@
"group": "Group",
"birthday": "Born {birthday}",
"joined": "Joined",
+ "admin_data": {
+ "data": "Administrative info",
+ "registration_reason": "Registration reason",
+ "tags": "Tags"
+ },
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
"revoke_admin": "Revoke Admin",
"grant_moderator": "Grant Moderator",
"revoke_moderator": "Revoke Moderator",
- "activate_account": "Activate account",
- "deactivate_account": "Deactivate account",
- "delete_account": "Delete account",
+ "activate_account": "Activate",
+ "deactivate_account": "Deactivate",
+ "delete_account": "Delete",
+ "suggest_account": "Add to suggested",
+ "remove_suggested_account": "Remove from suggested",
+ "approve_account": "Approve",
+ "confirm_account": "Confirm",
+ "show_statuses": "Show all posts",
+
+ "disable_mfa": "Disable MFA",
+
"force_nsfw": "Mark all posts as NSFW",
"strip_media": "Remove media from posts",
"force_unlisted": "Force posts to be unlisted",
@@ -1732,8 +1810,48 @@
"disable_remote_subscription": "Disallow following user from remote instances",
"disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating",
- "delete_user": "Delete user",
- "delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?"
+
+ "require_password_change": "Require Password Change",
+ "resend_confirmation": "Resend Confirmation Email",
+ "confirm_modal": {
+ "delete_title": "User deletion",
+ "delete_content": "Delete user {user}? | Delete {count} users?",
+ "delete_content_2": "This will permanently delete the data from this accounts and deactivate it. Are you absolutely sure?",
+ "activate_title": "User activation",
+ "activate_content": "Activate user {user}? | Activate {count} users?",
+ "deactivate_content": "Dectivate user {user}? | Dectivate {count} users?",
+ "approval_title": "Approve users",
+ "approval_content": "Approve user {user}? | Approve {count} users?",
+ "confirm_title": "Confirm users",
+ "confirm_content": "Approve user {user}? | Approve {count} users?",
+ "suggest_title": "Suggest users",
+ "add_suggest_content": "Add user {user} to suggested users list? | Add {count} users to suggested users list?",
+ "remove_suggest_content": "Remove user {user} from suggested users list? | Add {count} users to suggested users list?",
+ "rights_title": "Promote users",
+ "grant_rights_content": "Grant user {user} {name} role? | Grant {count} users {name} role?",
+ "revoke_rights_content": "Revoke {name} role from user {user}? | Revoke {name} from {count} users?",
+ "tag_title": "Assign user policy",
+ "assign_tag_content": "Assign {user} a {name} policy? | Assign {name} policy to {count} users?",
+ "unassign_tag_content": "Unassign policy {name} from {user}? | Unassign policy {name} from {count} users?",
+ "resend_confirmation_title": "Email confirmation resend",
+ "resend_confirmation_content": "Resend confirmation email to {count} users?",
+ "disable_mfa_title": "Disable MFA",
+ "disable_mfa_content": "Disable Mult-Factor Authentication for {count} users?",
+ "require_password_change_title": "Force password change",
+ "require_password_change_content": "Force {count} users to change password on next login?",
+ "add": "Add",
+ "remove": "Remove",
+ "delete": "Delete",
+ "activate": "Activate",
+ "deactivate": "Deactivate",
+ "grant": "Grant",
+ "revoke": "Revoke",
+ "approve": "Approve",
+ "confirm": "Confirm",
+ "assign": "Assign",
+ "unassign": "Unassign",
+ "send": "Send"
+ }
},
"highlight_new": {
"disabled": "Don't highlight",
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js
deleted file mode 100644
index 33cc8a595..000000000
--- a/src/modules/adminSettings.js
+++ /dev/null
@@ -1,294 +0,0 @@
-import { cloneDeep, differenceWith, flatten, get, isEqual, set } 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) => {
- f.installedRefs = f.installed_refs
- 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({ rootState, 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 }, { 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(' -> ')))
- }
-
- // we need to preserve tuples on second level only, possibly third
- // but it's not a case right now.
- const convert = (value, preserveTuples, preserveTuplesLv2) => {
- if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
- if (!preserveTuples) {
- return value.reduce((acc, c) => {
- if (c.tuple == null) {
- return {
- ...acc,
- [c]: c,
- }
- }
- return {
- ...acc,
- [c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
- }
- }, {})
- } else {
- return value.map((x) => x.tuple)
- }
- } else {
- if (!preserveTuples) {
- return value
- } else {
- return value.tuple
- }
- }
- }
- // for most stuff we want maps since those are more convenient
- // however this doesn't allow for multiple values per same key
- // so for those cases we want to preserve tuples as-is
- // right now it's made exclusively for :pleroma.:rate_limit
- // so it might not work properly elsewhere
- const preserveTuples = path.find((x) => x === ':rate_limit')
- set(config, path, convert(c.value, false, preserveTuples))
- })
- // patching http adapter config to be easier to handle
- const adapter = config[':pleroma'][':http'][':adapter']
- if (Array.isArray(adapter)) {
- config[':pleroma'][':http'][':adapter'] = {
- [':ssl_options']: {
- [':versions']: [],
- },
- }
- }
- commit('updateAdminSettings', { config, modifiedPaths })
- commit('resetAdminDraft')
- },
- setInstanceAdminDescriptions({ commit }, { 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))
- 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, 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, 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, dispatch }, { path }) {
- const [group, key, subkey] = Array.isArray(path)
- ? path
- : 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/default_config_state.js b/src/modules/default_config_state.js
index e53a9f64b..6a970ef0f 100644
--- a/src/modules/default_config_state.js
+++ b/src/modules/default_config_state.js
@@ -141,7 +141,7 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
default: false,
},
allowForeignUserBackground: {
- description: 'Allow other user\'s profiles to override wallpaper',
+ description: "Allow other user's profiles to override wallpaper",
default: true,
},
hideInstanceWallpaper: {
diff --git a/src/modules/index.js b/src/modules/index.js
index 76bdbf7f3..e42260c06 100644
--- a/src/modules/index.js
+++ b/src/modules/index.js
@@ -1,4 +1,3 @@
-import adminSettings from './adminSettings.js'
import api from './api.js'
import chats from './chats.js'
import drafts from './drafts.js'
@@ -13,7 +12,6 @@ export default {
users,
api,
profileConfig,
- adminSettings,
drafts,
chats,
}
diff --git a/src/modules/users.js b/src/modules/users.js
index db5b16ccc..ec70f8105 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -170,15 +170,11 @@ const unmuteDomain = (store, domain) => {
export const mutations = {
tagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
- const tags = user.tags || []
- const newTags = tags.concat([tag])
- user.tags = newTags
+ user.tags.add(tag)
},
untagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
- const tags = user.tags || []
- const newTags = tags.filter((t) => t !== tag)
- user.tags = newTags
+ user.tags.delete(tag)
},
updateRight(state, { user: { id }, right, value }) {
const user = state.usersObject[id]
@@ -186,9 +182,12 @@ export const mutations = {
newRights[right] = value
user.rights = newRights
},
- updateActivationStatus(state, { user: { id }, deactivated }) {
- const user = state.usersObject[id]
- user.deactivated = deactivated
+ updateUserAdminData(state, { user }) {
+ const { id } = user
+ const localUser = state.usersObject[id]
+ localUser.adminData = user
+ localUser.deactivated = !user.is_active
+ localUser.tags = new Set(user.tags)
},
setCurrentUser(state, user) {
state.lastLoginName = user.screen_name
@@ -369,10 +368,22 @@ const users = {
getters,
actions: {
fetchUserIfMissing(store, id) {
- if (!store.getters.findUser(id)) {
- store.dispatch('fetchUser', id)
+ const user = store.getters.findUser(id)
+ if (!user) {
+ return store.dispatch('fetchUser', id)
+ } else {
+ return Promise.resolve(user)
}
},
+ updateUserAdminData(store, { userAdminData }) {
+ return store
+ .dispatch('fetchUserIfMissing', userAdminData.id)
+ .then((user) => {
+ user.adminData = userAdminData
+ store.commit('addNewUsers', [user])
+ return user
+ })
+ },
fetchUser(store, id) {
return store.rootState.api.backendInteractor
.fetchUser({ id })
@@ -541,15 +552,6 @@ const users = {
commit('updateUserRelationship', [relationship]),
)
},
- toggleActivationStatus({ rootState, commit }, { user }) {
- const api = user.deactivated
- ? rootState.api.backendInteractor.activateUser
- : rootState.api.backendInteractor.deactivateUser
- api({ user }).then((user) => {
- const deactivated = !user.is_active
- commit('updateActivationStatus', { user, deactivated })
- })
- },
registerPushNotifications(store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = useInstanceStore().vapidPublicKey
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 899978132..85fc84ff9 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,4 +1,4 @@
-import { concat, each, get, last, map } from 'lodash'
+import { concat, each, last, map } from 'lodash'
import {
parseAttachment,
@@ -20,12 +20,6 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases'
-const TAG_USER_URL = '/api/pleroma/admin/users/tag'
-const PERMISSION_GROUP_URL = (screenName, right) =>
- `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
-const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
-const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
-const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@@ -121,7 +115,6 @@ const PLEROMA_CHAT_MESSAGES_URL = (id) => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
-const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
@@ -138,6 +131,7 @@ const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
`/api/v1/pleroma/bookmark_folders/${id}`
+const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL =
'/api/v1/pleroma/admin/config/descriptions'
@@ -145,6 +139,65 @@ const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL =
'/api/v1/pleroma/admin/frontends/install'
+const PLEROMA_ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
+const PLEROMA_ADMIN_USERS_URL_SHOW = (nickname) =>
+ `/api/v1/pleroma/admin/users/${nickname}`
+const PLEROMA_ADMIN_USERS_URL_LIST = ({
+ page,
+ pageSize,
+ filters = {},
+ query = '',
+ name = '',
+ email = '',
+}) => {
+ const {
+ local = false,
+ external = false,
+ active = false,
+ needApproval = false,
+ unconfirmed = false,
+ deactivated = false,
+ isAdmin = true,
+ isModerator = true,
+ } = filters
+ const filters_str = [
+ local && 'local',
+ external && 'external',
+ active && 'active',
+ needApproval && 'need_approval',
+ unconfirmed && 'unconfirmed',
+ deactivated && 'deactivated',
+ isAdmin && 'is_admin',
+ isModerator && 'is_moderator',
+ ]
+ .filter((x) => x)
+ .join(',')
+ return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
+}
+const PLEROMA_ADMIN_TAG_USER_URL = '/api/pleroma/admin/users/tag'
+const PLEROMA_ADMIN_PERMISSION_GROUP_URL = (right) =>
+ `/api/pleroma/admin/users/permission_group/${right}`
+const PLEROMA_ADMIN_ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'
+const PLEROMA_ADMIN_DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'
+const PLEROMA_ADMIN_SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'
+const PLEROMA_ADMIN_UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'
+const PLEROMA_ADMIN_APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'
+const PLEROMA_ADMIN_CONFIRM_USERS_URL =
+ '/api/v1/pleroma/admin/users/confirm_email'
+const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL =
+ '/api/v1/pleroma/admin/users/resend_confirmation_email'
+const PLEROMA_ADMIN_LIST_STATUSES_URL = ({
+ id,
+ pageSize,
+ godmode,
+ withReblogs,
+}) =>
+ `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&godmode=${godmode}&with_reblogs=${withReblogs}`
+const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) =>
+ `/api/v1/pleroma/admin/statuses/${id}`
+const PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL =
+ '/api/v1/pleroma/admin/users/force_password_reset'
+const PLEROMA_ADMIN_DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) =>
@@ -205,8 +258,11 @@ const promisedRequest = ({
}
}
return fetch(url, options).then((response) => {
- return new Promise((resolve, reject) =>
- response
+ return new Promise((resolve, reject) => {
+ // 204 is "No content", which fails to parse json (as you'd might think)
+ if (response.ok && response.status === 204) resolve()
+
+ return response
.json()
.then((json) => {
if (!response.ok) {
@@ -230,8 +286,8 @@ const promisedRequest = ({
response,
),
)
- }),
- )
+ })
+ })
})
}
@@ -723,89 +779,117 @@ const fetchStatusHistory = ({ status, credentials }) => {
})
}
-const tagUser = ({ tag, credentials, user }) => {
- const screenName = user.screen_name
- const form = {
- nicknames: [screenName],
- tags: [tag],
- }
-
- const headers = authHeaders(credentials)
- headers['Content-Type'] = 'application/json'
-
- return fetch(TAG_USER_URL, {
- method: 'PUT',
- headers,
- body: JSON.stringify(form),
- })
-}
-
-const untagUser = ({ tag, credentials, user }) => {
- const screenName = user.screen_name
- const body = {
- nicknames: [screenName],
- tags: [tag],
- }
-
- const headers = authHeaders(credentials)
- headers['Content-Type'] = 'application/json'
-
- return fetch(TAG_USER_URL, {
- method: 'DELETE',
- headers,
- body: JSON.stringify(body),
- })
-}
-
-const addRight = ({ right, credentials, user }) => {
- const screenName = user.screen_name
-
- return fetch(PERMISSION_GROUP_URL(screenName, right), {
- method: 'POST',
- headers: authHeaders(credentials),
- body: {},
- })
-}
-
-const deleteRight = ({ right, credentials, user }) => {
- const screenName = user.screen_name
-
- return fetch(PERMISSION_GROUP_URL(screenName, right), {
- method: 'DELETE',
- headers: authHeaders(credentials),
- body: {},
- })
-}
-
-const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
+const adminSetUsersTags = ({
+ tags,
+ credentials,
+ value,
+ screen_names: nicknames,
+}) => {
return promisedRequest({
- url: ACTIVATE_USER_URL,
+ url: PLEROMA_ADMIN_TAG_USER_URL,
+ method: value ? 'PUT' : 'DELETE',
+ credentials,
+ payload: {
+ nicknames,
+ tags,
+ },
+ })
+}
+
+const adminSetUsersRight = ({
+ right,
+ credentials,
+ value,
+ screen_names: nicknames,
+}) => {
+ return promisedRequest({
+ url: PLEROMA_ADMIN_PERMISSION_GROUP_URL(right),
+ method: value ? 'POST' : 'DELETE',
+ credentials,
+ payload: {
+ nicknames,
+ },
+ })
+}
+
+const adminSetUsersActivationStatus = ({
+ credentials,
+ screen_names: nicknames,
+ value,
+}) => {
+ return promisedRequest({
+ url: value
+ ? PLEROMA_ADMIN_ACTIVATE_USERS_URL
+ : PLEROMA_ADMIN_DEACTIVATE_USERS_URL,
method: 'PATCH',
credentials,
payload: {
- nicknames: [nickname],
+ nicknames,
},
- }).then((response) => get(response, 'users.0'))
+ }).then((response) => response.users)
}
-const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
+const adminSetUsersApprovalStatus = ({
+ credentials,
+ screen_names: nicknames,
+}) => {
return promisedRequest({
- url: DEACTIVATE_USER_URL,
+ url: PLEROMA_ADMIN_APPROVE_USERS_URL,
method: 'PATCH',
credentials,
payload: {
- nicknames: [nickname],
+ nicknames,
},
- }).then((response) => get(response, 'users.0'))
+ }).then((response) => response.users)
}
-const deleteUser = ({ credentials, user }) => {
- const screenName = user.screen_name
- const headers = authHeaders(credentials)
+const adminSetUsersConfirmationStatus = ({
+ credentials,
+ screen_names: nicknames,
+}) => {
+ return promisedRequest({
+ url: PLEROMA_ADMIN_CONFIRM_USERS_URL,
+ method: 'PATCH',
+ credentials,
+ payload: {
+ nicknames,
+ },
+ }).then((response) => response.users)
+}
- return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
+const adminSetUsersSuggestionStatus = ({
+ credentials,
+ screen_names: nicknames,
+ value,
+}) => {
+ return promisedRequest({
+ url: value
+ ? PLEROMA_ADMIN_SUGGEST_USERS_URL
+ : PLEROMA_ADMIN_UNSUGGEST_USERS_URL,
+ method: 'PATCH',
+ credentials,
+ payload: {
+ nicknames,
+ },
+ }).then((response) => response.users)
+}
+
+const adminGetUserData = ({ credentials, screen_name: nickname }) => {
+ return promisedRequest({
+ url: PLEROMA_ADMIN_USERS_URL_SHOW(nickname),
+ method: 'GET',
+ credentials,
+ })
+}
+
+const adminDeleteAccounts = ({ credentials, screen_names: nicknames }) => {
+ return promisedRequest({
+ url: PLEROMA_ADMIN_USERS_URL,
method: 'DELETE',
- headers,
+ credentials,
+ payload: {
+ nicknames,
+ },
})
}
@@ -1617,6 +1701,90 @@ const dismissAnnouncement = ({ id, credentials }) => {
})
}
+const adminListUsers = ({ opts, credentials }) => {
+ // the reported list is hardly useful because standards are for dating i guess,
+ // so make sure to fetchIfMissing right afterward using this call
+ const url = PLEROMA_ADMIN_USERS_URL_LIST(opts)
+
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'GET',
+ })
+}
+
+const adminResendConfirmationEmail = ({
+ screen_names: nicknames,
+ credentials,
+}) => {
+ const url = PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'PATCH',
+ payload: {
+ nicknames,
+ },
+ })
+}
+
+const adminRequirePasswordChange = ({
+ screen_names: nicknames,
+ credentials,
+}) => {
+ const url = PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'PATCH',
+ payload: {
+ nicknames,
+ },
+ })
+}
+
+const adminDisableMFA = ({ screen_name: nickname, credentials }) => {
+ const url = PLEROMA_ADMIN_DISABLE_MFA_URL
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'PUT',
+ payload: {
+ nickname,
+ },
+ })
+}
+
+const adminListStatuses = ({ opts, credentials }) => {
+ const url = PLEROMA_ADMIN_LIST_STATUSES_URL(opts)
+
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'GET',
+ })
+}
+
+const adminChangeStatusScope = ({
+ opts: { id, sensitive, visibility },
+ credentials,
+}) => {
+ const url = PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL(id)
+ var payload = {}
+ if (typeof sensitive !== 'undefined') {
+ payload['sensitive'] = sensitive
+ }
+ if (typeof visibility !== 'undefined') {
+ payload['visibility'] = visibility
+ }
+ return promisedRequest({
+ url,
+ credentials,
+ method: 'PUT',
+ payload,
+ })
+}
+
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
const payload = { content }
@@ -2089,7 +2257,6 @@ const listEmojiPacks = ({ page, pageSize }) => {
}
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
- console.log(instance)
if (!instance.startsWith('http')) {
instance = 'https://' + instance
}
@@ -2253,13 +2420,6 @@ const apiService = {
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
- tagUser,
- untagUser,
- deleteUser,
- addRight,
- deleteRight,
- activateUser,
- deactivateUser,
register,
getCaptcha,
updateProfileImages,
@@ -2347,6 +2507,20 @@ const apiService = {
createBookmarkFolder,
updateBookmarkFolder,
deleteBookmarkFolder,
+ adminListUsers,
+ adminGetUserData,
+ adminResendConfirmationEmail,
+ adminDeleteAccounts,
+ adminSetUsersRight,
+ adminSetUsersTags,
+ adminSetUsersApprovalStatus,
+ adminSetUsersConfirmationStatus,
+ adminSetUsersActivationStatus,
+ adminSetUsersSuggestionStatus,
+ adminListStatuses,
+ adminChangeStatusScope,
+ adminRequirePasswordChange,
+ adminDisableMFA,
}
export default apiService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 30ff13c3f..94a5681be 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -15,234 +15,154 @@ import { isStatusNotification } from '../notification_utils/notification_utils.j
* it would be reverted back to []
*/
-const qvitterStatusType = (status) => {
- if (status.is_post_verb) {
- return 'status'
- }
-
- if (status.retweeted_status) {
- return 'retweet'
- }
-
- if (
- (typeof status.uri === 'string' &&
- status.uri.match(/(fave|objectType=Favourite)/)) ||
- (typeof status.text === 'string' && status.text.match(/favorited/))
- ) {
- return 'favorite'
- }
-
- if (
- status.text.match(/deleted notice {{tag/) ||
- status.qvitter_delete_notice
- ) {
- return 'deletion'
- }
-
- if (
- status.text.match(/started following/) ||
- status.activity_type === 'follow'
- ) {
- return 'follow'
- }
-
- return 'unknown'
-}
-
export const parseUser = (data) => {
const output = {}
- const masto = Object.hasOwn(data, 'acct')
+ output._original = data // used for server-side settings
+
// case for users in "mentions" property for statuses in MastoAPI
- const mastoShort = masto && !Object.hasOwn(data, 'avatar')
+ const mastoShort = !Object.hasOwn(data, 'avatar')
output.inLists = null
output.id = String(data.id)
- output._original = data // used for server-side settings
- if (masto) {
- output.screen_name = data.acct
- output.fqn = data.fqn
- output.statusnet_profile_url = data.url
+ output.screen_name = data.acct
+ output.fqn = data.fqn
+ output.statusnet_profile_url = data.url
- if (Object.hasOwn(data, 'mute_expires_at')) {
- output.mute_expires_at =
- data.mute_expires_at == null ? false : data.mute_expires_at
+ if (Object.hasOwn(data, 'mute_expires_at')) {
+ output.mute_expires_at =
+ data.mute_expires_at == null ? false : data.mute_expires_at
+ }
+
+ if (Object.hasOwn(data, 'block_expires_at')) {
+ output.block_expires_at =
+ data.block_expires_at == null ? false : data.block_expires_at
+ }
+
+ // There's nothing else to get
+ if (mastoShort) {
+ return output
+ }
+
+ output.emoji = data.emojis
+ output.name = escapeHtml(data.display_name)
+ output.name_html = output.name
+ output.name_unescaped = data.display_name
+
+ output.description = data.note
+ // TODO cleanup this shit, output.description is overriden with source data
+ output.description_html = data.note
+
+ output.fields = data.fields
+ output.fields_html = data.fields.map((field) => {
+ return {
+ name: escapeHtml(field.name),
+ value: field.value,
+ }
+ })
+ output.fields_text = data.fields.map((field) => {
+ return {
+ name: unescape(field.name.replace(/<[^>]*>/g, '')),
+ value: unescape(field.value.replace(/<[^>]*>/g, '')),
+ }
+ })
+
+ // Utilize avatar_static for gif avatars?
+ output.profile_image_url = data.avatar
+ output.profile_image_url_original = data.avatar
+
+ // Same, utilize header_static?
+ output.cover_photo = data.header
+
+ output.friends_count = data.following_count
+
+ output.bot = data.bot
+
+ output.privileges = []
+
+ if (data.pleroma) {
+ if (data.pleroma.settings_store) {
+ output.storage = data.pleroma.settings_store['pleroma-fe']
+ output.user_highlight = data.pleroma.settings_store['user_highlight']
+ }
+ const relationship = data.pleroma.relationship
+
+ output.background_image = data.pleroma.background_image
+ output.favicon = data.pleroma.favicon
+ output.token = data.pleroma.chat_token
+
+ if (relationship) {
+ output.relationship = relationship
}
- if (Object.hasOwn(data, 'block_expires_at')) {
- output.block_expires_at =
- data.block_expires_at == null ? false : data.block_expires_at
+ output.allow_following_move = data.pleroma.allow_following_move
+
+ output.hide_favorites = data.pleroma.hide_favorites
+ output.hide_follows = data.pleroma.hide_follows
+ output.hide_followers = data.pleroma.hide_followers
+ output.hide_follows_count = data.pleroma.hide_follows_count
+ output.hide_followers_count = data.pleroma.hide_followers_count
+
+ output.rights = {
+ moderator: data.pleroma.is_moderator,
+ admin: data.pleroma.is_admin,
+ }
+ // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
+ if (output.rights.admin) {
+ output.role = 'admin'
+ } else if (output.rights.moderator) {
+ output.role = 'moderator'
+ } else {
+ output.role = 'member'
}
- // There's nothing else to get
- if (mastoShort) {
- return output
- }
+ output.birthday = data.pleroma.birthday
- output.emoji = data.emojis
- output.name = escapeHtml(data.display_name)
- output.name_html = output.name
- output.name_unescaped = data.display_name
-
- output.description = data.note
- // TODO cleanup this shit, output.description is overriden with source data
- output.description_html = data.note
-
- output.fields = data.fields
- output.fields_html = data.fields.map((field) => {
- return {
- name: escapeHtml(field.name),
- value: field.value,
- }
- })
- output.fields_text = data.fields.map((field) => {
- return {
- name: unescape(field.name.replace(/<[^>]*>/g, '')),
- value: unescape(field.value.replace(/<[^>]*>/g, '')),
- }
- })
-
- // Utilize avatar_static for gif avatars?
- output.profile_image_url = data.avatar
- output.profile_image_url_original = data.avatar
-
- // Same, utilize header_static?
- output.cover_photo = data.header
-
- output.friends_count = data.following_count
-
- output.bot = data.bot
-
- output.privileges = []
-
- if (data.pleroma) {
- if (data.pleroma.settings_store) {
- output.storage = data.pleroma.settings_store['pleroma-fe']
- output.user_highlight = data.pleroma.settings_store['user_highlight']
- }
- const relationship = data.pleroma.relationship
-
- output.background_image = data.pleroma.background_image
- output.favicon = data.pleroma.favicon
- output.token = data.pleroma.chat_token
-
- if (relationship) {
- output.relationship = relationship
- }
-
- output.allow_following_move = data.pleroma.allow_following_move
-
- output.hide_favorites = data.pleroma.hide_favorites
- output.hide_follows = data.pleroma.hide_follows
- output.hide_followers = data.pleroma.hide_followers
- output.hide_follows_count = data.pleroma.hide_follows_count
- output.hide_followers_count = data.pleroma.hide_followers_count
-
- output.rights = {
- moderator: data.pleroma.is_moderator,
- admin: data.pleroma.is_admin,
- }
- // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
- if (output.rights.admin) {
- output.role = 'admin'
- } else if (output.rights.moderator) {
- output.role = 'moderator'
- } else {
- output.role = 'member'
- }
-
- output.birthday = data.pleroma.birthday
-
- if (data.pleroma.privileges) {
- output.privileges = data.pleroma.privileges
- } else if (data.pleroma.is_admin) {
- output.privileges = [
- 'users_read',
- 'users_manage_invites',
- 'users_manage_activation_state',
- 'users_manage_tags',
- 'users_manage_credentials',
- 'users_delete',
- 'messages_read',
- 'messages_delete',
- 'instances_delete',
- 'reports_manage_reports',
- 'moderation_log_read',
- 'announcements_manage_announcements',
- 'emoji_manage_emoji',
- 'statistics_read',
- ]
- } else if (data.pleroma.is_moderator) {
- output.privileges = ['messages_delete', 'reports_manage_reports']
- } else {
- output.privileges = []
- }
- }
-
- if (data.source) {
- output.description = data.source.note
- output.default_scope = data.source.privacy
- output.fields = data.source.fields
- if (data.source.pleroma) {
- output.no_rich_text = data.source.pleroma.no_rich_text
- output.show_role = data.source.pleroma.show_role
- output.discoverable = data.source.pleroma.discoverable
- output.show_birthday = data.pleroma.show_birthday
- output.actor_type = data.source.pleroma.actor_type
- }
- }
-
- // TODO: handle is_local
- output.is_local = !output.screen_name.includes('@')
- } else {
- output.screen_name = data.screen_name
-
- output.name = data.name
- output.name_html = data.name_html
-
- output.description = data.description
- output.description_html = data.description_html
-
- output.profile_image_url = data.profile_image_url
- output.profile_image_url_original = data.profile_image_url_original
-
- output.cover_photo = data.cover_photo
-
- output.friends_count = data.friends_count
-
- // output.bot = ??? missing
-
- output.statusnet_profile_url = data.statusnet_profile_url
-
- output.is_local = data.is_local
- output.role = data.role
- output.show_role = data.show_role
-
- if (data.rights) {
- output.rights = {
- moderator: data.rights.delete_others_notice,
- admin: data.rights.admin,
- }
- }
- output.no_rich_text = data.no_rich_text
- output.default_scope = data.default_scope
- output.hide_follows = data.hide_follows
- output.hide_followers = data.hide_followers
- output.hide_follows_count = data.hide_follows_count
- output.hide_followers_count = data.hide_followers_count
- output.background_image = data.background_image
- // Websocket token
- output.token = data.token
-
- // Convert relationsip data to expected format
- output.relationship = {
- muting: data.muted,
- blocking: data.statusnet_blocking,
- followed_by: data.follows_you,
- following: data.following,
+ if (data.pleroma.privileges) {
+ output.privileges = new Set(data.pleroma.privileges)
+ } else if (data.pleroma.is_admin) {
+ output.privileges = new Set([
+ 'users_read',
+ 'users_manage_invites',
+ 'users_manage_activation_state',
+ 'users_manage_tags',
+ 'users_manage_credentials',
+ 'users_delete',
+ 'messages_read',
+ 'messages_delete',
+ 'instances_delete',
+ 'reports_manage_reports',
+ 'moderation_log_read',
+ 'announcements_manage_announcements',
+ 'emoji_manage_emoji',
+ 'statistics_read',
+ ])
+ } else if (data.pleroma.is_moderator) {
+ output.privileges = new Set(['messages_delete', 'reports_manage_reports'])
+ } else {
+ output.privileges = new Set()
}
}
+ if (data.source) {
+ output.description = data.source.note
+ output.default_scope = data.source.privacy
+ output.fields = data.source.fields
+ if (data.source.pleroma) {
+ output.no_rich_text = data.source.pleroma.no_rich_text
+ output.show_role =
+ typeof data.source.pleroma.show_role === 'boolean'
+ ? data.source.pleroma.show_role
+ : true
+ output.discoverable = data.source.pleroma.discoverable
+ output.show_birthday = data.pleroma.show_birthday
+ output.actor_type = data.source.pleroma.actor_type
+ }
+ }
+
+ // TODO: handle is_local
+ output.is_local = !output.screen_name.includes('@')
+
output.created_at = new Date(data.created_at)
output.locked = data.locked
output.last_status_at = new Date(data.last_status_at)
@@ -252,7 +172,7 @@ export const parseUser = (data) => {
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
- output.tags = data.pleroma.tags
+ output.tags = new Set(data.pleroma.tags)
// deactivated was changed to is_active in Pleroma 2.3.0
// so check if is_active is present
@@ -265,7 +185,7 @@ export const parseUser = (data) => {
output.unread_chat_count = data.pleroma.unread_chat_count
}
- output.tags = output.tags || []
+ output.tags = output.tags || new Set()
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
@@ -289,20 +209,15 @@ export const parseUser = (data) => {
export const parseAttachment = (data) => {
const output = {}
- const masto = !Object.hasOwn(data, 'oembed')
- if (masto) {
- // Not exactly same...
- output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
- output.meta = data.meta // not present in BE yet
- output.id = data.id
- } else {
- output.mimetype = data.mimetype
- // output.meta = ??? missing
- }
+ // Not exactly same...
+ output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
+ output.meta = data.meta // not present in BE yet
+ output.id = data.id
if (data.type !== 'unknown') {
- output.type = data.type
+ // treat gifv like it is "video"
+ output.type = data.type === 'gifv' ? 'video' : data.type
} else {
output.type = fileType(output.mimetype)
}
@@ -325,116 +240,76 @@ export const parseSource = (data) => {
export const parseStatus = (data) => {
const output = {}
- const masto = Object.hasOwn(data, 'account')
- if (masto) {
- output.favorited = data.favourited
- output.fave_num = data.favourites_count
+ output.favorited = data.favourited
+ output.fave_num = data.favourites_count
- output.repeated = data.reblogged
- output.repeat_num = data.reblogs_count
+ output.repeated = data.reblogged
+ output.repeat_num = data.reblogs_count
- output.bookmarked = data.bookmarked
+ output.bookmarked = data.bookmarked
- output.type = data.reblog ? 'retweet' : 'status'
- output.nsfw = data.sensitive
+ output.type = data.reblog ? 'retweet' : 'status'
+ output.nsfw = data.sensitive
- output.raw_html = data.content
- output.emojis = data.emojis
+ output.raw_html = data.content
+ output.emojis = data.emojis
- output.tags = data.tags
+ output.tags = data.tags
- output.edited_at = data.edited_at
+ output.edited_at = data.edited_at
- const { pleroma } = data
+ const { pleroma } = data
- if (data.pleroma) {
- output.text = pleroma.content
- ? data.pleroma.content['text/plain']
- : data.content
- output.summary = pleroma.spoiler_text
- ? data.pleroma.spoiler_text['text/plain']
- : data.spoiler_text
- output.statusnet_conversation_id = data.pleroma.conversation_id
- output.is_local = pleroma.local
- output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
- output.thread_muted = pleroma.thread_muted
- output.emoji_reactions = pleroma.emoji_reactions
- output.parent_visible =
- pleroma.parent_visible === undefined ? true : pleroma.parent_visible
- output.quote_visible = pleroma.quote_visible || true
- output.quotes_count = pleroma.quotes_count
- output.bookmark_folder_id = pleroma.bookmark_folder
- } else {
- output.text = data.content
- output.summary = data.spoiler_text
- }
-
- const quoteRaw = pleroma?.quote || data.quote
- const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
- output.quote = quoteData
- output.quote_id =
- data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id
- output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url
-
- output.in_reply_to_status_id = data.in_reply_to_id
- output.in_reply_to_user_id = data.in_reply_to_account_id
- output.replies_count = data.replies_count
-
- if (output.type === 'retweet') {
- output.retweeted_status = parseStatus(data.reblog)
- }
-
- output.summary_raw_html = escapeHtml(data.spoiler_text)
- output.external_url = data.uri || data.url
- output.poll = data.poll
- if (output.poll) {
- output.poll.options = (output.poll.options || []).map((field) => ({
- ...field,
- title_html: escapeHtml(field.title),
- }))
- }
- output.pinned = data.pinned
- output.muted = data.muted
+ if (data.pleroma) {
+ output.text = pleroma.content
+ ? data.pleroma.content['text/plain']
+ : data.content
+ output.summary = pleroma.spoiler_text
+ ? data.pleroma.spoiler_text['text/plain']
+ : data.spoiler_text
+ output.statusnet_conversation_id = data.pleroma.conversation_id
+ output.is_local = pleroma.local
+ output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
+ output.thread_muted = pleroma.thread_muted
+ output.emoji_reactions = pleroma.emoji_reactions
+ output.parent_visible =
+ pleroma.parent_visible === undefined ? true : pleroma.parent_visible
+ output.quote_visible = pleroma.quote_visible || true
+ output.quotes_count = pleroma.quotes_count
+ output.bookmark_folder_id = pleroma.bookmark_folder
} else {
- output.favorited = data.favorited
- output.fave_num = data.fave_num
-
- output.repeated = data.repeated
- output.repeat_num = data.repeat_num
-
- // catchall, temporary
- // Object.assign(output, data)
-
- output.type = qvitterStatusType(data)
-
- if (data.nsfw === undefined) {
- output.nsfw = isNsfw(data)
- if (data.retweeted_status) {
- output.nsfw = data.retweeted_status.nsfw
- }
- } else {
- output.nsfw = data.nsfw
- }
-
- output.raw_html = data.statusnet_html
- output.text = data.text
-
- output.in_reply_to_status_id = data.in_reply_to_status_id
- output.in_reply_to_user_id = data.in_reply_to_user_id
- output.in_reply_to_screen_name = data.in_reply_to_screen_name
- output.statusnet_conversation_id = data.statusnet_conversation_id
-
- if (output.type === 'retweet') {
- output.retweeted_status = parseStatus(data.retweeted_status)
- }
-
- output.summary = data.summary
- output.summary_html = data.summary_html
- output.external_url = data.external_url
- output.is_local = data.is_local
+ output.text = data.content
+ output.summary = data.spoiler_text
}
+ const quoteRaw = pleroma?.quote || data.quote
+ const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
+ output.quote = quoteData
+ output.quote_id =
+ data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id
+ output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url
+
+ output.in_reply_to_status_id = data.in_reply_to_id
+ output.in_reply_to_user_id = data.in_reply_to_account_id
+ output.replies_count = data.replies_count
+
+ if (output.type === 'retweet') {
+ output.retweeted_status = parseStatus(data.reblog)
+ }
+
+ output.summary_raw_html = escapeHtml(data.spoiler_text)
+ output.external_url = data.uri || data.url
+ output.poll = data.poll
+ if (output.poll) {
+ output.poll.options = (output.poll.options || []).map((field) => ({
+ ...field,
+ title_html: escapeHtml(field.title),
+ }))
+ }
+ output.pinned = data.pinned
+ output.muted = data.muted
+
output.id = String(data.id)
output.visibility = data.visibility
output.card = data.card
@@ -448,17 +323,13 @@ export const parseStatus = (data) => {
? String(output.in_reply_to_user_id)
: null
- output.user = parseUser(masto ? data.account : data.user)
+ output.user = parseUser(data.account)
- output.attentions = ((masto ? data.mentions : data.attentions) || []).map(
- parseUser,
- )
+ output.attentions = (data.mentions || []).map(parseUser)
- output.attachments = (
- (masto ? data.media_attachments : data.attachments) || []
- ).map(parseAttachment)
+ output.attachments = (data.media_attachments || []).map(parseAttachment)
- const retweetedStatus = masto ? data.reblog : data.retweeted_status
+ const retweetedStatus = data.reblog
if (retweetedStatus) {
output.retweeted_status = parseStatus(retweetedStatus)
}
@@ -478,42 +349,26 @@ export const parseNotification = (data) => {
favourite: 'like',
reblog: 'repeat',
}
- const masto = !Object.hasOwn(data, 'ntype')
const output = {}
- if (masto) {
- output.type = mastoDict[data.type] || data.type
- output.seen = data.pleroma.is_seen
- // TODO: null check should be a temporary fix, I guess.
- // Investigate why backend does this.
- output.status =
- isStatusNotification(output.type) && data.status !== null
- ? parseStatus(data.status)
- : null
- output.target = output.type !== 'move' ? null : parseUser(data.target)
- output.from_profile = parseUser(data.account)
- output.emoji = data.emoji
- output.emoji_url = data.emoji_url
- if (data.report) {
- output.report = data.report
- output.report.content = data.report.content
- output.report.acct = parseUser(data.report.account)
- output.report.actor = parseUser(data.report.actor)
- output.report.statuses = data.report.statuses.map(parseStatus)
- }
- } else {
- const parsedNotice = parseStatus(data.notice)
- output.type = data.ntype
- output.seen = Boolean(data.is_seen)
- output.status =
- output.type === 'like'
- ? parseStatus(data.notice.favorited_status)
- : parsedNotice
- output.action = parsedNotice
- output.from_profile =
- output.type === 'pleroma:chat_mention'
- ? parseUser(data.account)
- : parseUser(data.from_profile)
+ output.type = mastoDict[data.type] || data.type
+ output.seen = data.pleroma.is_seen
+ // TODO: null check should be a temporary fix, I guess.
+ // Investigate why backend does this.
+ output.status =
+ isStatusNotification(output.type) && data.status !== null
+ ? parseStatus(data.status)
+ : null
+ output.target = output.type !== 'move' ? null : parseUser(data.target)
+ output.from_profile = parseUser(data.account)
+ output.emoji = data.emoji
+ output.emoji_url = data.emoji_url
+ if (data.report) {
+ output.report = data.report
+ output.report.content = data.report.content
+ output.report.acct = parseUser(data.report.account)
+ output.report.actor = parseUser(data.report.actor)
+ output.report.statuses = data.report.statuses.map(parseStatus)
}
output.created_at = new Date(data.created_at)
@@ -522,14 +377,6 @@ export const parseNotification = (data) => {
return output
}
-const isNsfw = (status) => {
- const nsfwRegex = /#nsfw/i
- return (
- (status.tags || []).includes('nsfw') ||
- !!(status.text || '').match(nsfwRegex)
- )
-}
-
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
const flakeId = opts.flakeId
const parsedLinkHeader = parseLinkHeader(linkHeader)
diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js
new file mode 100644
index 000000000..04c89da22
--- /dev/null
+++ b/src/stores/admin_settings.js
@@ -0,0 +1,465 @@
+import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash'
+import { defineStore } from 'pinia'
+
+import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js'
+
+export const defaultState = {
+ frontends: [],
+ loaded: false,
+ needsReboot: null,
+ config: null,
+ modifiedPaths: null,
+ descriptions: null,
+ draft: null,
+ dbConfigEnabled: null,
+}
+
+export const newUserFlags = {
+ ...defaultState.flagStorage,
+}
+
+export const useAdminSettingsStore = defineStore('adminSettings', {
+ state: () => ({
+ ...cloneDeep(defaultState),
+ backendInteractor: window.vuex.state.api.backendInteractor,
+ }),
+ actions: {
+ // Configuration Stuff
+ setInstanceAdminNoDbConfig() {
+ this.loaded = false
+ this.dbConfigEnabled = false
+ },
+ updateAdminSettings({ config, modifiedPaths }) {
+ this.loaded = true
+ this.dbConfigEnabled = true
+ this.config = config
+ this.modifiedPaths = modifiedPaths
+ },
+ updateAdminDescriptions({ descriptions }) {
+ this.descriptions = descriptions
+ },
+ updateAdminDraft({ path, value }) {
+ const [group, key, subkey] = path
+ const parent = [group, key, subkey]
+
+ set(this.draft, path, value)
+
+ // force-updating grouped draft to trigger refresh of group settings
+ if (path.length > parent.length) {
+ set(this.draft, parent, cloneDeep(get(this.draft, parent)))
+ }
+ },
+ resetAdminDraft() {
+ this.draft = cloneDeep(this.config)
+ },
+
+ loadAdminStuff() {
+ this.backendInteractor.fetchInstanceDBConfig().then((backendDbConfig) => {
+ if (backendDbConfig.error) {
+ if (backendDbConfig.error.status === 400) {
+ backendDbConfig.error.json().then((errorJson) => {
+ if (/configurable_from_database/.test(errorJson.error)) {
+ this.setInstanceAdminNoDbConfig()
+ }
+ })
+ }
+ } else {
+ this.setInstanceAdminSettings({ backendDbConfig })
+ }
+ })
+ if (this.descriptions === null) {
+ this.backendInteractor
+ .fetchInstanceConfigDescriptions()
+ .then((backendDescriptions) =>
+ this.setInstanceAdminDescriptions({ backendDescriptions }),
+ )
+ }
+ },
+ setInstanceAdminSettings({ backendDbConfig }) {
+ const config = this.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(' -> ')))
+ }
+
+ // we need to preserve tuples on second level only, possibly third
+ // but it's not a case right now.
+ const convert = (value, preserveTuples, preserveTuplesLv2) => {
+ if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
+ if (!preserveTuples) {
+ return value.reduce((acc, c) => {
+ if (c.tuple == null) {
+ return {
+ ...acc,
+ [c]: c,
+ }
+ }
+ return {
+ ...acc,
+ [c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
+ }
+ }, {})
+ } else {
+ return value.map((x) => x.tuple)
+ }
+ } else {
+ if (!preserveTuples) {
+ return value
+ } else {
+ return value.tuple
+ }
+ }
+ }
+ // for most stuff we want maps since those are more convenient
+ // however this doesn't allow for multiple values per same key
+ // so for those cases we want to preserve tuples as-is
+ // right now it's made exclusively for :pleroma.:rate_limit
+ // so it might not work properly elsewhere
+ const preserveTuples = path.find((x) => x === ':rate_limit')
+ set(config, path, convert(c.value, false, preserveTuples))
+ })
+ // patching http adapter config to be easier to handle
+ const adapter = config[':pleroma'][':http'][':adapter']
+ if (Array.isArray(adapter)) {
+ config[':pleroma'][':http'][':adapter'] = {
+ [':ssl_options']: {
+ [':versions']: [],
+ },
+ }
+ }
+ this.updateAdminSettings({ config, modifiedPaths })
+ this.resetAdminDraft()
+ },
+ setInstanceAdminDescriptions({ 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))
+ this.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() {
+ // 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(this.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(this.config[group][key], this.draft[group][key])
+ })
+
+ // Here we take all changed group-keys and get all changed subkeys
+ const changed = changedGroupKeys.map(({ group, key }) => {
+ const config = this.config[group][key]
+ const draft = this.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)),
+ ),
+ }
+ })
+
+ window.vuex.state.api.backendInteractor
+ .pushInstanceDBConfig({
+ payload: {
+ configs: changed,
+ },
+ })
+ .then(() =>
+ window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
+ )
+ .then((backendDbConfig) =>
+ this.setInstanceAdminSettings({ backendDbConfig }),
+ )
+ },
+ pushAdminSetting({ 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] }))
+ }
+ }
+
+ window.vuex.state.api.backendInteractor
+ .pushInstanceDBConfig({
+ payload: {
+ configs: [
+ {
+ group,
+ key,
+ value: convert(clone),
+ },
+ ],
+ },
+ })
+ .then(() =>
+ window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
+ )
+ .then((backendDbConfig) =>
+ this.setInstanceAdminSettings({ backendDbConfig }),
+ )
+ },
+ resetAdminSetting({ path }) {
+ const [group, key, subkey] = Array.isArray(path)
+ ? path
+ : path.split(/\./g)
+
+ this.modifiedPaths.delete(path)
+
+ return window.vuex.state.api.backendInteractor
+ .pushInstanceDBConfig({
+ payload: {
+ configs: [
+ {
+ group,
+ key,
+ delete: true,
+ subkeys: [subkey],
+ },
+ ],
+ },
+ })
+ .then(() =>
+ window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
+ )
+ .then((backendDbConfig) =>
+ this.setInstanceAdminSettings({ backendDbConfig }),
+ )
+ },
+
+ // Frontends Stuff
+ loadFrontendsStuff() {
+ this.backendInteractor
+ .fetchAvailableFrontends()
+ .then((frontends) => this.setAvailableFrontends({ frontends }))
+ },
+
+ setAvailableFrontends({ frontends }) {
+ this.frontends = frontends.map((f) => {
+ f.installedRefs = f.installed_refs
+ if (f.name === 'pleroma-fe') {
+ f.refs = ['master', 'develop']
+ } else {
+ f.refs = [f.ref]
+ }
+ return f
+ })
+ },
+
+ // Statuses stuff
+ async fetchStatuses(opts) {
+ const { total, activities } =
+ await this.backendInteractor.adminListStatuses({
+ opts,
+ })
+
+ return {
+ items: activities.map(parseStatus),
+ count: total,
+ }
+ },
+ async changeStatusScope(opts) {
+ const raw = await this.backendInteractor.adminChangeStatusScope({
+ opts,
+ })
+ const status = parseStatus(raw)
+
+ await window.vuex.dispatch('addNewStatuses', {
+ statuses: [status],
+ userId: false,
+ })
+ },
+
+ // Users stuff
+ async fetchUsers(opts) {
+ const { users, count } = await this.backendInteractor.adminListUsers({
+ opts,
+ })
+
+ return {
+ items: await Promise.all(
+ users.map(
+ async (userAdminData) =>
+ await window.vuex.dispatch('updateUserAdminData', {
+ userAdminData,
+ }),
+ ),
+ ),
+ count,
+ }
+ },
+ async getUserData({ user }) {
+ const api = this.backendInteractor.adminGetUserData
+ const { screen_name } = user
+
+ const result = await api({ screen_name })
+ window.vuex.commit('updateUserAdminData', { user: result })
+ },
+ async deleteUsers({ users }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminDeleteAccounts
+
+ const resultUserIds = await api({ screen_names })
+
+ resultUserIds.forEach((userId) => {
+ window.vuex.dispatch(
+ 'markStatusesAsDeleted',
+ (status) => userId === status.user.id,
+ )
+ // TODO when migrated to pinia, also remove user
+ })
+
+ return resultUserIds
+ },
+ resendConfirmationEmail({ users }) {
+ const screen_names = users.map((u) => u.screen_name)
+
+ return this.backendInteractor.adminResendConfirmationEmail({
+ screen_names,
+ })
+ },
+ requirePasswordChange({ users }) {
+ const screen_names = users.map((u) => u.screen_name)
+
+ return this.backendInteractor.adminRequirePasswordChange({
+ screen_names,
+ })
+ },
+ // Singular only!
+ disableMFA({ user }) {
+ const { screen_name } = user
+
+ return this.backendInteractor.adminDisableMFA({ screen_name })
+ },
+ async setUsersTags({ users, tags, value }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersTags
+
+ await api({
+ screen_names,
+ tags,
+ value,
+ })
+
+ users.forEach((user) => {
+ this.getUserData({ user })
+ })
+ },
+ async setUsersRight({ users, right, value }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersRight
+
+ await api({
+ screen_names,
+ right,
+ value,
+ })
+
+ users.forEach((user) => {
+ window.vuex.commit('updateRight', { user, right, value })
+ })
+ },
+ async setUsersActivationStatus({ users, value }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersActivationStatus
+
+ const resultUsers = await api({
+ screen_names,
+ value,
+ })
+
+ resultUsers.forEach((user) => {
+ window.vuex.commit('updateUserAdminData', { user })
+ })
+ },
+ async setUsersSuggestionStatus({ users, value }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersSuggestionStatus
+
+ const resultUsers = await api({
+ screen_names,
+ value,
+ })
+
+ resultUsers.forEach((user) => {
+ window.vuex.commit('updateUserAdminData', { user })
+ })
+ },
+ async setUsersConfirmationStatus({ users }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersConfirmationStatus
+
+ await api({ screen_names })
+
+ users.forEach((user) => {
+ this.getUserData({ user })
+ })
+ },
+ async setUsersApprovalStatus({ users }) {
+ const screen_names = users.map((u) => u.screen_name)
+ const api = this.backendInteractor.adminSetUsersApprovalStatus
+
+ const resultUsers = await api({
+ screen_names,
+ })
+
+ resultUsers.forEach((user) => {
+ window.vuex.commit('updateUserAdminData', { user })
+ })
+ },
+ },
+})
diff --git a/src/stores/announcements.js b/src/stores/announcements.js
index bf88666c6..cb325dadd 100644
--- a/src/stores/announcements.js
+++ b/src/stores/announcements.js
@@ -29,7 +29,7 @@ export const useAnnouncementsStore = defineStore('announcements', {
const currentUser = window.vuex.state.users.currentUser
const isAdmin =
currentUser &&
- currentUser.privileges.includes('announcements_manage_announcements')
+ currentUser.privileges.has('announcements_manage_announcements')
const getAnnouncements = async () => {
if (!isAdmin) {
diff --git a/test/e2e-playwright/specs/user_smoke.spec.js b/test/e2e-playwright/specs/user_smoke.spec.js
index a71378c06..205cd3e69 100644
--- a/test/e2e-playwright/specs/user_smoke.spec.js
+++ b/test/e2e-playwright/specs/user_smoke.spec.js
@@ -34,14 +34,9 @@ const logout = async (page) => {
name: 'Logout',
exact: true,
})
- if (await confirmLogout.isVisible()) {
- await Promise.all([
- page.waitForURL(/\/main\/(public|all)/),
- confirmLogout.click(),
- ])
- } else {
- await page.waitForURL(/\/main\/(public|all)/)
- }
+ await expect(confirmLogout).toBeVisible()
+ await confirmLogout.click()
+ await page.waitForURL(/\/main\/(public|all)/)
await expect(page.locator('#sidebar form.login-form')).toBeVisible()
}
diff --git a/test/fixtures/setup_test.js b/test/fixtures/setup_test.js
index f713fa8f0..85a062cc7 100644
--- a/test/fixtures/setup_test.js
+++ b/test/fixtures/setup_test.js
@@ -1,3 +1,4 @@
+import { createTestingPinia } from '@pinia/testing'
import { config } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import VueVirtualScroller from 'vue-virtual-scroller'
@@ -5,9 +6,7 @@ import VueVirtualScroller from 'vue-virtual-scroller'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import Status from 'src/components/status/status.vue'
import StillImage from 'src/components/still-image/still-image.vue'
-
import makeMockStore from './mock_store'
-import { createTestingPinia } from '@pinia/testing'
import routes from 'src/boot/routes'
diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js
index 2f341bb48..6df0ab80d 100644
--- a/test/unit/specs/boot/routes.spec.js
+++ b/test/unit/specs/boot/routes.spec.js
@@ -37,10 +37,9 @@ describe('routes', () => {
const matchedComponents = router.currentRoute.value.matched
- expect(
- matchedComponents[0].components.default.name,
- ).to.eql('AsyncComponentWrapper')
-
+ expect(matchedComponents[0].components.default.name).to.eql(
+ 'AsyncComponentWrapper',
+ )
})
it("user's profile at /users", async () => {
@@ -48,9 +47,9 @@ describe('routes', () => {
const matchedComponents = router.currentRoute.value.matched
- expect(
- matchedComponents[0].components.default.name,
- ).to.eql('AsyncComponentWrapper')
+ expect(matchedComponents[0].components.default.name).to.eql(
+ 'AsyncComponentWrapper',
+ )
})
it('list view', async () => {
@@ -58,9 +57,9 @@ describe('routes', () => {
const matchedComponents = router.currentRoute.value.matched
- expect(
- matchedComponents[0].components.default.name,
- ).to.eql('AsyncComponentWrapper')
+ expect(matchedComponents[0].components.default.name).to.eql(
+ 'AsyncComponentWrapper',
+ )
})
it('list timeline', async () => {
@@ -68,9 +67,9 @@ describe('routes', () => {
const matchedComponents = router.currentRoute.value.matched
- expect(
- matchedComponents[0].components.default.name,
- ).to.eql('AsyncComponentWrapper')
+ expect(matchedComponents[0].components.default.name).to.eql(
+ 'AsyncComponentWrapper',
+ )
})
it('list edit', async () => {
@@ -78,8 +77,8 @@ describe('routes', () => {
const matchedComponents = router.currentRoute.value.matched
- expect(
- matchedComponents[0].components.default.name,
- ).to.eql('AsyncComponentWrapper')
+ expect(matchedComponents[0].components.default.name).to.eql(
+ 'AsyncComponentWrapper',
+ )
})
})
diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js
index 98c0a282e..be2400fc3 100644
--- a/test/unit/specs/components/draft.spec.js
+++ b/test/unit/specs/components/draft.spec.js
@@ -65,7 +65,7 @@ describe('Draft saving', () => {
},
)
- it.only('should auto-save if it is enabled', async function () {
+ it('should auto-save if it is enabled', async function () {
vi.useFakeTimers()
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
|