mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: blocking frontend (#6911)
* feat: blocking api interfaces * feat: block action on user pages + shared instance flows * feat: safety settings subpage * feat: interaction source settings * feat: finish social settings * feat: profile settings xplat * fix: prepr * feat: click on friends * default instance -> game options & fix feature flag width * fix: scroll indicators --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -146,6 +146,7 @@ import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { AppNotificationManager } from './providers/app-notifications'
|
||||
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
|
||||
import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
@@ -795,6 +796,8 @@ const sharedInstanceInviteHandler = ref()
|
||||
const updateToPlayModal = ref()
|
||||
|
||||
const modrinthLoginModal = ref()
|
||||
const appSettingsModal = ref()
|
||||
provide(appSettingsModalOpenProfileKey, () => appSettingsModal.value?.showProfile())
|
||||
|
||||
watch(incompatibilityWarningModal, (modal) => {
|
||||
if (modal) {
|
||||
@@ -1500,7 +1503,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
</Transition>
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
<AppSettingsModal ref="appSettingsModal" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<ModrinthAccountRequiredModal ref="modrinthLoginModal" :request-auth="requestModrinthAuth" />
|
||||
@@ -1568,7 +1571,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton
|
||||
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="() => $refs.settingsModal.show()"
|
||||
:to="() => appSettingsModal?.show()"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
|
||||
@@ -123,45 +123,46 @@ const messages = defineMessages({
|
||||
<div
|
||||
v-for="friend in friends"
|
||||
:key="friend.username"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
class="group grid items-center grid-cols-[1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar
|
||||
:src="friend.avatar"
|
||||
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||
class="w-12 h-12 rounded-full"
|
||||
size="32px"
|
||||
circle
|
||||
/>
|
||||
<span
|
||||
v-if="friend.online"
|
||||
aria-hidden="true"
|
||||
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm m-0"
|
||||
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ friend.username }}
|
||||
</span>
|
||||
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||
{{ formatMessage(messages.friendRequestSent) }}
|
||||
</span>
|
||||
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
<RouterLink
|
||||
:to="`/user/${encodeURIComponent(friend.username)}`"
|
||||
class="grid min-w-0 grid-cols-[auto_1fr] items-center gap-2 text-inherit no-underline"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar
|
||||
:src="friend.avatar"
|
||||
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||
class="w-12 h-12 rounded-full"
|
||||
size="32px"
|
||||
circle
|
||||
/>
|
||||
<span
|
||||
v-if="friend.online"
|
||||
aria-hidden="true"
|
||||
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm m-0"
|
||||
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ friend.username }}
|
||||
</span>
|
||||
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||
{{ formatMessage(messages.friendRequestSent) }}
|
||||
</span>
|
||||
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<ButtonStyled v-if="friend.accepted" circular type="transparent">
|
||||
<OverflowMenu
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:options="[
|
||||
{
|
||||
id: 'view-profile',
|
||||
action: () => openProfile(friend.username),
|
||||
},
|
||||
{
|
||||
id: 'remove-friend',
|
||||
action: () => removeFriend(friend),
|
||||
@@ -170,10 +171,6 @@ const messages = defineMessages({
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #view-profile>
|
||||
<UserIcon />
|
||||
{{ formatMessage(messages.viewProfile) }}
|
||||
</template>
|
||||
<template #remove-friend>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.removeFriend) }}
|
||||
|
||||
@@ -3,12 +3,14 @@ import {
|
||||
CoffeeIcon,
|
||||
GameIcon,
|
||||
GaugeIcon,
|
||||
HeartHandshakeIcon,
|
||||
LanguagesIcon,
|
||||
ModrinthIcon,
|
||||
PaintbrushIcon,
|
||||
Settings2Icon,
|
||||
ShieldIcon,
|
||||
ToggleRightIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
@@ -17,13 +19,16 @@ import {
|
||||
defineMessages,
|
||||
ProgressBar,
|
||||
TabbedModal,
|
||||
UnsavedChangesPopup,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, provide, ref, watch } from 'vue'
|
||||
|
||||
import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
|
||||
import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue'
|
||||
import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue'
|
||||
import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
|
||||
import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
|
||||
@@ -32,6 +37,10 @@ import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultI
|
||||
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import {
|
||||
appSettingsModalContextKey,
|
||||
type UnsavedChangesController,
|
||||
} from '@/providers/app-settings-modal'
|
||||
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
@@ -98,6 +107,18 @@ const tabs = [
|
||||
content: FeatureFlagSettings,
|
||||
developerOnly: true,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.profile,
|
||||
category: tabCategories.account,
|
||||
icon: UserIcon,
|
||||
content: ProfileSettings,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.social,
|
||||
category: tabCategories.account,
|
||||
icon: HeartHandshakeIcon,
|
||||
content: SocialSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.privacy',
|
||||
@@ -110,7 +131,7 @@ const tabs = [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.default-instance-options',
|
||||
defaultMessage: 'Default instance options',
|
||||
defaultMessage: 'Default game options',
|
||||
}),
|
||||
category: tabCategories.instances,
|
||||
icon: GameIcon,
|
||||
@@ -139,12 +160,58 @@ const tabs = [
|
||||
const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
|
||||
|
||||
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
|
||||
const unsavedChangesController = ref<UnsavedChangesController | null>(null)
|
||||
const emptyUnsavedChangesState: Record<string, unknown> = {}
|
||||
const originalUnsavedChangesState = computed(
|
||||
() => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState,
|
||||
)
|
||||
const modifiedUnsavedChangesState = computed(
|
||||
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
|
||||
)
|
||||
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
|
||||
const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false)
|
||||
|
||||
function canLeaveCurrentTab(): boolean {
|
||||
if (!unsavedChangesController.value?.hasChanges()) return true
|
||||
unsavedChangesPopup.value?.nudge()
|
||||
return false
|
||||
}
|
||||
|
||||
function close(): boolean {
|
||||
return modal.value?.hide() ?? false
|
||||
}
|
||||
|
||||
function registerUnsavedChangesController(controller: UnsavedChangesController | null): void {
|
||||
unsavedChangesController.value = controller
|
||||
}
|
||||
|
||||
provide(appSettingsModalContextKey, {
|
||||
close,
|
||||
registerUnsavedChangesController,
|
||||
})
|
||||
|
||||
function resetUnsavedChanges(): void {
|
||||
unsavedChangesController.value?.reset()
|
||||
}
|
||||
|
||||
function saveUnsavedChanges(): void {
|
||||
void unsavedChangesController.value?.save()
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
function showProfile(): void {
|
||||
const profileTabIndex = availableTabs.value.findIndex((tab) => tab.content === ProfileSettings)
|
||||
if (profileTabIndex >= 0) {
|
||||
modal.value?.setTab(profileTabIndex)
|
||||
}
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({ show, showProfile })
|
||||
|
||||
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
||||
|
||||
@@ -197,12 +264,30 @@ const messages = defineMessages({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<TabbedModal ref="modal" :tabs="availableTabs" :width="'min(928px, calc(95vw - 10rem))'">
|
||||
<TabbedModal
|
||||
ref="modal"
|
||||
:tabs="availableTabs"
|
||||
:width="'min(928px, calc(95vw - 10rem))'"
|
||||
:before-hide="canLeaveCurrentTab"
|
||||
:before-tab-change="canLeaveCurrentTab"
|
||||
:floating-action-bar-shown="hasUnsavedChanges"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(commonMessages.settingsLabel) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #floating-action-bar>
|
||||
<UnsavedChangesPopup
|
||||
ref="unsavedChangesPopup"
|
||||
:original="originalUnsavedChangesState"
|
||||
:modified="modifiedUnsavedChangesState"
|
||||
:saving="savingUnsavedChanges"
|
||||
inline
|
||||
@reset="resetUnsavedChanges"
|
||||
@save="saveUnsavedChanges"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-auto text-secondary text-sm">
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<AccountProfileSettings
|
||||
ref="profileSettings"
|
||||
:patch-user="patchUser"
|
||||
:change-avatar="changeAvatar"
|
||||
:delete-avatar="deleteAvatar"
|
||||
:get-authenticated-user="getAuthenticatedUser"
|
||||
@profile-link-click="handleProfileLinkClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountProfileSettings, injectAuth } from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
change_user_avatar,
|
||||
delete_user_avatar,
|
||||
get_user_profile,
|
||||
patch_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const auth = injectAuth()
|
||||
const profileSettings = ref<InstanceType<typeof AccountProfileSettings> | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => profileSettings.value?.hasChanges ?? false,
|
||||
getOriginal: () => profileSettings.value?.originalState ?? {},
|
||||
getModified: () => profileSettings.value?.modifiedState ?? {},
|
||||
isSaving: () => profileSettings.value?.saving ?? false,
|
||||
reset: () => profileSettings.value?.reset(),
|
||||
save: () => profileSettings.value?.save(),
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
function handleProfileLinkClick(event: MouseEvent): void {
|
||||
if (settingsModal && !settingsModal.close()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function patchUser(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'bio' | 'username'>>,
|
||||
): Promise<void> {
|
||||
return patch_user(userId, patch)
|
||||
}
|
||||
|
||||
async function changeAvatar(userId: string, file: Blob, extension: string): Promise<void> {
|
||||
await change_user_avatar(userId, new Uint8Array(await file.arrayBuffer()), extension)
|
||||
}
|
||||
|
||||
function deleteAvatar(userId: string): Promise<void> {
|
||||
return delete_user_avatar(userId)
|
||||
}
|
||||
|
||||
function getAuthenticatedUser(): Promise<Labrinth.Users.v3.User> {
|
||||
const userId = auth.user.value?.id
|
||||
if (!userId) throw new Error('Cannot refresh a signed-out user.')
|
||||
return get_user_profile(userId)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<AccountSocialSettings
|
||||
:get-blocked-users="get_blocked_users"
|
||||
:get-users="getUsers"
|
||||
:unblock-user="unblock_user"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountSocialSettings, injectModrinthClient } from '@modrinth/ui'
|
||||
|
||||
import { get_blocked_users, unblock_user } from '@/helpers/users'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
function getUsers(userIds: string[]): Promise<Labrinth.Users.v2.User[]> {
|
||||
return client.labrinth.users_v2.getMultiple(userIds)
|
||||
}
|
||||
</script>
|
||||
@@ -25,7 +25,7 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5 min-w-[600px]">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div v-for="option in options" :key="option" class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast capitalize">
|
||||
|
||||
+70
-10
@@ -120,8 +120,17 @@
|
||||
:max-height="240"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="reportOnly" class="flex flex-col gap-2">
|
||||
<Checkbox v-model="deleteInstance" :label="formatMessage(messages.deleteInstance)" />
|
||||
<div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
|
||||
<Checkbox
|
||||
v-if="reportOnly"
|
||||
v-model="deleteInstance"
|
||||
:label="formatMessage(messages.deleteInstance)"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="blockTargetUserId"
|
||||
v-model="blockUser"
|
||||
:label="formatMessage(messages.blockUser)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -249,12 +258,14 @@ import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
Avatar,
|
||||
blockedUsersQueryKey,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectAuth,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
@@ -266,6 +277,7 @@ import {
|
||||
useScrollIndicator,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
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 type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
import { create_report } from '@/helpers/reports'
|
||||
import { block_user } from '@/helpers/users'
|
||||
|
||||
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
||||
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
||||
@@ -284,6 +297,7 @@ type ExternalFileRow = {
|
||||
name: string
|
||||
}
|
||||
type SharedInstanceCreator = {
|
||||
id: string | null
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
}
|
||||
@@ -300,13 +314,17 @@ type ReportReason = 'malicious' | 'inappropriate' | 'spam'
|
||||
const reportReason = ref<ReportReason>('malicious')
|
||||
const additionalContext = ref('')
|
||||
const deleteInstance = ref(true)
|
||||
const blockUser = ref(true)
|
||||
const blockTargetUserId = ref<string | null>(null)
|
||||
const submitLoading = ref(false)
|
||||
const uploadedImageIDs = ref<string[]>([])
|
||||
const emit = defineEmits<{
|
||||
reported: [deleteInstance: boolean]
|
||||
}>()
|
||||
const { formatMessage } = useVIntl()
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
const { load } = useSharedInstancePreviewContent()
|
||||
const {
|
||||
@@ -365,13 +383,39 @@ async function submitReport() {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const uploadedImages = uploadedImageIDs.value.slice(-10)
|
||||
await create_report({
|
||||
report_type: reportReason.value,
|
||||
item_type: 'shared-instance',
|
||||
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
||||
body,
|
||||
uploaded_images: uploadedImages,
|
||||
})
|
||||
const blockTarget = blockUser.value ? blockTargetUserId.value : null
|
||||
const [reportResult, blockResult] = await Promise.allSettled([
|
||||
create_report({
|
||||
report_type: reportReason.value,
|
||||
item_type: 'shared-instance',
|
||||
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
|
||||
hide()
|
||||
@@ -426,6 +470,8 @@ function resetReportState() {
|
||||
reportReason.value = 'malicious'
|
||||
additionalContext.value = ''
|
||||
deleteInstance.value = true
|
||||
blockUser.value = true
|
||||
blockTargetUserId.value = null
|
||||
submitLoading.value = false
|
||||
uploadedImageIDs.value = []
|
||||
}
|
||||
@@ -437,12 +483,18 @@ function show(
|
||||
) {
|
||||
resetReportState()
|
||||
creator.value = creatorValue ?? null
|
||||
blockTargetUserId.value = creatorValue?.id ?? null
|
||||
install.value = installValue
|
||||
showPreview(previewValue, event)
|
||||
}
|
||||
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
|
||||
function showReport(
|
||||
previewValue: SharedInstanceInstallPreview,
|
||||
blockTargetUserIdValue?: string | null,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
resetReportState()
|
||||
creator.value = null
|
||||
blockTargetUserId.value = blockTargetUserIdValue ?? null
|
||||
reportMode.value = true
|
||||
reportOnly.value = true
|
||||
install.value = () => {}
|
||||
@@ -537,6 +589,14 @@ const messages = defineMessages({
|
||||
id: 'app.modal.install-to-play.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: {
|
||||
id: 'app.modal.install-to-play.unknown-files-warning',
|
||||
defaultMessage: 'Unknown files warning',
|
||||
|
||||
+3
@@ -32,6 +32,7 @@ type InstallModal = {
|
||||
}
|
||||
|
||||
type SharedInstanceCreator = {
|
||||
id: string | null
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
}
|
||||
@@ -190,6 +191,7 @@ export function useSharedInstanceInviteHandler(
|
||||
},
|
||||
invite.invitedByUsername
|
||||
? {
|
||||
id: invite.invitedById,
|
||||
username: invite.invitedByUsername,
|
||||
avatarUrl: invite.invitedByAvatarUrl,
|
||||
}
|
||||
@@ -294,6 +296,7 @@ export function useSharedInstanceInviteHandler(
|
||||
},
|
||||
manager
|
||||
? {
|
||||
id: manager.id,
|
||||
username: manager.username,
|
||||
avatarUrl: manager.avatar_url ?? null,
|
||||
}
|
||||
|
||||
@@ -50,7 +50,31 @@ export async function get_user_collections(
|
||||
|
||||
export async function patch_user(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'bio' | 'role' | 'username'>>,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|patch_user', { userId, patch })
|
||||
}
|
||||
|
||||
export async function change_user_avatar(
|
||||
userId: string,
|
||||
image: Uint8Array,
|
||||
extension: string,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|change_user_avatar', { userId, image, extension })
|
||||
}
|
||||
|
||||
export async function delete_user_avatar(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|delete_user_avatar', { userId })
|
||||
}
|
||||
|
||||
export async function block_user(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|block_user', { userId })
|
||||
}
|
||||
|
||||
export async function unblock_user(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|unblock_user', { userId })
|
||||
}
|
||||
|
||||
export async function get_blocked_users(): Promise<Labrinth.BlockedUsers.v3.BlockedUserId[]> {
|
||||
return await invoke<Labrinth.BlockedUsers.v3.BlockedUserId[]>('plugin:users|get_blocked_users')
|
||||
}
|
||||
|
||||
@@ -650,6 +650,9 @@
|
||||
"app.modal.install-to-play.additional-context-placeholder": {
|
||||
"message": "Include links and images if possible and relevant"
|
||||
},
|
||||
"app.modal.install-to-play.block-user": {
|
||||
"message": "Block user"
|
||||
},
|
||||
"app.modal.install-to-play.content-you-are-reporting": {
|
||||
"message": "Instance you’re reporting"
|
||||
},
|
||||
@@ -731,6 +734,9 @@
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Unrecognized files"
|
||||
},
|
||||
"app.modal.install-to-play.user-blocked": {
|
||||
"message": "User blocked"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "View contents"
|
||||
},
|
||||
@@ -990,7 +996,7 @@
|
||||
"message": "Behavior"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Default instance options"
|
||||
"message": "Default game options"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Java installations"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="w-full pt-2">
|
||||
<div class="w-full px-2 pt-2">
|
||||
<UserProfilePageLayout
|
||||
:user-id="userId"
|
||||
:project-type="projectType"
|
||||
variant="app"
|
||||
site-url="https://modrinth.com"
|
||||
project-link-mode="app"
|
||||
:edit-profile-link="openProfileSettings"
|
||||
external-navigation
|
||||
/>
|
||||
</div>
|
||||
@@ -14,19 +15,24 @@
|
||||
<script setup lang="ts">
|
||||
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, inject, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
block_user,
|
||||
get_blocked_users,
|
||||
get_user_collections,
|
||||
get_user_organizations,
|
||||
get_user_profile,
|
||||
get_user_projects,
|
||||
patch_user,
|
||||
unblock_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalOpenProfileKey } from '@/providers/app-settings-modal'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const openProfileSettings = inject(appSettingsModalOpenProfileKey, () => {})
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const userProfile = provideUserProfile({
|
||||
@@ -35,6 +41,9 @@ const userProfile = provideUserProfile({
|
||||
getOrganizations: get_user_organizations,
|
||||
getCollections: get_user_collections,
|
||||
patchUser: patch_user,
|
||||
getBlockedUsers: get_blocked_users,
|
||||
blockUser: block_user,
|
||||
unblockUser: unblock_user,
|
||||
})
|
||||
|
||||
const userId = computed(() => {
|
||||
|
||||
@@ -701,7 +701,7 @@ async function reportSharedInstance(event?: MouseEvent, closeUpdateModal = false
|
||||
)
|
||||
if (instance.value?.id !== reportInstance.id) return
|
||||
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
|
||||
sharedInstanceReportModal.value?.showReport(preview, event)
|
||||
sharedInstanceReportModal.value?.showReport(preview, sharedInstance.manager_id, event)
|
||||
} catch (error) {
|
||||
notifySharedInstanceError(error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export type UnsavedChangesController = {
|
||||
hasChanges: () => boolean
|
||||
getOriginal: () => Record<string, unknown>
|
||||
getModified: () => Record<string, unknown>
|
||||
isSaving: () => boolean
|
||||
reset: () => void
|
||||
save: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export type AppSettingsModalContext = {
|
||||
close: () => boolean
|
||||
registerUnsavedChangesController: (controller: UnsavedChangesController | null) => void
|
||||
}
|
||||
|
||||
export const appSettingsModalContextKey: InjectionKey<AppSettingsModalContext> =
|
||||
Symbol('appSettingsModalContext')
|
||||
export const appSettingsModalOpenProfileKey: InjectionKey<() => void> = Symbol(
|
||||
'appSettingsModalOpenProfile',
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type AuthUser,
|
||||
provideAuth,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type Ref, ref, watchEffect } from 'vue'
|
||||
import { computed, type Ref, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
type AppCredentials = {
|
||||
session?: string | null
|
||||
@@ -37,5 +37,13 @@ export function setupAuthProvider(
|
||||
user.value = credentials.value?.user ?? null
|
||||
})
|
||||
|
||||
watch(user, (updatedUser) => {
|
||||
if (!credentials.value || !updatedUser || credentials.value.user === updatedUser) return
|
||||
credentials.value = {
|
||||
...credentials.value,
|
||||
user: updatedUser,
|
||||
}
|
||||
})
|
||||
|
||||
provideAuth(authProvider)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user