mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
feat: block action on user pages + shared instance flows
This commit is contained in:
+72
-10
@@ -120,8 +120,17 @@
|
|||||||
:max-height="240"
|
:max-height="240"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="reportOnly" class="flex flex-col gap-2">
|
<div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
|
||||||
<Checkbox v-model="deleteInstance" :label="formatMessage(messages.deleteInstance)" />
|
<Checkbox
|
||||||
|
v-if="reportOnly"
|
||||||
|
v-model="deleteInstance"
|
||||||
|
:label="formatMessage(messages.deleteInstance)"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-if="blockTargetUserId"
|
||||||
|
v-model="blockUser"
|
||||||
|
:label="formatMessage(messages.blockUser)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -249,12 +258,14 @@ import {
|
|||||||
Admonition,
|
Admonition,
|
||||||
AutoLink,
|
AutoLink,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
blockedUsersQueryKey,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
type ComboboxOption,
|
type ComboboxOption,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
|
injectAuth,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
IntlFormatted,
|
IntlFormatted,
|
||||||
@@ -266,6 +277,7 @@ import {
|
|||||||
useScrollIndicator,
|
useScrollIndicator,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
@@ -274,6 +286,7 @@ import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
|||||||
import { toError } from '@/helpers/errors'
|
import { toError } from '@/helpers/errors'
|
||||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||||
import { create_report } from '@/helpers/reports'
|
import { create_report } from '@/helpers/reports'
|
||||||
|
import { block_user } from '@/helpers/users'
|
||||||
|
|
||||||
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
||||||
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
||||||
@@ -284,6 +297,7 @@ type ExternalFileRow = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
type SharedInstanceCreator = {
|
type SharedInstanceCreator = {
|
||||||
|
id: string | null
|
||||||
username: string
|
username: string
|
||||||
avatarUrl: string | null
|
avatarUrl: string | null
|
||||||
}
|
}
|
||||||
@@ -300,13 +314,17 @@ type ReportReason = 'malicious' | 'inappropriate' | 'spam'
|
|||||||
const reportReason = ref<ReportReason>('malicious')
|
const reportReason = ref<ReportReason>('malicious')
|
||||||
const additionalContext = ref('')
|
const additionalContext = ref('')
|
||||||
const deleteInstance = ref(true)
|
const deleteInstance = ref(true)
|
||||||
|
const blockUser = ref(true)
|
||||||
|
const blockTargetUserId = ref<string | null>(null)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
const uploadedImageIDs = ref<string[]>([])
|
const uploadedImageIDs = ref<string[]>([])
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reported: [deleteInstance: boolean]
|
reported: [deleteInstance: boolean]
|
||||||
}>()
|
}>()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const auth = injectAuth()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { addNotification, handleError } = injectNotificationManager()
|
const { addNotification, handleError } = injectNotificationManager()
|
||||||
const { load } = useSharedInstancePreviewContent()
|
const { load } = useSharedInstancePreviewContent()
|
||||||
const {
|
const {
|
||||||
@@ -365,13 +383,41 @@ async function submitReport() {
|
|||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const uploadedImages = uploadedImageIDs.value.slice(-10)
|
const uploadedImages = uploadedImageIDs.value.slice(-10)
|
||||||
await create_report({
|
const blockTarget = blockUser.value ? blockTargetUserId.value : null
|
||||||
report_type: reportReason.value,
|
const [reportResult, blockResult] = await Promise.allSettled([
|
||||||
item_type: 'shared-instance',
|
create_report({
|
||||||
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
report_type: reportReason.value,
|
||||||
body,
|
item_type: 'shared-instance',
|
||||||
uploaded_images: uploadedImages,
|
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
||||||
})
|
body,
|
||||||
|
uploaded_images: uploadedImages,
|
||||||
|
}),
|
||||||
|
blockTarget ? block_user(blockTarget) : Promise.resolve(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (blockTarget) {
|
||||||
|
if (blockResult.status === 'fulfilled') {
|
||||||
|
blockUser.value = false
|
||||||
|
const authUserId = auth.user.value?.id
|
||||||
|
if (authUserId) {
|
||||||
|
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
|
||||||
|
blockedUsersQueryKey(authUserId),
|
||||||
|
(blockedUsers = []) =>
|
||||||
|
blockedUsers.includes(blockTarget)
|
||||||
|
? blockedUsers
|
||||||
|
: [...blockedUsers, blockTarget],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: formatMessage(messages.userBlocked),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleError(toError(blockResult.reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportResult.status === 'rejected') throw reportResult.reason
|
||||||
|
|
||||||
const shouldDeleteInstance = reportOnly.value && deleteInstance.value
|
const shouldDeleteInstance = reportOnly.value && deleteInstance.value
|
||||||
hide()
|
hide()
|
||||||
@@ -426,6 +472,8 @@ function resetReportState() {
|
|||||||
reportReason.value = 'malicious'
|
reportReason.value = 'malicious'
|
||||||
additionalContext.value = ''
|
additionalContext.value = ''
|
||||||
deleteInstance.value = true
|
deleteInstance.value = true
|
||||||
|
blockUser.value = true
|
||||||
|
blockTargetUserId.value = null
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
uploadedImageIDs.value = []
|
uploadedImageIDs.value = []
|
||||||
}
|
}
|
||||||
@@ -437,12 +485,18 @@ function show(
|
|||||||
) {
|
) {
|
||||||
resetReportState()
|
resetReportState()
|
||||||
creator.value = creatorValue ?? null
|
creator.value = creatorValue ?? null
|
||||||
|
blockTargetUserId.value = creatorValue?.id ?? null
|
||||||
install.value = installValue
|
install.value = installValue
|
||||||
showPreview(previewValue, event)
|
showPreview(previewValue, event)
|
||||||
}
|
}
|
||||||
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
|
function showReport(
|
||||||
|
previewValue: SharedInstanceInstallPreview,
|
||||||
|
blockTargetUserIdValue?: string | null,
|
||||||
|
event?: MouseEvent,
|
||||||
|
) {
|
||||||
resetReportState()
|
resetReportState()
|
||||||
creator.value = null
|
creator.value = null
|
||||||
|
blockTargetUserId.value = blockTargetUserIdValue ?? null
|
||||||
reportMode.value = true
|
reportMode.value = true
|
||||||
reportOnly.value = true
|
reportOnly.value = true
|
||||||
install.value = () => {}
|
install.value = () => {}
|
||||||
@@ -537,6 +591,14 @@ const messages = defineMessages({
|
|||||||
id: 'app.modal.install-to-play.delete-instance',
|
id: 'app.modal.install-to-play.delete-instance',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Delete instance',
|
||||||
},
|
},
|
||||||
|
blockUser: {
|
||||||
|
id: 'app.modal.install-to-play.block-user',
|
||||||
|
defaultMessage: 'Block user',
|
||||||
|
},
|
||||||
|
userBlocked: {
|
||||||
|
id: 'app.modal.install-to-play.user-blocked',
|
||||||
|
defaultMessage: 'User blocked',
|
||||||
|
},
|
||||||
unknownFilesWarning: {
|
unknownFilesWarning: {
|
||||||
id: 'app.modal.install-to-play.unknown-files-warning',
|
id: 'app.modal.install-to-play.unknown-files-warning',
|
||||||
defaultMessage: 'Unknown files warning',
|
defaultMessage: 'Unknown files warning',
|
||||||
|
|||||||
+3
@@ -32,6 +32,7 @@ type InstallModal = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SharedInstanceCreator = {
|
type SharedInstanceCreator = {
|
||||||
|
id: string | null
|
||||||
username: string
|
username: string
|
||||||
avatarUrl: string | null
|
avatarUrl: string | null
|
||||||
}
|
}
|
||||||
@@ -190,6 +191,7 @@ export function useSharedInstanceInviteHandler(
|
|||||||
},
|
},
|
||||||
invite.invitedByUsername
|
invite.invitedByUsername
|
||||||
? {
|
? {
|
||||||
|
id: invite.invitedById,
|
||||||
username: invite.invitedByUsername,
|
username: invite.invitedByUsername,
|
||||||
avatarUrl: invite.invitedByAvatarUrl,
|
avatarUrl: invite.invitedByAvatarUrl,
|
||||||
}
|
}
|
||||||
@@ -294,6 +296,7 @@ export function useSharedInstanceInviteHandler(
|
|||||||
},
|
},
|
||||||
manager
|
manager
|
||||||
? {
|
? {
|
||||||
|
id: manager.id,
|
||||||
username: manager.username,
|
username: manager.username,
|
||||||
avatarUrl: manager.avatar_url ?? null,
|
avatarUrl: manager.avatar_url ?? null,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ import { computed, watch } from 'vue'
|
|||||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
block_user,
|
||||||
|
get_blocked_users,
|
||||||
get_user_collections,
|
get_user_collections,
|
||||||
get_user_organizations,
|
get_user_organizations,
|
||||||
get_user_profile,
|
get_user_profile,
|
||||||
get_user_projects,
|
get_user_projects,
|
||||||
patch_user,
|
patch_user,
|
||||||
|
unblock_user,
|
||||||
} from '@/helpers/users'
|
} from '@/helpers/users'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ const userProfile = provideUserProfile({
|
|||||||
getOrganizations: get_user_organizations,
|
getOrganizations: get_user_organizations,
|
||||||
getCollections: get_user_collections,
|
getCollections: get_user_collections,
|
||||||
patchUser: patch_user,
|
patchUser: patch_user,
|
||||||
|
getBlockedUsers: get_blocked_users,
|
||||||
|
blockUser: block_user,
|
||||||
|
unblockUser: unblock_user,
|
||||||
})
|
})
|
||||||
|
|
||||||
const userId = computed(() => {
|
const userId = computed(() => {
|
||||||
|
|||||||
@@ -701,7 +701,7 @@ async function reportSharedInstance(event?: MouseEvent, closeUpdateModal = false
|
|||||||
)
|
)
|
||||||
if (instance.value?.id !== reportInstance.id) return
|
if (instance.value?.id !== reportInstance.id) return
|
||||||
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
|
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
|
||||||
sharedInstanceReportModal.value?.showReport(preview, event)
|
sharedInstanceReportModal.value?.showReport(preview, sharedInstance.manager_id, event)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifySharedInstanceError(error)
|
notifySharedInstanceError(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const userProfile = provideUserProfile({
|
|||||||
getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId),
|
getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId),
|
||||||
getCollections: (userId) => client.labrinth.users_v2.getCollections(userId),
|
getCollections: (userId) => client.labrinth.users_v2.getCollections(userId),
|
||||||
patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch),
|
patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch),
|
||||||
|
getBlockedUsers: () => client.labrinth.blocked_users_v3.list(),
|
||||||
|
blockUser: (userId) => client.labrinth.blocked_users_v3.block(userId),
|
||||||
|
unblockUser: (userId) => client.labrinth.blocked_users_v3.unblock(userId),
|
||||||
})
|
})
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
|||||||
import {
|
import {
|
||||||
AffiliateIcon,
|
AffiliateIcon,
|
||||||
BadgeCheckIcon,
|
BadgeCheckIcon,
|
||||||
|
BanIcon,
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
@@ -123,6 +124,14 @@ const messages = defineMessages({
|
|||||||
id: 'profile.button.billing',
|
id: 'profile.button.billing',
|
||||||
defaultMessage: 'Manage user billing',
|
defaultMessage: 'Manage user billing',
|
||||||
},
|
},
|
||||||
|
blockButton: {
|
||||||
|
id: 'profile.button.block',
|
||||||
|
defaultMessage: 'Block',
|
||||||
|
},
|
||||||
|
unblockButton: {
|
||||||
|
id: 'profile.button.unblock',
|
||||||
|
defaultMessage: 'Unblock',
|
||||||
|
},
|
||||||
editRoleButton: {
|
editRoleButton: {
|
||||||
id: 'profile.button.edit-role',
|
id: 'profile.button.edit-role',
|
||||||
defaultMessage: 'Edit role',
|
defaultMessage: 'Edit role',
|
||||||
@@ -175,6 +184,7 @@ const props = withDefaults(
|
|||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
isStaff?: boolean
|
isStaff?: boolean
|
||||||
showStaffActions?: boolean
|
showStaffActions?: boolean
|
||||||
|
isBlocked?: boolean
|
||||||
projectsCount?: number
|
projectsCount?: number
|
||||||
downloads?: number
|
downloads?: number
|
||||||
}>(),
|
}>(),
|
||||||
@@ -190,6 +200,7 @@ const props = withDefaults(
|
|||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isStaff: false,
|
isStaff: false,
|
||||||
showStaffActions: false,
|
showStaffActions: false,
|
||||||
|
isBlocked: false,
|
||||||
projectsCount: 0,
|
projectsCount: 0,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
},
|
},
|
||||||
@@ -198,6 +209,7 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
manageProjects: []
|
manageProjects: []
|
||||||
report: []
|
report: []
|
||||||
|
block: []
|
||||||
copyId: []
|
copyId: []
|
||||||
copyPermalink: []
|
copyPermalink: []
|
||||||
openBilling: []
|
openBilling: []
|
||||||
@@ -236,6 +248,14 @@ const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
|||||||
color: 'red',
|
color: 'red',
|
||||||
shown: props.authUser?.id !== props.user.id,
|
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',
|
id: 'copy-id',
|
||||||
label: formatMessage(commonMessages.copyIdButton),
|
label: formatMessage(commonMessages.copyIdButton),
|
||||||
|
|||||||
@@ -1,5 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="user">
|
<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
|
<NewModal
|
||||||
v-if="variant === 'web'"
|
v-if="variant === 'web'"
|
||||||
ref="editRoleModal"
|
ref="editRoleModal"
|
||||||
@@ -151,10 +184,12 @@
|
|||||||
:is-admin="isAdminViewing"
|
:is-admin="isAdminViewing"
|
||||||
:is-staff="isStaffViewing"
|
:is-staff="isStaffViewing"
|
||||||
:show-staff-actions="variant === 'web'"
|
:show-staff-actions="variant === 'web'"
|
||||||
|
:is-blocked="isBlocked"
|
||||||
:projects-count="projects.length"
|
:projects-count="projects.length"
|
||||||
:downloads="sumDownloads"
|
:downloads="sumDownloads"
|
||||||
@manage-projects="openPath('/dashboard/projects')"
|
@manage-projects="openPath('/dashboard/projects')"
|
||||||
@report="reportProfile"
|
@report="reportProfile"
|
||||||
|
@block="handleBlockAction"
|
||||||
@copy-id="copyId"
|
@copy-id="copyId"
|
||||||
@copy-permalink="copyPermalink"
|
@copy-permalink="copyPermalink"
|
||||||
@open-billing="openPath(`/admin/billing/${user.id}`)"
|
@open-billing="openPath(`/admin/billing/${user.id}`)"
|
||||||
@@ -392,6 +427,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import {
|
import {
|
||||||
|
BanIcon,
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
@@ -411,6 +447,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||||
import Avatar from '#ui/components/base/Avatar.vue'
|
import Avatar from '#ui/components/base/Avatar.vue'
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.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 { injectAuth, injectNotificationManager, injectPageContext, injectTags } from '#ui/providers'
|
||||||
import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils'
|
import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils'
|
||||||
|
|
||||||
import { injectUserProfile } from './providers'
|
import { blockedUsersQueryKey, injectUserProfile } from './providers'
|
||||||
import {
|
import {
|
||||||
hasActivePride26Midas,
|
hasActivePride26Midas,
|
||||||
hasPride26Badge,
|
hasPride26Badge,
|
||||||
@@ -608,6 +645,55 @@ const messages = defineMessages({
|
|||||||
id: 'profile.role.update-error-description',
|
id: 'profile.role.update-error-description',
|
||||||
defaultMessage: 'An error occurred while updating the user role. Please try again.',
|
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({
|
const userQuery = useQuery({
|
||||||
@@ -634,6 +720,12 @@ const collectionsQuery = useQuery({
|
|||||||
enabled: computed(() => Boolean(props.userId)),
|
enabled: computed(() => Boolean(props.userId)),
|
||||||
staleTime: 30_000,
|
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 user = computed(() => userQuery.data.value)
|
||||||
const projects = computed<ResolvedProject[]>(() =>
|
const projects = computed<ResolvedProject[]>(() =>
|
||||||
@@ -644,6 +736,9 @@ const projects = computed<ResolvedProject[]>(() =>
|
|||||||
)
|
)
|
||||||
const organizations = computed(() => organizationsQuery.data.value ?? [])
|
const organizations = computed(() => organizationsQuery.data.value ?? [])
|
||||||
const collections = computed(() => collectionsQuery.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 selectedProjectType = computed(() => {
|
||||||
const projectType = props.projectType
|
const projectType = props.projectType
|
||||||
@@ -826,8 +921,11 @@ async function retryQueries(): Promise<void> {
|
|||||||
|
|
||||||
const userDetailsModal = ref<ModalRef | null>(null)
|
const userDetailsModal = ref<ModalRef | null>(null)
|
||||||
const editRoleModal = 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 selectedRole = ref<Labrinth.Users.v3.Role | null>(null)
|
||||||
const isSavingRole = ref(false)
|
const isSavingRole = ref(false)
|
||||||
|
const isBlockingUser = ref(false)
|
||||||
|
const isUnblockingUser = ref(false)
|
||||||
const roleOptions = [
|
const roleOptions = [
|
||||||
{ value: 'developer', label: 'Developer' },
|
{ value: 'developer', label: 'Developer' },
|
||||||
{ value: 'moderator', label: 'Moderator' },
|
{ value: 'moderator', label: 'Moderator' },
|
||||||
@@ -851,6 +949,82 @@ function openRoleEditModal(): void {
|
|||||||
editRoleModal.value?.show()
|
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 {
|
function cancelRoleEdit(): void {
|
||||||
selectedRole.value = user.value?.role ?? null
|
selectedRole.value = user.value?.role ?? null
|
||||||
editRoleModal.value?.hide()
|
editRoleModal.value?.hide()
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ export interface UserProfileContext {
|
|||||||
userId: string,
|
userId: string,
|
||||||
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
||||||
) => Promise<void>
|
) => 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>(
|
export const [injectUserProfile, provideUserProfile] = createContext<UserProfileContext>(
|
||||||
'UserProfilePageLayout',
|
'UserProfilePageLayout',
|
||||||
'userProfileContext',
|
'userProfileContext',
|
||||||
|
|||||||
Reference in New Issue
Block a user