feat: show avatar in install to play modal

This commit is contained in:
Calum H. (IMB11)
2026-07-27 17:37:01 +01:00
parent 893b163a86
commit 5b5468020d
2 changed files with 92 additions and 16 deletions
@@ -64,7 +64,31 @@
</div> </div>
</Admonition> </Admonition>
<p v-else class="m-0 text-primary"> <p v-else class="m-0 text-primary">
{{ formatMessage(messages.inviteWarning) }} <IntlFormatted
v-if="creator"
:message-id="messages.inviteWarningWithCreator"
:values="{ username: creator.username }"
>
<template #creator="{ children }">
<AutoLink
:to="creatorProfileLink"
class="inline-flex items-center gap-1 align-middle font-medium text-contrast hover:underline"
>
<Avatar
:src="creator.avatarUrl"
:alt="creator.username"
:tint-by="creator.username"
size="24px"
circle
no-shadow
/>
<component :is="() => children" />
</AutoLink>
</template>
</IntlFormatted>
<template v-else>
{{ formatMessage(messages.inviteWarning) }}
</template>
</p> </p>
<SharedInstanceInstallSummary <SharedInstanceInstallSummary
:preview="preview" :preview="preview"
@@ -226,6 +250,7 @@ import { BanIcon, DownloadIcon, ReportIcon, SendIcon, SpinnerIcon, XIcon } from
import { import {
Admonition, Admonition,
AutoLink, AutoLink,
Avatar,
ButtonStyled, ButtonStyled,
Checkbox, Checkbox,
Combobox, Combobox,
@@ -243,8 +268,10 @@ import {
useScrollIndicator, useScrollIndicator,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
import { config } from '@/config'
import { hide_ads_window, show_ads_window } from '@/helpers/ads' 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'
@@ -258,11 +285,16 @@ type ExternalFileRow = {
id: string id: string
name: string name: string
} }
type SharedInstanceCreator = {
username: string
avatarUrl: string | null
}
const modal = ref<InstanceType<typeof NewModal>>() const modal = ref<InstanceType<typeof NewModal>>()
const contentModal = ref<InstanceType<typeof ModpackContentModal>>() const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
const externalFileTable = ref<HTMLElement | null>(null) const externalFileTable = ref<HTMLElement | null>(null)
const preview = ref<SharedInstanceInstallPreview | null>(null) const preview = ref<SharedInstanceInstallPreview | null>(null)
const creator = ref<SharedInstanceCreator | null>(null)
const install = ref<() => void | Promise<void>>(() => {}) const install = ref<() => void | Promise<void>>(() => {})
const reportMode = ref(false) const reportMode = ref(false)
const reportOnly = ref(false) const reportOnly = ref(false)
@@ -302,6 +334,12 @@ const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
const canSubmitReport = computed( const canSubmitReport = computed(
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value, () => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
) )
const creatorProfileLink = computed(() => {
const username = creator.value?.username
return username
? () => openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
: undefined
})
async function accept() { async function accept() {
hide() hide()
@@ -381,6 +419,7 @@ function handleCancel() {
} }
function handleHide() { function handleHide() {
resetReportState() resetReportState()
creator.value = null
show_ads_window() show_ads_window()
} }
function resetReportState() { function resetReportState() {
@@ -395,14 +434,17 @@ function resetReportState() {
function show( function show(
previewValue: SharedInstanceInstallPreview, previewValue: SharedInstanceInstallPreview,
installValue: () => void | Promise<void>, installValue: () => void | Promise<void>,
creatorValue?: SharedInstanceCreator,
event?: MouseEvent, event?: MouseEvent,
) { ) {
resetReportState() resetReportState()
creator.value = creatorValue ?? null
install.value = installValue install.value = installValue
showPreview(previewValue, event) showPreview(previewValue, event)
} }
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) { function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
resetReportState() resetReportState()
creator.value = null
reportMode.value = true reportMode.value = true
reportOnly.value = true reportOnly.value = true
install.value = () => {} install.value = () => {}
@@ -437,6 +479,11 @@ const messages = defineMessages({
defaultMessage: defaultMessage:
'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.', 'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
}, },
inviteWarningWithCreator: {
id: 'app.modal.install-to-play.invite-warning-with-creator',
defaultMessage:
'This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust.',
},
reportDescription: { reportDescription: {
id: 'app.modal.install-to-play.report-description', id: 'app.modal.install-to-play.report-description',
defaultMessage: defaultMessage:
@@ -29,9 +29,15 @@ type InstallModal = {
show( show(
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>, preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>, install: () => Promise<void>,
creator?: SharedInstanceCreator,
): void ): void
} }
type SharedInstanceCreator = {
username: string
avatarUrl: string | null
}
type AccountRequiredModal = { type AccountRequiredModal = {
show(event?: MouseEvent): Promise<boolean> show(event?: MouseEvent): Promise<boolean>
} }
@@ -63,6 +69,7 @@ export function useSharedInstanceInviteHandler(
instanceId: string instanceId: string
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>> preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>
install: () => Promise<void> install: () => Promise<void>
creator?: SharedInstanceCreator
onGoToInstance?: () => void | Promise<void> onGoToInstance?: () => void | Promise<void>
} }
| undefined | undefined
@@ -78,7 +85,7 @@ export function useSharedInstanceInviteHandler(
async function resolveInvite(invite: SharedInstanceInvite) { async function resolveInvite(invite: SharedInstanceInvite) {
const [invitedBy, sharedInstance] = await Promise.all([ const [invitedBy, sharedInstance] = await Promise.all([
!invite.invitedByUsername && invite.invitedById (!invite.invitedByUsername || !invite.invitedByAvatarUrl) && invite.invitedById
? get_user(invite.invitedById, 'bypass').catch(() => null) ? get_user(invite.invitedById, 'bypass').catch(() => null)
: null, : null,
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => { client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => {
@@ -98,15 +105,17 @@ export function useSharedInstanceInviteHandler(
function showInstall( function showInstall(
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>, preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>, install: () => Promise<void>,
creator?: SharedInstanceCreator,
) { ) {
if (!installModal.value) throw new Error('Shared instance install modal is not available.') if (!installModal.value) throw new Error('Shared instance install modal is not available.')
installModal.value.show(preview, install) installModal.value.show(preview, install, creator)
} }
async function showInstallOrAlreadyInstalled( async function showInstallOrAlreadyInstalled(
sharedInstanceId: string, sharedInstanceId: string,
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>, preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
install: () => Promise<void>, install: () => Promise<void>,
creator?: SharedInstanceCreator,
onGoToInstance?: () => void | Promise<void>, onGoToInstance?: () => void | Promise<void>,
) { ) {
const existingInstance = (await list()).find( const existingInstance = (await list()).find(
@@ -114,7 +123,7 @@ export function useSharedInstanceInviteHandler(
) )
if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) { if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
showInstall(preview, install) showInstall(preview, install, creator)
return return
} }
@@ -126,6 +135,7 @@ export function useSharedInstanceInviteHandler(
instanceId: existingInstance.id, instanceId: existingInstance.id,
preview, preview,
install, install,
creator,
onGoToInstance, onGoToInstance,
} }
alreadyInstalledModal.value.show(existingInstance.name) alreadyInstalledModal.value.show(existingInstance.name)
@@ -154,7 +164,7 @@ export function useSharedInstanceInviteHandler(
const pending = pendingAlreadyInstalled const pending = pendingAlreadyInstalled
pendingAlreadyInstalled = undefined pendingAlreadyInstalled = undefined
if (!pending) return if (!pending) return
showInstall(pending.preview, pending.install) showInstall(pending.preview, pending.install, pending.creator)
} }
async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) { async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) {
@@ -180,6 +190,12 @@ export function useSharedInstanceInviteHandler(
await markNotificationRead(notification) await markNotificationRead(notification)
await queryClient.invalidateQueries({ queryKey: ['instances'] }) await queryClient.invalidateQueries({ queryKey: ['instances'] })
}, },
invite.invitedByUsername
? {
username: invite.invitedByUsername,
avatarUrl: invite.invitedByAvatarUrl,
}
: undefined,
() => markNotificationRead(notification), () => markNotificationRead(notification),
) )
} catch (error) { } catch (error) {
@@ -261,17 +277,30 @@ export function useSharedInstanceInviteHandler(
try { try {
if (!(await requireAccount())) return if (!(await requireAccount())) return
const invite = await install_accept_shared_instance_invite(inviteId) const invite = await install_accept_shared_instance_invite(inviteId)
await showInstallOrAlreadyInstalled(invite.sharedInstanceId, invite.preview, async () => { const manager = invite.managerId
await install_shared_instance( ? await get_user(invite.managerId, 'bypass').catch(() => null)
invite.sharedInstanceId, : null
invite.preview.name, await showInstallOrAlreadyInstalled(
invite.managerId, invite.sharedInstanceId,
invite.serverManagerName, invite.preview,
invite.serverManagerIconUrl, async () => {
invite.instanceIconUrl, await install_shared_instance(
) invite.sharedInstanceId,
await queryClient.invalidateQueries({ queryKey: ['instances'] }) invite.preview.name,
}) invite.managerId,
invite.serverManagerName,
invite.serverManagerIconUrl,
invite.instanceIconUrl,
)
await queryClient.invalidateQueries({ queryKey: ['instances'] })
},
manager
? {
username: manager.username,
avatarUrl: manager.avatar_url ?? null,
}
: undefined,
)
} catch (error) { } catch (error) {
notifySharedInstanceError(error) notifySharedInstanceError(error)
} }