feat: malware warning modal changes (#6721)

* feat: improved warning modals

* fix: qa

* feat: install to play and update to play changes

* fix: dont warn for server projects as already reviewed

* fix: lint
This commit is contained in:
Calum H.
2026-07-14 20:47:10 +00:00
committed by GitHub
parent 8cca911775
commit 905204cc5f
58 changed files with 1244 additions and 1351 deletions
@@ -170,6 +170,7 @@ const props = withDefaults(
header?: string
hideHeader?: boolean
onHide?: () => void
onAfterHide?: () => void
onShow?: () => void
mergeHeader?: boolean
scrollable?: boolean
@@ -196,6 +197,7 @@ const props = withDefaults(
header: undefined,
hideHeader: false,
onHide: () => {},
onAfterHide: () => {},
onShow: () => {},
mergeHeader: false,
// TODO: migrate all modals to use scrollable and remove this prop
@@ -289,6 +291,7 @@ function hide() {
previousFocusEl = null
setTimeout(() => {
open.value = false
nextTick(() => props.onAfterHide?.())
}, 300)
}
@@ -0,0 +1,255 @@
<template>
<NewModal
ref="modal"
:header="formatMessage(messages.header)"
:on-hide="handleHide"
:on-after-hide="handleAfterHide"
max-width="544px"
width="544px"
>
<div class="flex flex-col items-end gap-6">
<Admonition
type="warning"
:header="
formatMessage(
mode === 'modpack' ? messages.modpackWarningTitle : messages.modWarningTitle,
)
"
class="w-full"
>
<span class="font-medium text-contrast">{{ fileName }}</span>
{{
formatMessage(mode === 'modpack' ? messages.modpackWarningBody : messages.modWarningBody)
}}
</Admonition>
<p class="m-0 w-full leading-6 text-primary">
{{ formatMessage(messages.reviewedFiles) }}
</p>
<div v-if="mode === 'modpack'" class="relative w-full">
<div
ref="externalFileTableBody"
class="max-h-[242px] overflow-y-auto rounded-2xl"
@scroll="checkTableScrollState"
>
<Table
:columns="externalFileColumns"
:data="externalFileRows"
row-key="id"
virtualized
:virtual-row-height="48"
class="shadow-sm"
>
<template #cell-name="{ value }">
<span class="block truncate" :title="String(value)">{{ value }}</span>
</template>
</Table>
</div>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-2"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-2"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTableTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-2 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-2"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-2"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTableBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-2 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
</div>
<p class="m-0 w-full font-medium leading-6 text-orange">
{{ formatMessage(messages.malwareWarning) }}
</p>
<Checkbox
v-if="mode === 'mod'"
v-model="dontShowAgain"
class="w-full"
:label="formatMessage(messages.dontShowAgain)"
/>
<div class="flex items-center gap-2">
<ButtonStyled type="transparent" color="orange">
<button type="button" @click="continueInstallation">
{{ formatMessage(messages.installAnyway) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button type="button" @click="cancelInstallation">
<BanIcon aria-hidden="true" />
{{ formatMessage(messages.dontInstall) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { BanIcon } from '@modrinth/assets'
import { computed, nextTick, ref, useTemplateRef } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { useScrollIndicator } from '../../composables/scroll-indicator'
import Admonition from '../base/Admonition.vue'
import ButtonStyled from '../base/ButtonStyled.vue'
import Checkbox from '../base/Checkbox.vue'
import Table, { type TableColumn } from '../base/Table.vue'
import NewModal from './NewModal.vue'
const props = withDefaults(
defineProps<{
mode: 'modpack' | 'mod'
fileName: string
externalFilesInModpack?: string[]
}>(),
{
externalFilesInModpack: () => [],
},
)
const emit = defineEmits<{
cancel: []
continue: [dontShowAgain: boolean]
}>()
const { formatMessage } = useVIntl()
const modal = useTemplateRef('modal')
const externalFileTableBody = ref<HTMLElement | null>(null)
const dontShowAgain = ref(false)
let pendingAction: { type: 'cancel' } | { type: 'continue'; dontShowAgain: boolean } | null = null
const {
showTopFade: showTableTopFade,
showBottomFade: showTableBottomFade,
checkScrollState: checkTableScrollState,
forceCheck: forceCheckTableScroll,
} = useScrollIndicator(externalFileTableBody)
const messages = defineMessages({
header: {
id: 'unknown-file-warning-modal.header',
defaultMessage: 'Confirm installation',
},
modpackWarningTitle: {
id: 'unknown-file-warning-modal.modpack-warning-title',
defaultMessage: 'Unknown files warning',
},
modWarningTitle: {
id: 'unknown-file-warning-modal.mod-warning-title',
defaultMessage: 'Unknown file warning',
},
modpackWarningBody: {
id: 'unknown-file-warning-modal.modpack-warning-body',
defaultMessage:
' contains files that arent published on Modrinth. We strongly recommend only installing files from sources you trust.',
},
modWarningBody: {
id: 'unknown-file-warning-modal.mod-warning-body',
defaultMessage:
' isnt published on Modrinth. We strongly recommend only installing files from sources you trust.',
},
reviewedFiles: {
id: 'unknown-file-warning-modal.reviewed-files',
defaultMessage:
'A file is only reviewed if its published to Modrinth, regardless of its file format (including .mrpack).',
},
unrecognizedFiles: {
id: 'unknown-file-warning-modal.unrecognized-files',
defaultMessage: 'Unrecognized files',
},
malwareWarning: {
id: 'unknown-file-warning-modal.malware-warning',
defaultMessage:
'Malware is often distributed through mod files by sharing them on platforms like Discord.',
},
dontShowAgain: {
id: 'unknown-file-warning-modal.dont-show-again',
defaultMessage: 'Dont show this warning again',
},
installAnyway: {
id: 'unknown-file-warning-modal.install-anyway',
defaultMessage: 'Install anyway',
},
dontInstall: {
id: 'unknown-file-warning-modal.dont-install',
defaultMessage: 'Dont install',
},
})
type ExternalFileColumn = 'name'
type ExternalFileRow = {
id: string
name: string
}
const externalFileColumns = computed<TableColumn<ExternalFileColumn>[]>(() => [
{
key: 'name',
label: formatMessage(messages.unrecognizedFiles),
cellClass: '!h-12',
},
])
const externalFileRows = computed<ExternalFileRow[]>(() =>
props.externalFilesInModpack.map((name, index) => ({
id: `${index}-${name}`,
name,
})),
)
async function show() {
dontShowAgain.value = false
pendingAction = null
modal.value?.show()
await nextTick()
forceCheckTableScroll()
}
function hide() {
modal.value?.hide()
}
function handleHide() {
pendingAction ??= { type: 'cancel' }
}
function handleAfterHide() {
const action = pendingAction
pendingAction = null
dontShowAgain.value = false
if (action?.type === 'continue') {
emit('continue', action.dontShowAgain)
} else {
emit('cancel')
}
}
function cancelInstallation() {
pendingAction = { type: 'cancel' }
modal.value?.hide()
}
function continueInstallation() {
pendingAction = { type: 'continue', dontShowAgain: dontShowAgain.value }
modal.value?.hide()
}
defineExpose({ show, hide })
</script>
@@ -8,4 +8,5 @@ export { default as OpenInAppModal } from './OpenInAppModal.vue'
export { default as ShareModal } from './ShareModal.vue'
export type { Tab as TabbedModalTab } from './TabbedModal.vue'
export { default as TabbedModal } from './TabbedModal.vue'
export { default as UnknownFileWarningModal } from './UnknownFileWarningModal.vue'
export { default as UploadProgressModal } from './UploadProgressModal.vue'