diff --git a/apps/frontend/src/composables/creator-project-stats.ts b/apps/frontend/src/composables/creator-project-stats.ts
index c6e3f2b93de..2a62ced3a74 100644
--- a/apps/frontend/src/composables/creator-project-stats.ts
+++ b/apps/frontend/src/composables/creator-project-stats.ts
@@ -1,9 +1,19 @@
import type { AbstractModrinthClient } from '@modrinth/api-client'
-import { injectModrinthClient } from '@modrinth/ui'
+import { getRemovedScopes, injectModrinthClient, useVIntl } from '@modrinth/ui'
import { useQueries, useQuery } from '@tanstack/vue-query'
import { computed, type Ref } from 'vue'
export const creatorProjectStatsQueryOptions = {
+ memberAccount: (id: string, client: AbstractModrinthClient) => ({
+ queryKey: ['user', id, 'moderation-account'] as const,
+ queryFn: () => client.labrinth.users_v3.get(id),
+ staleTime: 60_000,
+ }),
+ memberBlacklist: (id: string, client: AbstractModrinthClient) => ({
+ queryKey: ['shared-instance-blacklist', 'v1', id] as const,
+ queryFn: () => client.sharedinstances.users_v1.getBlacklistStatus(id),
+ staleTime: 60_000,
+ }),
organizationProjects: (id: string, client: AbstractModrinthClient) => ({
queryKey: ['organization', id, 'projects'] as const,
queryFn: () => client.labrinth.organizations_v3.getProjects(id),
@@ -32,6 +42,7 @@ export function useCreatorProjectStats(
enabled: Ref
= computed(() => true),
) {
const client = injectModrinthClient()
+ const { formatMessage } = useVIntl()
const memberIds = computed(() => (enabled.value ? [...new Set(userIds.value)].toSorted() : []))
const organizationProjects = useQuery(
computed(() => ({
@@ -68,10 +79,28 @@ export function useCreatorProjectStats(
})),
),
})
+ const memberAccounts = useQueries({
+ queries: computed(() =>
+ memberIds.value.map((id) => ({
+ ...creatorProjectStatsQueryOptions.memberAccount(id, client),
+ enabled: enabled.value,
+ })),
+ ),
+ })
+ const memberBlacklists = useQueries({
+ queries: computed(() =>
+ memberIds.value.map((id) => ({
+ ...creatorProjectStatsQueryOptions.memberBlacklist(id, client),
+ enabled: enabled.value,
+ })),
+ ),
+ })
const memberStats = computed(() =>
Object.fromEntries(
memberIds.value.map((userId, index) => {
const projects = memberProjects.value[index]?.data
+ const account = memberAccounts.value[index]?.data
+ const restriction = account?.restriction
return [
userId,
[
@@ -85,6 +114,28 @@ export function useCreatorProjectStats(
status: 'tech_review_failed',
count: memberFlaggedProjects.data.value?.[userId]?.length ?? 0,
},
+ {
+ status: 'shared_instances_banned',
+ count: memberBlacklists.value[index]?.data?.blacklisted ? 1 : 0,
+ },
+ {
+ status: 'account_locked',
+ count: account?.lock ? 1 : 0,
+ details: account?.lock?.reason,
+ },
+ {
+ status: 'account_restricted',
+ count: restriction?.removed_perms ? 1 : 0,
+ details: restriction
+ ? [
+ ...getRemovedScopes(restriction.removed_perms).map((scope) =>
+ formatMessage(scope.label),
+ ),
+ restriction.reason,
+ restriction.private_reason,
+ ].filter((detail): detail is string => !!detail)
+ : undefined,
+ },
],
]
}),
diff --git a/apps/frontend/src/pages/[type]/[project].vue b/apps/frontend/src/pages/[type]/[project].vue
index 0a4e7e42a8d..c31839a4ed5 100644
--- a/apps/frontend/src/pages/[type]/[project].vue
+++ b/apps/frontend/src/pages/[type]/[project].vue
@@ -511,7 +511,7 @@
:user-link="(username) => `/user/${username}`"
class="card flex-card"
>
-
+
-
+
{
return owner ? [owner, ...rest] : rest
})
+const showCreatorStats = computed(
+ () =>
+ isStaff(auth.value.user) &&
+ modSettings.value.get(moderationSettings.General.ShowCreatorStatsOnPublicPages),
+)
const creatorStats = useCreatorProjectStats(
computed(() => members.value.map((member) => member.user.id)),
computed(() => organization.value?.id ?? ''),
- computed(() => isStaff(auth.value.user)),
+ showCreatorStats,
)
const isMember = computed(
diff --git a/apps/frontend/src/pages/settings.vue b/apps/frontend/src/pages/settings.vue
index ef69d5f312b..7897f4d7e58 100644
--- a/apps/frontend/src/pages/settings.vue
+++ b/apps/frontend/src/pages/settings.vue
@@ -86,12 +86,19 @@
icon: ServerIcon,
}
: null,
- isStaff(auth.user) ? { type: 'heading', label: 'Staff' } : null,
+ isStaff(auth.user) ? { type: 'heading', label: 'Moderation' } : null,
isStaff(auth.user)
? {
link: '/settings/moderation',
- label: 'Moderation',
- icon: ScaleIcon,
+ label: 'Settings',
+ icon: SettingsIcon,
+ }
+ : null,
+ isStaff(auth.user)
+ ? {
+ link: '/settings/moderation/keybinds',
+ label: 'Keybinds',
+ icon: KeyboardIcon,
}
: null,
].filter(Boolean)
@@ -107,12 +114,13 @@ import {
CardIcon,
GridIcon,
HeartHandshakeIcon,
+ KeyboardIcon,
KeyIcon,
LanguagesIcon,
MonitorSmartphoneIcon,
PaintbrushIcon,
- ScaleIcon,
ServerIcon,
+ SettingsIcon,
ShieldIcon,
ToggleRightIcon,
UserIcon,
diff --git a/apps/frontend/src/pages/settings/moderation/index.vue b/apps/frontend/src/pages/settings/moderation/index.vue
index 9570a7c1513..0453a89c53d 100644
--- a/apps/frontend/src/pages/settings/moderation/index.vue
+++ b/apps/frontend/src/pages/settings/moderation/index.vue
@@ -1,6 +1,5 @@
-
-
-
+
+
+
+
+
diff --git a/apps/frontend/src/pages/settings/moderation/keybinds.vue b/apps/frontend/src/pages/settings/moderation/keybinds.vue
new file mode 100644
index 00000000000..a0c650a4bf9
--- /dev/null
+++ b/apps/frontend/src/pages/settings/moderation/keybinds.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/apps/frontend/src/pages/user/[user].vue b/apps/frontend/src/pages/user/[user].vue
index 0dc0df06d69..9bba8a6ee04 100644
--- a/apps/frontend/src/pages/user/[user].vue
+++ b/apps/frontend/src/pages/user/[user].vue
@@ -10,6 +10,9 @@
:on-create-project="openProjectCreateModal"
:on-create-collection="openCollectionCreateModal"
>
+
+
+
@@ -18,12 +21,16 @@