+
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue
index fa87c9094..cb2436806 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("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
@@ -70,7 +70,7 @@
>
| {{ entry.instance }} |
- {{ $t("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
@@ -97,7 +97,7 @@
>
| {{ entry.instance }} |
- {{ $t("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
@@ -124,7 +124,7 @@
>
| {{ entry.instance }} |
- {{ $t("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
@@ -151,7 +151,7 @@
>
| {{ entry.instance }} |
- {{ $t("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
@@ -178,7 +178,7 @@
>
| {{ entry.instance }} |
- {{ $t("general.not_applicable") }}
+ {{ $t("about.mrf.simple.not_applicable") }}
|
{{ entry.reason }}
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index a9418cffa..83520dc22 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 7dfaa28a5..a166e2196 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -96,11 +96,6 @@
content: "✓";
}
- &.menu-checkbox-indeterminate::after {
- font-size: 1.25em;
- content: "–";
- }
-
&.-radio {
border-radius: 9999px;
@@ -108,11 +103,6 @@
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
new file mode 100644
index 000000000..b82b3d40c
--- /dev/null
+++ b/src/components/selectable_list/selectable_list.js
@@ -0,0 +1,66 @@
+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
new file mode 100644
index 000000000..3d3a5ff04
--- /dev/null
+++ b/src/components/selectable_list/selectable_list.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+ 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
deleted file mode 100644
index 6d9e17b36..000000000
--- a/src/components/settings_modal/admin_tabs/admin_card.js
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 9ba9a2dd4..000000000
--- a/src/components/settings_modal/admin_tabs/admin_card.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.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
deleted file mode 100644
index d0ab596ca..000000000
--- a/src/components/settings_modal/admin_tabs/admin_card.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ $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
deleted file mode 100644
index ff078a709..000000000
--- a/src/components/settings_modal/admin_tabs/admin_status_card.js
+++ /dev/null
@@ -1,95 +0,0 @@
-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
deleted file mode 100644
index f56df92e9..000000000
--- a/src/components/settings_modal/admin_tabs/admin_status_card.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
- {{ $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 7ccae2ca0..627150587 100644
--- a/src/components/settings_modal/admin_tabs/auth_tab.js
+++ b/src/components/settings_modal/admin_tabs/auth_tab.js
@@ -9,8 +9,6 @@ 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 {
@@ -32,7 +30,9 @@ const AuthTab = {
computed: {
...SharedComputedObject(),
LDAPEnabled() {
- return useAdminSettingsStore().draft[':pleroma'][':ldap'][':enabled']
+ return this.$store.state.adminSettings.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 ea7b970fb..b76cd9015 100644
--- a/src/components/settings_modal/admin_tabs/frontends_tab.js
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -7,7 +7,6 @@ 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'
@@ -38,13 +37,13 @@ const FrontendsTab = {
},
created() {
if (this.user.rights.admin) {
- useAdminSettingsStore().loadFrontendsStuff()
+ this.$store.dispatch('loadFrontendsStuff')
}
},
computed: {
...SharedComputedObject(),
frontends() {
- return useAdminSettingsStore().frontends
+ return this.$store.state.adminSettings.frontends
},
},
methods: {
@@ -77,7 +76,7 @@ const FrontendsTab = {
this.working = false
})
.then(async (response) => {
- useAdminSettingsStore().loadFrontendsStuff()
+ this.$store.dispatch('loadFrontendsStuff')
if (response.error) {
const reason = await response.error.json()
useInterfaceStore().pushGlobalNotice({
@@ -105,7 +104,7 @@ const FrontendsTab = {
const ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend
- useAdminSettingsStore.updateAdminDraft({
+ this.$store.commit('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 1e45763f9..ea76ebe6f 100644
--- a/src/components/settings_modal/admin_tabs/http_tab.js
+++ b/src/components/settings_modal/admin_tabs/http_tab.js
@@ -12,8 +12,6 @@ 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 {
@@ -37,7 +35,7 @@ const HTTPTab = {
...SharedComputedObject(),
sslOptions() {
const desc = get(
- useAdminSettingsStore().descriptions,
+ this.$store.state.adminSettings.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 a428a78f9..67d04c303 100644
--- a/src/components/settings_modal/admin_tabs/instance_tab.js
+++ b/src/components/settings_modal/admin_tabs/instance_tab.js
@@ -12,8 +12,6 @@ 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 {
@@ -36,7 +34,7 @@ const InstanceTab = {
computed: {
...SharedComputedObject(),
providersOptions() {
- const desc = get(useAdminSettingsStore().descriptions, [
+ const desc = get(this.$store.state.adminSettings.descriptions, [
':pleroma',
'Pleroma.Web.Metadata',
':providers',
@@ -49,7 +47,7 @@ const InstanceTab = {
)
},
limitLocalContentOptions() {
- const desc = get(useAdminSettingsStore().descriptions, [
+ const desc = get(this.$store.state.adminSettings.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 b4563576b..b3b5f1f2e 100644
--- a/src/components/settings_modal/admin_tabs/links_tab.js
+++ b/src/components/settings_modal/admin_tabs/links_tab.js
@@ -10,8 +10,6 @@ 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 {
@@ -32,27 +30,27 @@ const LinksTab = {
computed: {
classIsPresent() {
return (
- useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
+ this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
':class'
] !== false
)
},
relIsPresent() {
return (
- useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
+ this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
':rel'
] !== false
)
},
truncateIsPresent() {
return (
- useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
+ this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
':truncate'
] !== false
)
},
truncateDescription() {
- return get(useAdminSettingsStore().descriptions, [
+ return get(this.$store.state.adminSettings.descriptions, [
':pleroma',
'Pleroma.Formatter',
':truncate',
@@ -60,7 +58,7 @@ const LinksTab = {
},
ttlSettersOptions() {
const desc = get(
- useAdminSettingsStore().descriptions,
+ this.$store.state.adminSettings.descriptions,
':pleroma.:rich_media.:ttl_setters',
)
return new Set(
@@ -72,7 +70,7 @@ const LinksTab = {
},
parsersOptions() {
const desc = get(
- useAdminSettingsStore().descriptions,
+ this.$store.state.adminSettings.descriptions,
':pleroma.:rich_media.:parsers',
)
return new Set(
@@ -99,12 +97,12 @@ const LinksTab = {
]
},
mediaProxyEnabled() {
- return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
+ return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
':enabled'
]
},
mediaInvalidationProvider() {
- return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
+ return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
':invalidation'
][':provider']
},
@@ -112,19 +110,19 @@ const LinksTab = {
},
methods: {
checkRel(e) {
- useAdminSettingsStore.updateAdminDraft({
+ this.$store.commit('updateAdminDraft', {
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
value: e ? '' : false,
})
},
checkClass(e) {
- useAdminSettingsStore.updateAdminDraft({
+ this.$store.commit('updateAdminDraft', {
path: [':pleroma', 'Pleroma.Formatter', ':class'],
value: e ? '' : false,
})
},
checkTruncate(e) {
- useAdminSettingsStore.updateAdminDraft({
+ this.$store.commit('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 b0750474b..0b909334b 100644
--- a/src/components/settings_modal/admin_tabs/mailer_tab.js
+++ b/src/components/settings_modal/admin_tabs/mailer_tab.js
@@ -7,8 +7,6 @@ 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 {
@@ -28,7 +26,7 @@ const MailerTab = {
computed: {
adaptersLabels() {
const prefix = 'Swoosh.Adapters.'
- const descriptions = useAdminSettingsStore().descriptions
+ const descriptions = this.$store.state.adminSettings.descriptions
const options =
descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
.suggestions
@@ -48,20 +46,20 @@ const MailerTab = {
// ]))
},
adapter() {
- return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
- ':adapter'
- ]
+ return this.$store.state.adminSettings.draft[':pleroma'][
+ 'Pleroma.Emails.Mailer'
+ ][':adapter']
},
mailerEnabled() {
- return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
- ':enabled'
- ]
+ return this.$store.state.adminSettings.draft[':pleroma'][
+ 'Pleroma.Emails.Mailer'
+ ][':enabled']
},
...SharedComputedObject(),
},
methods: {
adapterHasKey(key) {
- const descriptions = useAdminSettingsStore().descriptions
+ const descriptions = this.$store.state.adminSettings.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 9f8a7140e..6c7231312 100644
--- a/src/components/settings_modal/admin_tabs/media_proxy_tab.js
+++ b/src/components/settings_modal/admin_tabs/media_proxy_tab.js
@@ -7,8 +7,6 @@ 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 {
@@ -27,12 +25,12 @@ const MediaProxyTab = {
},
computed: {
mediaProxyEnabled() {
- return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
+ return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
':enabled'
]
},
mediaInvalidationProvider() {
- return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
+ return this.$store.state.adminSettings.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 90be43a17..760206499 100644
--- a/src/components/settings_modal/admin_tabs/uploads_tab.js
+++ b/src/components/settings_modal/admin_tabs/uploads_tab.js
@@ -4,8 +4,6 @@ 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 {
@@ -42,9 +40,9 @@ const UploadsTab = {
},
computed: {
uploader() {
- return useAdminSettingsStore().draft[':pleroma']['Pleroma.Upload'][
- ':uploader'
- ]
+ return this.$store.state.adminSettings.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
deleted file mode 100644
index c12111263..000000000
--- a/src/components/settings_modal/admin_tabs/users_tab.js
+++ /dev/null
@@ -1,133 +0,0 @@
-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
deleted file mode 100644
index 537e43d09..000000000
--- a/src/components/settings_modal/admin_tabs/users_tab.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-.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
deleted file mode 100644
index 1dbd7bcb0..000000000
--- a/src/components/settings_modal/admin_tabs/users_tab.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
- {{ $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 2202d128c..af8ebecbd 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -4,7 +4,6 @@ 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'
@@ -126,14 +125,14 @@ export default {
draft: {
get() {
if (this.realSource === 'admin' || this.path == null) {
- return get(useAdminSettingsStore().draft, this.canonPath)
+ return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
},
set(value) {
if (this.realSource === 'admin' || this.path == null) {
- useAdminSettingsStore.updateAdminDraft({
+ this.$store.commit('updateAdminDraft', {
path: this.canonPath,
value,
})
@@ -165,7 +164,10 @@ export default {
: this.draftMode
},
backendDescription() {
- return get(useAdminSettingsStore().descriptions, this.descriptionPath)
+ return get(
+ this.$store.state.adminSettings.descriptions,
+ this.descriptionPath,
+ )
},
backendDescriptionLabel() {
if (this.realSource !== 'admin') return ''
@@ -219,7 +221,10 @@ export default {
let parentValue = null
if (this.parentPath !== undefined && this.realSource === 'admin') {
if (this.realDraftMode) {
- parentValue = get(useAdminSettingsStore().draft, this.parentPath)
+ parentValue = get(
+ this.$store.state.adminSettings.draft,
+ this.parentPath,
+ )
} else {
parentValue = get(this.configSource, this.parentPath)
}
@@ -238,7 +243,7 @@ export default {
case 'profile':
return this.$store.state.profileConfig
case 'admin':
- return useAdminSettingsStore().config
+ return this.$store.state.adminSettings.config
default:
return useMergedConfigStore().mergedConfig
}
@@ -254,7 +259,7 @@ export default {
this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) =>
- useAdminSettingsStore.pushAdminSetting({ path: k, value: v })
+ this.$store.dispatch('pushAdminSetting', { path: k, value: v })
default:
return (readPath, value) => {
const writePath = `${readPath}`
@@ -367,7 +372,9 @@ export default {
canHardReset() {
return (
this.realSource === 'admin' &&
- useAdminSettingsStore().modifiedPaths?.has(this.canonPath.join(' -> '))
+ this.$store.state.adminSettings.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 4a3b9d4f8..39e0b5b9c 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -1,7 +1,6 @@
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 = () => ({
@@ -9,11 +8,9 @@ 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 4c0e94dff..67da9305b 100644
--- a/src/components/settings_modal/helpers/vertical_tab_switcher.scss
+++ b/src/components/settings_modal/helpers/vertical_tab_switcher.scss
@@ -40,7 +40,6 @@
.tab-slot-wrapper {
flex: 1 1 auto;
- position: relative;
height: 100%;
padding: 0 1em;
overflow-y: auto;
@@ -57,16 +56,10 @@
}
&.-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 34e9cad25..e2df79c26 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -8,7 +8,6 @@ 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'
@@ -233,10 +232,10 @@ const SettingsModal = {
return clone
},
resetAdminDraft() {
- useAdminSettingsStore.resetAdminDraft()
+ this.$store.commit('resetAdminDraft')
},
pushAdminDraft() {
- useAdminSettingsStore.pushAdminDraft()
+ this.$store.dispatch('pushAdminDraft')
},
...mapActions(useInterfaceStore, [
'temporaryChangesRevert',
@@ -266,8 +265,8 @@ const SettingsModal = {
},
adminDraftAny() {
return !isEqual(
- useAdminSettingsStore().config,
- useAdminSettingsStore().draft,
+ this.$store.state.adminSettings.config,
+ this.$store.state.adminSettings.draft,
)
},
},
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index e1405a42f..7d6e17244 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -36,38 +36,6 @@
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 fb2d26778..6cd760833 100644
--- a/src/components/settings_modal/settings_modal_admin_content.js
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -1,4 +1,3 @@
-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'
@@ -16,35 +15,27 @@ 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'
@@ -54,12 +45,6 @@ library.add(
faChain,
faGlobe,
faLaptopCode,
- faPaintBrush,
- faBell,
- faDownload,
- faEyeSlash,
- faInfo,
- faUser,
faTowerBroadcast,
faEnvelope,
faChartLine,
@@ -78,12 +63,11 @@ const SettingsModalAdminContent = {
VerticalTabSwitcher,
InstanceTab,
- UsersTab,
- LimitsTab,
RegistrationsTab,
EmojiTab,
FrontendsTab,
FederationTab,
+ LimitsTab,
MailerTab,
UploadsTab,
MediaProxyTab,
@@ -110,18 +94,18 @@ const SettingsModalAdminContent = {
return useInterfaceStore().settingsModalState === 'visible'
},
adminDbLoaded() {
- return useAdminSettingsStore().loaded
+ return this.$store.state.adminSettings.loaded
},
adminDescriptionsLoaded() {
- return useAdminSettingsStore().descriptions !== null
+ return this.$store.state.adminSettings.descriptions !== null
},
noDb() {
- return useAdminSettingsStore().dbConfigEnabled === false
+ return this.$store.state.adminSettings.dbConfigEnabled === false
},
},
created() {
if (this.user.rights.admin) {
- useAdminSettingsStore().loadAdminStuff()
+ this.$store.dispatch('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 679cdde42..94f843871 100644
--- a/src/components/settings_modal/settings_modal_admin_content.vue
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -49,17 +49,6 @@
-
-
-
-
$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() {
@@ -33,10 +52,12 @@ const MutesAndBlocks = {
},
components: {
TabSwitcher,
- DomainMuteCard,
+ BlockList,
+ MuteList,
+ DomainMuteList,
BlockCard,
- List,
MuteCard,
+ DomainMuteCard,
ProgressButton,
Autosuggest,
Checkbox,
@@ -48,20 +69,8 @@ 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 c07de6be1..5fa3a27b1 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 {
- min-height: 100%;
+ height: 100%;
.usersearch-wrapper {
padding: 1em;
@@ -26,13 +26,4 @@
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 1bc446ffb..ed4b15a49 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -1,12 +1,9 @@
-
+ class="mutes-and-blocks-tab"
+ >
+
-
@@ -57,106 +51,103 @@
{{ $t('settings.no_blocks') }}
-
+
-
-
-
-
-
-
+
+
+
+
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
+
+
-
-
- {{ $t('user_card.unmute') }}
-
- {{ $t('user_card.unmute_progress') }}
-
-
+
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
+
+
+
+
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
+
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
-
-
-
-
-
-
+
+
- {{ $t('domain_mute_card.unmute') }}
-
- {{ $t('domain_mute_card.unmute_progress') }}
+
+
-
+
-
-
- {{ item }}
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
+
+
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0107bde3c..acd25b512 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -262,9 +262,7 @@ 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 e3cdf1fb1..661d1befd 100644
--- a/src/components/status_action_buttons/action_button.js
+++ b/src/components/status_action_buttons/action_button.js
@@ -19,7 +19,6 @@ import {
faChevronDown,
faChevronRight,
faExternalLinkAlt,
- faEye,
faEyeSlash,
faHistory,
faMinus,
@@ -52,7 +51,6 @@ 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 1f4cfc3f6..012844dc8 100644
--- a/src/components/status_action_buttons/action_button_container.js
+++ b/src/components/status_action_buttons/action_button_container.js
@@ -3,30 +3,14 @@ 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,
- faEye,
- faEyeSlash,
- faLock,
- faLockOpen,
- faEnvelope,
-)
+library.add(faUser, faGlobe, faFolderTree)
export default {
components: {
@@ -77,27 +61,8 @@ 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)
},
@@ -114,18 +79,6 @@ 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 919207622..da0beed79 100644
--- a/src/components/status_action_buttons/action_button_container.vue
+++ b/src/components/status_action_buttons/action_button_container.vue
@@ -14,58 +14,6 @@
/>
-
useMergedConfigStore().mergedConfig.modalOnDelete,
@@ -243,26 +243,6 @@ 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 30e0bd903..f788ea30e 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -49,7 +49,6 @@
.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 acb2c4ea1..d987695dd 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -84,12 +84,6 @@ 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,
@@ -286,7 +280,7 @@ export default {
},
},
visibleRole() {
- if (!this.user.show_role && !this.user.adminData) {
+ if (!this.newShowRole) {
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 b724c335b..e676db766 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -22,23 +22,6 @@
}
}
- 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
}
@@ -73,7 +56,7 @@
.user-card-bio {
text-align: center;
- margin: 0.6em;
+ margin: 0 0.6em;
&.input {
margin: 0 1em;
@@ -98,18 +81,15 @@
--_still-image-label-visibility: hidden;
}
- .admin-data,
.personal-marks {
- margin: 0 0.6em;
- padding: 0 0.6em;
+ margin: 0.6em;
+ padding: 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 cdea958af..3eb142954 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 d2d766a7f..d0923d037 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,15 +1,17 @@
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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -17,6 +19,28 @@ 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 = {
@@ -40,8 +64,6 @@ const UserProfile = {
unmounted() {
this.stopFetching()
useInterfaceStore().setForeignProfileBackground(null)
- this.$store.dispatch('clearFollowers', this.userId)
- this.$store.dispatch('clearFriends', this.userId)
},
computed: {
timeline() {
@@ -82,31 +104,11 @@ 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
@@ -201,9 +203,11 @@ const UserProfile = {
components: {
UserCard,
Timeline,
- List,
+ FollowerList,
+ FriendList,
FollowCard,
TabSwitcher,
+ Conversation,
},
}
diff --git a/src/components/user_profile/user_profile.scss b/src/components/user_profile/user_profile.scss
deleted file mode 100644
index 35aa49ca9..000000000
--- a/src/components/user_profile/user_profile.scss
+++ /dev/null
@@ -1,65 +0,0 @@
-.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 6ca0c39bd..72515c90d 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
deleted file mode 100644
index 73f9d83d2..000000000
--- a/src/components/user_profile/user_profile_admin_view.js
+++ /dev/null
@@ -1,69 +0,0 @@
-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
deleted file mode 100644
index 992ce3b4b..000000000
--- a/src/components/user_profile/user_profile_admin_view.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
- {{ $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
new file mode 100644
index 000000000..e839f796f
--- /dev/null
+++ b/src/hocs/with_load_more/with_load_more.jsx
@@ -0,0 +1,119 @@
+// 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
new file mode 100644
index 000000000..6b007ac2e
--- /dev/null
+++ b/src/hocs/with_load_more/with_load_more.scss
@@ -0,0 +1,16 @@
+.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
new file mode 100644
index 000000000..7805161c6
--- /dev/null
+++ b/src/hocs/with_subscription/with_subscription.jsx
@@ -0,0 +1,94 @@
+// 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
new file mode 100644
index 000000000..8c0af336c
--- /dev/null
+++ b/src/hocs/with_subscription/with_subscription.scss
@@ -0,0 +1,10 @@
+.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 e7c03d673..e03e21403 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -86,7 +86,6 @@
"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}",
@@ -106,9 +105,6 @@
"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": {
@@ -385,9 +381,6 @@
"selectable_list": {
"select_all": "Select all"
},
- "page_list": {
- "load_more": "Load more"
- },
"settings": {
"invalid_settings_imported": "Error importing settings",
"add_language": "Add fallback language",
@@ -591,9 +584,6 @@
"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",
@@ -1166,7 +1156,6 @@
"tabs": {
"nodb": "No DB Config",
"instance": "Instance",
- "users": "Users",
"limits": "Limits",
"frontends": "Front-ends",
"mailer": "EMails",
@@ -1295,57 +1284,6 @@
"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",
@@ -1690,10 +1628,7 @@
"invisible_quote": "Quoted status unavailable: {link}",
"more_actions": "More actions on this status",
"loading": "Loading...",
- "load_error": "Unable to load status: {error}",
- "admin_change_scope": "Change visibility",
- "mark_as_sensitive": "Sensitive",
- "mark_as_non-sensitive": "Non-sensitive"
+ "load_error": "Unable to load status: {error}"
},
"user_card": {
"approve": "Approve",
@@ -1781,28 +1716,15 @@
"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",
- "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",
-
+ "activate_account": "Activate account",
+ "deactivate_account": "Deactivate account",
+ "delete_account": "Delete account",
"force_nsfw": "Mark all posts as NSFW",
"strip_media": "Remove media from posts",
"force_unlisted": "Force posts to be unlisted",
@@ -1810,48 +1732,8 @@
"disable_remote_subscription": "Disallow following user from remote instances",
"disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating",
-
- "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"
- }
+ "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?"
},
"highlight_new": {
"disabled": "Don't highlight",
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js
new file mode 100644
index 000000000..33cc8a595
--- /dev/null
+++ b/src/modules/adminSettings.js
@@ -0,0 +1,294 @@
+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 6a970ef0f..e53a9f64b 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 e42260c06..76bdbf7f3 100644
--- a/src/modules/index.js
+++ b/src/modules/index.js
@@ -1,3 +1,4 @@
+import adminSettings from './adminSettings.js'
import api from './api.js'
import chats from './chats.js'
import drafts from './drafts.js'
@@ -12,6 +13,7 @@ export default {
users,
api,
profileConfig,
+ adminSettings,
drafts,
chats,
}
diff --git a/src/modules/users.js b/src/modules/users.js
index ec70f8105..db5b16ccc 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -170,11 +170,15 @@ const unmuteDomain = (store, domain) => {
export const mutations = {
tagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
- user.tags.add(tag)
+ const tags = user.tags || []
+ const newTags = tags.concat([tag])
+ user.tags = newTags
},
untagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
- user.tags.delete(tag)
+ const tags = user.tags || []
+ const newTags = tags.filter((t) => t !== tag)
+ user.tags = newTags
},
updateRight(state, { user: { id }, right, value }) {
const user = state.usersObject[id]
@@ -182,12 +186,9 @@ export const mutations = {
newRights[right] = value
user.rights = newRights
},
- 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)
+ updateActivationStatus(state, { user: { id }, deactivated }) {
+ const user = state.usersObject[id]
+ user.deactivated = deactivated
},
setCurrentUser(state, user) {
state.lastLoginName = user.screen_name
@@ -368,22 +369,10 @@ const users = {
getters,
actions: {
fetchUserIfMissing(store, id) {
- const user = store.getters.findUser(id)
- if (!user) {
- return store.dispatch('fetchUser', id)
- } else {
- return Promise.resolve(user)
+ if (!store.getters.findUser(id)) {
+ store.dispatch('fetchUser', id)
}
},
- 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 })
@@ -552,6 +541,15 @@ 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 85fc84ff9..899978132 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,4 +1,4 @@
-import { concat, each, last, map } from 'lodash'
+import { concat, each, get, last, map } from 'lodash'
import {
parseAttachment,
@@ -20,6 +20,12 @@ 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'
@@ -115,6 +121,7 @@ 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'
@@ -131,7 +138,6 @@ 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'
@@ -139,65 +145,6 @@ 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) =>
@@ -258,11 +205,8 @@ const promisedRequest = ({
}
}
return fetch(url, options).then((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
+ return new Promise((resolve, reject) =>
+ response
.json()
.then((json) => {
if (!response.ok) {
@@ -286,8 +230,8 @@ const promisedRequest = ({
response,
),
)
- })
- })
+ }),
+ )
})
}
@@ -779,117 +723,89 @@ const fetchStatusHistory = ({ status, credentials }) => {
})
}
-const adminSetUsersTags = ({
- tags,
- credentials,
- value,
- screen_names: nicknames,
-}) => {
- return promisedRequest({
- url: PLEROMA_ADMIN_TAG_USER_URL,
- method: value ? 'PUT' : 'DELETE',
- credentials,
- payload: {
- nicknames,
- tags,
- },
+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 adminSetUsersRight = ({
- right,
- credentials,
- value,
- screen_names: nicknames,
-}) => {
- return promisedRequest({
- url: PLEROMA_ADMIN_PERMISSION_GROUP_URL(right),
- method: value ? 'POST' : 'DELETE',
- credentials,
- payload: {
- nicknames,
- },
- })
-}
+const untagUser = ({ tag, credentials, user }) => {
+ const screenName = user.screen_name
+ const body = {
+ nicknames: [screenName],
+ tags: [tag],
+ }
-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,
- },
- }).then((response) => response.users)
-}
+ const headers = authHeaders(credentials)
+ headers['Content-Type'] = 'application/json'
-const adminSetUsersApprovalStatus = ({
- credentials,
- screen_names: nicknames,
-}) => {
- return promisedRequest({
- url: PLEROMA_ADMIN_APPROVE_USERS_URL,
- method: 'PATCH',
- credentials,
- payload: {
- nicknames,
- },
- }).then((response) => response.users)
-}
-
-const adminSetUsersConfirmationStatus = ({
- credentials,
- screen_names: nicknames,
-}) => {
- return promisedRequest({
- url: PLEROMA_ADMIN_CONFIRM_USERS_URL,
- method: 'PATCH',
- credentials,
- payload: {
- nicknames,
- },
- }).then((response) => response.users)
-}
-
-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,
+ 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 } }) => {
+ return promisedRequest({
+ url: ACTIVATE_USER_URL,
+ method: 'PATCH',
credentials,
payload: {
- nicknames,
+ nicknames: [nickname],
},
+ }).then((response) => get(response, 'users.0'))
+}
+
+const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
+ return promisedRequest({
+ url: DEACTIVATE_USER_URL,
+ method: 'PATCH',
+ credentials,
+ payload: {
+ nicknames: [nickname],
+ },
+ }).then((response) => get(response, 'users.0'))
+}
+
+const deleteUser = ({ credentials, user }) => {
+ const screenName = user.screen_name
+ const headers = authHeaders(credentials)
+
+ return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
+ method: 'DELETE',
+ headers,
})
}
@@ -1701,90 +1617,6 @@ 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 }
@@ -2257,6 +2089,7 @@ const listEmojiPacks = ({ page, pageSize }) => {
}
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
+ console.log(instance)
if (!instance.startsWith('http')) {
instance = 'https://' + instance
}
@@ -2420,6 +2253,13 @@ const apiService = {
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
+ tagUser,
+ untagUser,
+ deleteUser,
+ addRight,
+ deleteRight,
+ activateUser,
+ deactivateUser,
register,
getCaptcha,
updateProfileImages,
@@ -2507,20 +2347,6 @@ 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 94a5681be..30ff13c3f 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -15,154 +15,234 @@ 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 = {}
- output._original = data // used for server-side settings
-
+ const masto = Object.hasOwn(data, 'acct')
// case for users in "mentions" property for statuses in MastoAPI
- const mastoShort = !Object.hasOwn(data, 'avatar')
+ const mastoShort = masto && !Object.hasOwn(data, 'avatar')
output.inLists = null
output.id = String(data.id)
+ output._original = data // used for server-side settings
- output.screen_name = data.acct
- output.fqn = data.fqn
- output.statusnet_profile_url = data.url
+ if (masto) {
+ 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, '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, 'mute_expires_at')) {
+ output.mute_expires_at =
+ data.mute_expires_at == null ? false : data.mute_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'
+ if (Object.hasOwn(data, 'block_expires_at')) {
+ output.block_expires_at =
+ data.block_expires_at == null ? false : data.block_expires_at
}
- output.birthday = data.pleroma.birthday
+ // There's nothing else to get
+ if (mastoShort) {
+ return output
+ }
- 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()
+ 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.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)
@@ -172,7 +252,7 @@ export const parseUser = (data) => {
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
- output.tags = new Set(data.pleroma.tags)
+ output.tags = data.pleroma.tags
// deactivated was changed to is_active in Pleroma 2.3.0
// so check if is_active is present
@@ -185,7 +265,7 @@ export const parseUser = (data) => {
output.unread_chat_count = data.pleroma.unread_chat_count
}
- output.tags = output.tags || new Set()
+ output.tags = output.tags || []
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
@@ -209,15 +289,20 @@ export const parseUser = (data) => {
export const parseAttachment = (data) => {
const output = {}
+ const masto = !Object.hasOwn(data, 'oembed')
- // 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 (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
+ }
if (data.type !== 'unknown') {
- // treat gifv like it is "video"
- output.type = data.type === 'gifv' ? 'video' : data.type
+ output.type = data.type
} else {
output.type = fileType(output.mimetype)
}
@@ -240,76 +325,116 @@ export const parseSource = (data) => {
export const parseStatus = (data) => {
const output = {}
+ const masto = Object.hasOwn(data, 'account')
- output.favorited = data.favourited
- output.fave_num = data.favourites_count
+ if (masto) {
+ 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
+ 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
} else {
- output.text = data.content
- output.summary = data.spoiler_text
+ 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
}
- 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
@@ -323,13 +448,17 @@ export const parseStatus = (data) => {
? String(output.in_reply_to_user_id)
: null
- output.user = parseUser(data.account)
+ output.user = parseUser(masto ? data.account : data.user)
- output.attentions = (data.mentions || []).map(parseUser)
+ output.attentions = ((masto ? data.mentions : data.attentions) || []).map(
+ parseUser,
+ )
- output.attachments = (data.media_attachments || []).map(parseAttachment)
+ output.attachments = (
+ (masto ? data.media_attachments : data.attachments) || []
+ ).map(parseAttachment)
- const retweetedStatus = data.reblog
+ const retweetedStatus = masto ? data.reblog : data.retweeted_status
if (retweetedStatus) {
output.retweeted_status = parseStatus(retweetedStatus)
}
@@ -349,26 +478,42 @@ export const parseNotification = (data) => {
favourite: 'like',
reblog: 'repeat',
}
+ const masto = !Object.hasOwn(data, 'ntype')
const output = {}
- 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)
+ 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.created_at = new Date(data.created_at)
@@ -377,6 +522,14 @@ 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
deleted file mode 100644
index 04c89da22..000000000
--- a/src/stores/admin_settings.js
+++ /dev/null
@@ -1,465 +0,0 @@
-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 cb325dadd..bf88666c6 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.has('announcements_manage_announcements')
+ currentUser.privileges.includes('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 205cd3e69..a71378c06 100644
--- a/test/e2e-playwright/specs/user_smoke.spec.js
+++ b/test/e2e-playwright/specs/user_smoke.spec.js
@@ -34,9 +34,14 @@ const logout = async (page) => {
name: 'Logout',
exact: true,
})
- await expect(confirmLogout).toBeVisible()
- await confirmLogout.click()
- await page.waitForURL(/\/main\/(public|all)/)
+ if (await confirmLogout.isVisible()) {
+ await Promise.all([
+ page.waitForURL(/\/main\/(public|all)/),
+ confirmLogout.click(),
+ ])
+ } else {
+ 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 85a062cc7..f713fa8f0 100644
--- a/test/fixtures/setup_test.js
+++ b/test/fixtures/setup_test.js
@@ -1,4 +1,3 @@
-import { createTestingPinia } from '@pinia/testing'
import { config } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import VueVirtualScroller from 'vue-virtual-scroller'
@@ -6,7 +5,9 @@ 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 6df0ab80d..2f341bb48 100644
--- a/test/unit/specs/boot/routes.spec.js
+++ b/test/unit/specs/boot/routes.spec.js
@@ -37,9 +37,10 @@ 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 () => {
@@ -47,9 +48,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 () => {
@@ -57,9 +58,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 () => {
@@ -67,9 +68,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 () => {
@@ -77,8 +78,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 be2400fc3..98c0a282e 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('should auto-save if it is enabled', async function () {
+ it.only('should auto-save if it is enabled', async function () {
vi.useFakeTimers()
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
|