mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: block action on user pages + shared instance flows
This commit is contained in:
@@ -82,6 +82,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
AffiliateIcon,
|
||||
BadgeCheckIcon,
|
||||
BanIcon,
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
@@ -123,6 +124,14 @@ const messages = defineMessages({
|
||||
id: 'profile.button.billing',
|
||||
defaultMessage: 'Manage user billing',
|
||||
},
|
||||
blockButton: {
|
||||
id: 'profile.button.block',
|
||||
defaultMessage: 'Block',
|
||||
},
|
||||
unblockButton: {
|
||||
id: 'profile.button.unblock',
|
||||
defaultMessage: 'Unblock',
|
||||
},
|
||||
editRoleButton: {
|
||||
id: 'profile.button.edit-role',
|
||||
defaultMessage: 'Edit role',
|
||||
@@ -175,6 +184,7 @@ const props = withDefaults(
|
||||
isAdmin?: boolean
|
||||
isStaff?: boolean
|
||||
showStaffActions?: boolean
|
||||
isBlocked?: boolean
|
||||
projectsCount?: number
|
||||
downloads?: number
|
||||
}>(),
|
||||
@@ -190,6 +200,7 @@ const props = withDefaults(
|
||||
isAdmin: false,
|
||||
isStaff: false,
|
||||
showStaffActions: false,
|
||||
isBlocked: false,
|
||||
projectsCount: 0,
|
||||
downloads: 0,
|
||||
},
|
||||
@@ -198,6 +209,7 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
manageProjects: []
|
||||
report: []
|
||||
block: []
|
||||
copyId: []
|
||||
copyPermalink: []
|
||||
openBilling: []
|
||||
@@ -236,6 +248,14 @@ const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
color: 'red',
|
||||
shown: props.authUser?.id !== props.user.id,
|
||||
},
|
||||
{
|
||||
id: 'block',
|
||||
label: formatMessage(props.isBlocked ? messages.unblockButton : messages.blockButton),
|
||||
icon: BanIcon,
|
||||
action: () => emit('block'),
|
||||
color: 'red',
|
||||
shown: props.authUser?.id !== props.user.id,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
<template>
|
||||
<template v-if="user">
|
||||
<NewModal
|
||||
ref="blockUserModal"
|
||||
:header="formatMessage(messages.blockUserTitle, { username: user.username })"
|
||||
:closable="!isBlockingUser"
|
||||
fade="danger"
|
||||
max-width="500px"
|
||||
>
|
||||
<Admonition
|
||||
type="critical"
|
||||
:header="formatMessage(messages.blockUserAdmonitionTitle)"
|
||||
>
|
||||
{{ formatMessage(messages.blockUserAdmonitionBody, { username: user.username }) }}
|
||||
</Admonition>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button type="button" :disabled="isBlockingUser" @click="blockUserModal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button type="button" :disabled="isBlockingUser" @click="confirmBlockUser">
|
||||
<SpinnerIcon v-if="isBlockingUser" class="animate-spin" />
|
||||
<BanIcon v-else />
|
||||
{{ formatMessage(messages.blockButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
<NewModal
|
||||
v-if="variant === 'web'"
|
||||
ref="editRoleModal"
|
||||
@@ -151,10 +184,12 @@
|
||||
:is-admin="isAdminViewing"
|
||||
:is-staff="isStaffViewing"
|
||||
:show-staff-actions="variant === 'web'"
|
||||
:is-blocked="isBlocked"
|
||||
:projects-count="projects.length"
|
||||
:downloads="sumDownloads"
|
||||
@manage-projects="openPath('/dashboard/projects')"
|
||||
@report="reportProfile"
|
||||
@block="handleBlockAction"
|
||||
@copy-id="copyId"
|
||||
@copy-permalink="copyPermalink"
|
||||
@open-billing="openPath(`/admin/billing/${user.id}`)"
|
||||
@@ -392,6 +427,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
BanIcon,
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
GlobeIcon,
|
||||
@@ -411,6 +447,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -429,7 +466,7 @@ import { defineMessages, useVIntl } from '#ui/composables'
|
||||
import { injectAuth, injectNotificationManager, injectPageContext, injectTags } from '#ui/providers'
|
||||
import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils'
|
||||
|
||||
import { injectUserProfile } from './providers'
|
||||
import { blockedUsersQueryKey, injectUserProfile } from './providers'
|
||||
import {
|
||||
hasActivePride26Midas,
|
||||
hasPride26Badge,
|
||||
@@ -608,6 +645,55 @@ const messages = defineMessages({
|
||||
id: 'profile.role.update-error-description',
|
||||
defaultMessage: 'An error occurred while updating the user role. Please try again.',
|
||||
},
|
||||
blockButton: {
|
||||
id: 'profile.button.block',
|
||||
defaultMessage: 'Block',
|
||||
},
|
||||
unblockUserSuccessTitle: {
|
||||
id: 'profile.unblock-user.success-title',
|
||||
defaultMessage: 'User unblocked',
|
||||
},
|
||||
unblockUserSuccessDescription: {
|
||||
id: 'profile.unblock-user.success-description',
|
||||
defaultMessage: '{username} has been unblocked.',
|
||||
},
|
||||
unblockUserErrorTitle: {
|
||||
id: 'profile.unblock-user.error-title',
|
||||
defaultMessage: 'Failed to unblock user',
|
||||
},
|
||||
unblockUserErrorDescription: {
|
||||
id: 'profile.unblock-user.error-description',
|
||||
defaultMessage: 'An error occurred while unblocking this user. Please try again.',
|
||||
},
|
||||
blockUserTitle: {
|
||||
id: 'profile.block-user.title',
|
||||
defaultMessage: 'Block {username}',
|
||||
},
|
||||
blockUserAdmonitionTitle: {
|
||||
id: 'profile.block-user.admonition-title',
|
||||
defaultMessage: 'Are you sure you want to block this user?',
|
||||
},
|
||||
blockUserAdmonitionBody: {
|
||||
id: 'profile.block-user.admonition-body',
|
||||
defaultMessage:
|
||||
'{username} will not be able to send you friend requests, invite you to shared instances or invite you to Modrinth Hosting servers.',
|
||||
},
|
||||
blockUserSuccessTitle: {
|
||||
id: 'profile.block-user.success-title',
|
||||
defaultMessage: 'User blocked',
|
||||
},
|
||||
blockUserSuccessDescription: {
|
||||
id: 'profile.block-user.success-description',
|
||||
defaultMessage: '{username} has been blocked.',
|
||||
},
|
||||
blockUserErrorTitle: {
|
||||
id: 'profile.block-user.error-title',
|
||||
defaultMessage: 'Failed to block user',
|
||||
},
|
||||
blockUserErrorDescription: {
|
||||
id: 'profile.block-user.error-description',
|
||||
defaultMessage: 'An error occurred while blocking this user. Please try again.',
|
||||
},
|
||||
})
|
||||
|
||||
const userQuery = useQuery({
|
||||
@@ -634,6 +720,12 @@ const collectionsQuery = useQuery({
|
||||
enabled: computed(() => Boolean(props.userId)),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const blockedUsersQuery = useQuery({
|
||||
queryKey: computed(() => blockedUsersQueryKey(auth.user.value?.id)),
|
||||
queryFn: userProfile.getBlockedUsers,
|
||||
enabled: computed(() => Boolean(auth.user.value)),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const user = computed(() => userQuery.data.value)
|
||||
const projects = computed<ResolvedProject[]>(() =>
|
||||
@@ -644,6 +736,9 @@ const projects = computed<ResolvedProject[]>(() =>
|
||||
)
|
||||
const organizations = computed(() => organizationsQuery.data.value ?? [])
|
||||
const collections = computed(() => collectionsQuery.data.value ?? [])
|
||||
const isBlocked = computed(() =>
|
||||
user.value ? (blockedUsersQuery.data.value ?? []).includes(user.value.id) : false,
|
||||
)
|
||||
|
||||
const selectedProjectType = computed(() => {
|
||||
const projectType = props.projectType
|
||||
@@ -826,8 +921,11 @@ async function retryQueries(): Promise<void> {
|
||||
|
||||
const userDetailsModal = ref<ModalRef | null>(null)
|
||||
const editRoleModal = ref<ModalRef | null>(null)
|
||||
const blockUserModal = ref<ModalRef | null>(null)
|
||||
const selectedRole = ref<Labrinth.Users.v3.Role | null>(null)
|
||||
const isSavingRole = ref(false)
|
||||
const isBlockingUser = ref(false)
|
||||
const isUnblockingUser = ref(false)
|
||||
const roleOptions = [
|
||||
{ value: 'developer', label: 'Developer' },
|
||||
{ value: 'moderator', label: 'Moderator' },
|
||||
@@ -851,6 +949,82 @@ function openRoleEditModal(): void {
|
||||
editRoleModal.value?.show()
|
||||
}
|
||||
|
||||
async function handleBlockAction(): Promise<void> {
|
||||
if (!auth.user.value) {
|
||||
await auth.requestSignIn(route.fullPath)
|
||||
return
|
||||
}
|
||||
|
||||
if (isBlocked.value) {
|
||||
await unblockCurrentUser()
|
||||
return
|
||||
}
|
||||
|
||||
blockUserModal.value?.show()
|
||||
}
|
||||
|
||||
async function confirmBlockUser(): Promise<void> {
|
||||
if (!user.value || isBlockingUser.value) return
|
||||
|
||||
const blockedUser = user.value
|
||||
const authUserId = auth.user.value?.id
|
||||
isBlockingUser.value = true
|
||||
try {
|
||||
await userProfile.blockUser(blockedUser.id)
|
||||
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
|
||||
blockedUsersQueryKey(authUserId),
|
||||
(blockedUsers = []) =>
|
||||
blockedUsers.includes(blockedUser.id) ? blockedUsers : [...blockedUsers, blockedUser.id],
|
||||
)
|
||||
blockUserModal.value?.hide()
|
||||
notificationManager.addNotification({
|
||||
type: 'success',
|
||||
title: formatMessage(messages.blockUserSuccessTitle),
|
||||
text: formatMessage(messages.blockUserSuccessDescription, {
|
||||
username: blockedUser.username,
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
notificationManager.addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.blockUserErrorTitle),
|
||||
text: formatMessage(messages.blockUserErrorDescription),
|
||||
})
|
||||
} finally {
|
||||
isBlockingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unblockCurrentUser(): Promise<void> {
|
||||
if (!user.value || isUnblockingUser.value) return
|
||||
|
||||
const blockedUser = user.value
|
||||
const authUserId = auth.user.value?.id
|
||||
isUnblockingUser.value = true
|
||||
try {
|
||||
await userProfile.unblockUser(blockedUser.id)
|
||||
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
|
||||
blockedUsersQueryKey(authUserId),
|
||||
(blockedUsers = []) => blockedUsers.filter((userId) => userId !== blockedUser.id),
|
||||
)
|
||||
notificationManager.addNotification({
|
||||
type: 'success',
|
||||
title: formatMessage(messages.unblockUserSuccessTitle),
|
||||
text: formatMessage(messages.unblockUserSuccessDescription, {
|
||||
username: blockedUser.username,
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
notificationManager.addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.unblockUserErrorTitle),
|
||||
text: formatMessage(messages.unblockUserErrorDescription),
|
||||
})
|
||||
} finally {
|
||||
isUnblockingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRoleEdit(): void {
|
||||
selectedRole.value = user.value?.role ?? null
|
||||
editRoleModal.value?.hide()
|
||||
|
||||
@@ -11,8 +11,14 @@ export interface UserProfileContext {
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
||||
) => Promise<void>
|
||||
getBlockedUsers: () => Promise<Labrinth.BlockedUsers.v3.BlockedUserId[]>
|
||||
blockUser: (userId: string) => Promise<void>
|
||||
unblockUser: (userId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const blockedUsersQueryKey = (userId?: string | null) =>
|
||||
['blocked-users', userId ?? null] as const
|
||||
|
||||
export const [injectUserProfile, provideUserProfile] = createContext<UserProfileContext>(
|
||||
'UserProfilePageLayout',
|
||||
'userProfileContext',
|
||||
|
||||
Reference in New Issue
Block a user