diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue index 9ffa818bcb..af632a9017 100644 --- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue +++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue @@ -251,6 +251,7 @@ import { type ComboboxOption, commonMessages, defineMessages, + formatReportType, injectAuth, injectModrinthClient, injectNotificationManager, @@ -329,9 +330,9 @@ const externalFileRows = computed(() => .sort((left, right) => left.name.localeCompare(right.name)), ) const reportReasonOptions = computed[]>(() => [ - { value: 'malicious', label: formatMessage(messages.maliciousReason) }, - { value: 'inappropriate', label: formatMessage(messages.inappropriateReason) }, - { value: 'spam', label: formatMessage(messages.spamReason) }, + { value: 'malicious', label: formatReportType(formatMessage, 'malicious') }, + { value: 'inappropriate', label: formatReportType(formatMessage, 'inappropriate') }, + { value: 'spam', label: formatReportType(formatMessage, 'spam') }, ]) const canSubmitReport = computed( () => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value, @@ -543,18 +544,6 @@ const messages = defineMessages({ id: 'app.modal.install-to-play.report-reason', defaultMessage: 'Which rule does this instance violate?', }, - maliciousReason: { - id: 'app.modal.install-to-play.report-reason.malicious', - defaultMessage: 'Malicious', - }, - inappropriateReason: { - id: 'app.modal.install-to-play.report-reason.inappropriate', - defaultMessage: 'Inappropriate', - }, - spamReason: { - id: 'app.modal.install-to-play.report-reason.spam', - defaultMessage: 'Spam', - }, additionalContext: { id: 'app.modal.install-to-play.additional-context', defaultMessage: 'Additional context', diff --git a/apps/app-frontend/src/locales/da-DK/index.json b/apps/app-frontend/src/locales/da-DK/index.json index c0aa6b0e50..02afec374a 100644 --- a/apps/app-frontend/src/locales/da-DK/index.json +++ b/apps/app-frontend/src/locales/da-DK/index.json @@ -440,9 +440,6 @@ "app.modal.install-to-play.report-image-too-large": { "message": "Filen overskrider 1MiB størrelse begrænsning" }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.shared-instance": { "message": "Delt instance" }, diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json index c74ea4c9f9..bf398912b5 100644 --- a/apps/app-frontend/src/locales/de-CH/index.json +++ b/apps/app-frontend/src/locales/de-CH/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "Gegen welche Regel verstößt diese Instanz?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Unangemessen" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Bösartig" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Geteilte Instanz melden" }, diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json index aec68c7665..2ac9c1e88b 100644 --- a/apps/app-frontend/src/locales/de-DE/index.json +++ b/apps/app-frontend/src/locales/de-DE/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "Gegen welche Regel verstößt diese Instanz?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Unangemessen" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Bösartig" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Geteilte Instanz melden" }, diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 9b5a674d18..48966b7e93 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -704,15 +704,6 @@ "app.modal.install-to-play.report-reason": { "message": "Which rule does this instance violate?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Inappropriate" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Malicious" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Report shared instance" }, diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json index 967688bdfb..735768c394 100644 --- a/apps/app-frontend/src/locales/es-419/index.json +++ b/apps/app-frontend/src/locales/es-419/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "¿Qué regla rompe esta instancia?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Inapropiado" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Malicioso" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Reportar instancia compartida" }, diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json index 3fdb65f6f0..5cb15c2374 100644 --- a/apps/app-frontend/src/locales/fr-FR/index.json +++ b/apps/app-frontend/src/locales/fr-FR/index.json @@ -698,15 +698,6 @@ "app.modal.install-to-play.report-reason": { "message": "Quelle règle cette instance enfreint-elle ?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Inapproprié" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Malicieux" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Signaler une instance partagée" }, diff --git a/apps/app-frontend/src/locales/hu-HU/index.json b/apps/app-frontend/src/locales/hu-HU/index.json index 77f9bffdb4..1e79f49dd8 100644 --- a/apps/app-frontend/src/locales/hu-HU/index.json +++ b/apps/app-frontend/src/locales/hu-HU/index.json @@ -704,15 +704,6 @@ "app.modal.install-to-play.report-reason": { "message": "Melyik szabályt sérti meg ez az adott játékprofil?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Nem megfelelő" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Káros" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Megosztott játékprofil jelentése" }, diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json index 16587ed018..a527c512fa 100644 --- a/apps/app-frontend/src/locales/it-IT/index.json +++ b/apps/app-frontend/src/locales/it-IT/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "Quale regola è violata da quest'istanza?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Inappropriato" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Dannoso" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Segnala istanza condivisa" }, diff --git a/apps/app-frontend/src/locales/ja-JP/index.json b/apps/app-frontend/src/locales/ja-JP/index.json index 6bd7ff8194..92ac823f87 100644 --- a/apps/app-frontend/src/locales/ja-JP/index.json +++ b/apps/app-frontend/src/locales/ja-JP/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "この起動構成はどの規則に違反していますか?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "不適切なコンテンツ" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "悪意がある" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "スパム" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "共有された起動構成を通報する" }, diff --git a/apps/app-frontend/src/locales/ko-KR/index.json b/apps/app-frontend/src/locales/ko-KR/index.json index 1be3efe362..c135221d5a 100644 --- a/apps/app-frontend/src/locales/ko-KR/index.json +++ b/apps/app-frontend/src/locales/ko-KR/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "이 인스턴스가 위반한 규칙은 무엇인가요?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "부적절함" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "악성/유해함" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "스팸" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "공유 인스턴스 신고" }, diff --git a/apps/app-frontend/src/locales/ms-MY/index.json b/apps/app-frontend/src/locales/ms-MY/index.json index 28836c710e..5c0f11251c 100644 --- a/apps/app-frontend/src/locales/ms-MY/index.json +++ b/apps/app-frontend/src/locales/ms-MY/index.json @@ -239,9 +239,6 @@ "app.modal.install-to-play.mod-count": { "message": "{count, plural, other {# mod}}" }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.shared-instance": { "message": "Pemasangan yang dikongsi" }, diff --git a/apps/app-frontend/src/locales/nl-NL/index.json b/apps/app-frontend/src/locales/nl-NL/index.json index 10af52cfdb..84f80db23e 100644 --- a/apps/app-frontend/src/locales/nl-NL/index.json +++ b/apps/app-frontend/src/locales/nl-NL/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "Welke regel wordt in deze instantie overtreden?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Ongepast" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Kwaadaardig" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Gedeelde instantie melden" }, diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json index 5fb45c66c8..61dfb14aa6 100644 --- a/apps/app-frontend/src/locales/pl-PL/index.json +++ b/apps/app-frontend/src/locales/pl-PL/index.json @@ -650,15 +650,6 @@ "app.modal.install-to-play.report-reason": { "message": "Którą zasadę narusza ta instancja?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Treści nieodpowiednie" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Zawiera Złośliwy kod" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Zgłoś przesłaną Instalacje" }, diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json index f46b9bbab6..93395022f8 100644 --- a/apps/app-frontend/src/locales/pt-BR/index.json +++ b/apps/app-frontend/src/locales/pt-BR/index.json @@ -704,15 +704,6 @@ "app.modal.install-to-play.report-reason": { "message": "Qual regra esta instância viola?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Inapropriado" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Malicioso" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Denunciar instância compartilhada" }, diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json index 76ad5b2c8e..7b450ff120 100644 --- a/apps/app-frontend/src/locales/ru-RU/index.json +++ b/apps/app-frontend/src/locales/ru-RU/index.json @@ -695,15 +695,6 @@ "app.modal.install-to-play.report-reason": { "message": "Какое правило нарушает данная сборка?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Неприемлемое содержимое" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Вредоносное содержимое" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Спам" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Сообщить об общем экземпляре" }, diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json index a6eb17552c..6a77471e85 100644 --- a/apps/app-frontend/src/locales/sv-SE/index.json +++ b/apps/app-frontend/src/locales/sv-SE/index.json @@ -674,15 +674,6 @@ "app.modal.install-to-play.report-reason": { "message": "Vilken regel bryter instansen mot?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Olämplig" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Skadlig" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Rapportera delad instans" }, diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json index 9fe9fd9359..becb7f8f6b 100644 --- a/apps/app-frontend/src/locales/tr-TR/index.json +++ b/apps/app-frontend/src/locales/tr-TR/index.json @@ -638,15 +638,6 @@ "app.modal.install-to-play.report-image-too-large": { "message": "Dosya 1 MiB boyut sınırını aşıyor" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Uygunsuz" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Kötü niyetli" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Spam" - }, "app.modal.install-to-play.report-submitted": { "message": "Rapor sunuldu" }, diff --git a/apps/app-frontend/src/locales/uk-UA/index.json b/apps/app-frontend/src/locales/uk-UA/index.json index 1666af7c26..ff2e4fd1c0 100644 --- a/apps/app-frontend/src/locales/uk-UA/index.json +++ b/apps/app-frontend/src/locales/uk-UA/index.json @@ -701,15 +701,6 @@ "app.modal.install-to-play.report-reason": { "message": "Яке правило порушує цей профіль?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "Неприпустимий уміст" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "Шкідливий уміст" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "Спам" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "Скарга на спільний профіль" }, diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 0d6ebfc67c..59ea631d27 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -704,15 +704,6 @@ "app.modal.install-to-play.report-reason": { "message": "该实例违反了哪项规则?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "不当内容" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "恶意内容" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "垃圾内容" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "举报共享实例" }, diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json index c87fdb7f28..ea72f805b0 100644 --- a/apps/app-frontend/src/locales/zh-TW/index.json +++ b/apps/app-frontend/src/locales/zh-TW/index.json @@ -704,15 +704,6 @@ "app.modal.install-to-play.report-reason": { "message": "這個實例違反了哪一條規則?" }, - "app.modal.install-to-play.report-reason.inappropriate": { - "message": "不當內容" - }, - "app.modal.install-to-play.report-reason.malicious": { - "message": "惡意內容" - }, - "app.modal.install-to-play.report-reason.spam": { - "message": "垃圾內容" - }, "app.modal.install-to-play.report-shared-instance-header": { "message": "檢舉共用實例" }, diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue index f2a4eb084e..1d6a4b2fcd 100644 --- a/apps/app-frontend/src/pages/Browse.vue +++ b/apps/app-frontend/src/pages/Browse.vue @@ -1242,11 +1242,25 @@ const advancedFiltersCollapsed = computed({ }, }) +const dismissedPhotosensitivityFilterWarning = computed({ + get: () => themeStore.getFeatureFlag('dismissed_photosensitivity_filter_warning'), + set: (value) => { + themeStore.featureFlags['dismissed_photosensitivity_filter_warning'] = value + getSettings() + .then((settings) => { + settings.feature_flags['dismissed_photosensitivity_filter_warning'] = value + return setSettings(settings) + }) + .catch(handleError) + }, +}) + provideBrowseManager({ tags, projectType, ...searchState, advancedFiltersCollapsed, + dismissedPhotosensitivityFilterWarning, getProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) => ({ path: `/project/${result.project_id ?? result.slug}`, query: getProjectBrowseQuery(), diff --git a/apps/app-frontend/src/store/theme.ts b/apps/app-frontend/src/store/theme.ts index 00d47dc5f3..8e0fe8f2b7 100644 --- a/apps/app-frontend/src/store/theme.ts +++ b/apps/app-frontend/src/store/theme.ts @@ -22,6 +22,7 @@ export const DEFAULT_FEATURE_FLAGS = { friends_online_collapsed: false, friends_offline_collapsed: true, friends_pending_collapsed: true, + dismissed_photosensitivity_filter_warning: false, } export const THEME_OPTIONS = ['dark', 'light', 'oled', 'retro', 'system'] as const diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 8852cd5123..d143b5699e 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -38,6 +38,7 @@ "wrangler": "^4.115.0" }, "dependencies": { + "@contentauth/c2pa-web": "^0.13.1", "@formatjs/intl-localematcher": "^0.5.4", "@ltd/j-toml": "^1.38.0", "@modrinth/api-client": "workspace:*", diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index edc80e96d8..42e3059ce7 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -40,6 +40,8 @@ html { interpolate-size: allow-keywords; scrollbar-gutter: stable; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); } .light-mode { diff --git a/apps/frontend/src/components/ui/AiImageWarningModal.vue b/apps/frontend/src/components/ui/AiImageWarningModal.vue new file mode 100644 index 0000000000..af37b271ce --- /dev/null +++ b/apps/frontend/src/components/ui/AiImageWarningModal.vue @@ -0,0 +1,77 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue index 115fa98e31..9819d36487 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationReportCard.vue @@ -278,11 +278,13 @@ import { CollapsibleRegion, type ContentItem, CopyCode, + formatReportType, getProjectTypeIcon, injectModrinthClient, injectNotificationManager, useFormatDateTime, useRelativeTime, + useVIntl, } from '@modrinth/ui' import { formatProjectType } from '@modrinth/utils' import { computed, ref, watch } from 'vue' @@ -297,6 +299,7 @@ import SharedInstanceReportContext, { } from './SharedInstanceReportContext.vue' const { addNotification } = injectNotificationManager() +const { formatMessage } = useVIntl() const client = injectModrinthClient() const auth = await useAuth() @@ -787,13 +790,9 @@ const reportItemUrl = computed(() => { } }) -const formattedReportType = computed(() => { - const reportType = props.report.report_type - - // some are split by -, some are split by " " - const words = reportType.includes('-') ? reportType.split('-') : reportType.split(' ') - return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') -}) +const formattedReportType = computed(() => + formatReportType(formatMessage, props.report.report_type), +) function copyId() { navigator.clipboard.writeText(props.report.id).then(() => { diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue new file mode 100644 index 0000000000..8b877f5693 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue @@ -0,0 +1,83 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue new file mode 100644 index 0000000000..fb71d2a464 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue @@ -0,0 +1,144 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/ArchivedDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/ArchivedDisclosureCard.vue new file mode 100644 index 0000000000..7c4ea95e91 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/ArchivedDisclosureCard.vue @@ -0,0 +1,80 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue new file mode 100644 index 0000000000..95afae3f24 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue @@ -0,0 +1,171 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue new file mode 100644 index 0000000000..da89a1568b --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue @@ -0,0 +1,97 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue new file mode 100644 index 0000000000..fa0ccc588d --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue new file mode 100644 index 0000000000..87d2be5021 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue new file mode 100644 index 0000000000..53de20fa4e --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue @@ -0,0 +1,136 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/form.ts b/apps/frontend/src/components/ui/project-settings/disclosures/form.ts new file mode 100644 index 0000000000..9556e59662 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/form.ts @@ -0,0 +1,189 @@ +import type { Labrinth } from '@modrinth/api-client' + +import type { + DisclosureFormState, + DisclosureOf, + DisclosureType, + NoteDisclosure, + ProjectDisclosure, + ProjectDisclosureData, +} from './types' + +function findDisclosure( + disclosures: ProjectDisclosureData[], + type: T, +): DisclosureOf | undefined { + return disclosures.find((disclosure): disclosure is DisclosureOf => disclosure.type === type) +} + +type NoteDisclosureType = + | 'advertisements' + | 'epilepsy_triggers' + | 'system_interactions' + | 'archived' + +function createNoteModel( + disclosures: ProjectDisclosureData[], + type: NoteDisclosureType, +): NoteDisclosure { + const disclosure = findDisclosure(disclosures, type) + return { + enabled: !!disclosure, + note: disclosure?.note ?? '', + } +} + +function nonemptyOrPlaceholder(values: string[]): string[] { + return values.length > 0 ? [...values] : [''] +} + +export function disclosuresToForm(disclosures: ProjectDisclosureData[]): DisclosureFormState { + const ai = findDisclosure(disclosures, 'ai_content') + const paidFeatures = findDisclosure(disclosures, 'paid_features') + const telemetry = findDisclosure(disclosures, 'telemetry') + const derivative = findDisclosure(disclosures, 'derivative_work') + + return { + ai: { + enabled: !!ai, + uses: ai ? [...(ai.uses ?? [])] : [], + note: ai?.note ?? '', + }, + advertising: createNoteModel(disclosures, 'advertisements'), + paidFeatures: { + enabled: !!paidFeatures, + features: paidFeatures ? nonemptyOrPlaceholder(paidFeatures.features) : [], + }, + telemetry: { + enabled: !!telemetry, + consent: telemetry?.consent ?? 'opt_in', + entries: telemetry ? nonemptyOrPlaceholder(telemetry.data_collected) : [], + }, + derivative: { + enabled: !!derivative, + sources: derivative ? derivative.sources.map((source) => ({ ...source })) : [], + }, + photosensitivity: createNoteModel(disclosures, 'epilepsy_triggers'), + systemInteractions: createNoteModel(disclosures, 'system_interactions'), + archived: createNoteModel(disclosures, 'archived'), + } +} + +export function formToDisclosures(form: DisclosureFormState): ProjectDisclosure[] { + const set: ProjectDisclosure[] = [] + + if (form.ai.enabled) { + set.push({ + type: 'ai_content', + uses: [...form.ai.uses], + note: form.ai.note.trim() || null, + }) + } + + if (form.advertising.enabled) { + set.push({ type: 'advertisements', note: form.advertising.note.trim() || null }) + } + + if (form.paidFeatures.enabled) { + set.push({ + type: 'paid_features', + features: form.paidFeatures.features.map((feature) => feature.trim()).filter(Boolean), + }) + } + + if (form.telemetry.enabled) { + set.push({ + type: 'telemetry', + consent: form.telemetry.consent, + data_collected: form.telemetry.entries.map((entry) => entry.trim()).filter(Boolean), + }) + } + + if (form.derivative.enabled) { + set.push({ + type: 'derivative_work', + sources: form.derivative.sources.map((source) => ({ + label: source.label.trim(), + link: source.link?.trim() || null, + note: source.note?.trim() || null, + })), + }) + } + + if (form.photosensitivity.enabled) { + set.push({ type: 'epilepsy_triggers', note: form.photosensitivity.note.trim() || null }) + } + + if (form.systemInteractions.enabled) { + set.push({ type: 'system_interactions', note: form.systemInteractions.note.trim() || null }) + } + + if (form.archived.enabled) { + set.push({ type: 'archived', note: form.archived.note.trim() || null }) + } + + return set +} + +export function toModifyRequest( + form: DisclosureFormState, + previous: DisclosureFormState, +): Labrinth.Projects.v3.ModifyProjectDisclosures { + const set = formToDisclosures(form) + const nextTypes = new Set(set.map((disclosure) => disclosure.type)) + return { + set, + remove: formToDisclosures(previous) + .map((disclosure) => disclosure.type) + .filter((type) => !nextTypes.has(type)), + } +} + +export function toCachedDisclosures(set: ProjectDisclosure[]): ProjectDisclosureData[] { + const now = new Date().toISOString() + return set.map((disclosure) => ({ + ...disclosure, + set_by_moderator: false, + updated_at: now, + updated_by: null, + })) +} + +export type DisclosureFormIssue = + | 'advertising-note' + | 'paid-features-empty' + | 'telemetry-empty' + | 'derivative-empty' + | 'derivative-source-label' + | 'photosensitivity-note' + | 'system-interactions-note' + +export function getDisclosureFormIssues(form: DisclosureFormState): DisclosureFormIssue[] { + const issues: DisclosureFormIssue[] = [] + const missingNote = (model: NoteDisclosure) => model.enabled && !model.note.trim() + + if (missingNote(form.advertising)) { + issues.push('advertising-note') + } + if (form.paidFeatures.enabled && !form.paidFeatures.features.some((feature) => feature.trim())) { + issues.push('paid-features-empty') + } + if (form.telemetry.enabled && !form.telemetry.entries.some((entry) => entry.trim())) { + issues.push('telemetry-empty') + } + if (form.derivative.enabled) { + if (form.derivative.sources.length === 0) { + issues.push('derivative-empty') + } else if (form.derivative.sources.some((source) => !source.label?.trim())) { + issues.push('derivative-source-label') + } + } + if (missingNote(form.photosensitivity)) { + issues.push('photosensitivity-note') + } + if (missingNote(form.systemInteractions)) { + issues.push('system-interactions-note') + } + + return issues +} diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/index.ts b/apps/frontend/src/components/ui/project-settings/disclosures/index.ts new file mode 100644 index 0000000000..97185705d2 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/index.ts @@ -0,0 +1,10 @@ +export { default as AdvertisingDisclosureCard } from './AdvertisingDisclosureCard.vue' +export { default as AiDisclosureCard } from './AiDisclosureCard.vue' +export { default as ArchivedDisclosureCard } from './ArchivedDisclosureCard.vue' +export { default as DerivativeDisclosureCard } from './DerivativeDisclosureCard.vue' +export * from './form' +export { default as PaidFeaturesDisclosureCard } from './PaidFeaturesDisclosureCard.vue' +export { default as PhotosensitivityDisclosureCard } from './PhotosensitivityDisclosureCard.vue' +export { default as SystemInteractionsDisclosureCard } from './SystemInteractionsDisclosureCard.vue' +export { default as TelemetryDisclosureCard } from './TelemetryDisclosureCard.vue' +export * from './types' diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/types.ts b/apps/frontend/src/components/ui/project-settings/disclosures/types.ts new file mode 100644 index 0000000000..2aeb67410a --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/types.ts @@ -0,0 +1,47 @@ +import type { Labrinth } from '@modrinth/api-client' + +export type TelemetryConsent = Labrinth.Projects.v3.TelemetryConsent +export type AiUsage = Labrinth.Projects.v3.AiUsage +export type DerivativeSource = Labrinth.Projects.v3.DerivativeSource +export type ProjectDisclosure = Labrinth.Projects.v3.ProjectDisclosure +export type ProjectDisclosureData = Labrinth.Projects.v3.ProjectDisclosureData +export type DisclosureType = Labrinth.Projects.v3.ProjectDisclosureType +export type DisclosureOf = Labrinth.Projects.v3.ProjectDisclosureOf + +export type NoteDisclosure = { + enabled: boolean + note: string +} + +export type AiDisclosure = { + enabled: boolean + uses: AiUsage[] + note: string +} + +export type PaidFeaturesDisclosure = { + enabled: boolean + features: string[] +} + +export type TelemetryDisclosure = { + enabled: boolean + consent: TelemetryConsent + entries: string[] +} + +export type DerivativeDisclosure = { + enabled: boolean + sources: DerivativeSource[] +} + +export type DisclosureFormState = { + ai: AiDisclosure + advertising: NoteDisclosure + paidFeatures: PaidFeaturesDisclosure + telemetry: TelemetryDisclosure + derivative: DerivativeDisclosure + photosensitivity: NoteDisclosure + systemInteractions: NoteDisclosure + archived: NoteDisclosure +} diff --git a/apps/frontend/src/components/ui/report/ReportInfo.vue b/apps/frontend/src/components/ui/report/ReportInfo.vue index 0d5ccfec21..1de7dfb119 100644 --- a/apps/frontend/src/components/ui/report/ReportInfo.vue +++ b/apps/frontend/src/components/ui/report/ReportInfo.vue @@ -90,7 +90,14 @@
- +
import { BoxesIcon, ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets' -import { Avatar, Badge, CopyCode, useFormatDateTime, useRelativeTime } from '@modrinth/ui' +import { + Avatar, + Badge, + CopyCode, + defineMessages, + formatReportType, + useFormatDateTime, + useRelativeTime, + useVIntl, +} from '@modrinth/ui' import { formatProjectType, renderHighlightedString } from '@modrinth/utils' import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue' import { getProjectTypeForUrl } from '~/helpers/projects.js' +const { formatMessage } = useVIntl() const formatRelativeTime = useRelativeTime() const formatDateTime = useFormatDateTime({ timeStyle: 'short', @@ -165,6 +182,13 @@ defineProps({ }) const flags = useFeatureFlags() + +const messages = defineMessages({ + reportedFor: { + id: 'report.reported-for', + defaultMessage: 'Reported for {type}', + }, +}) diff --git a/apps/frontend/src/pages/moderation/reports/index.vue b/apps/frontend/src/pages/moderation/reports/index.vue index 67973d5e54..e769d666b2 100644 --- a/apps/frontend/src/pages/moderation/reports/index.vue +++ b/apps/frontend/src/pages/moderation/reports/index.vue @@ -217,6 +217,7 @@ import { Combobox, type ComboboxOption, commonMessages, + formatReportType, injectModrinthClient, MultiSelect, type MultiSelectItem, @@ -361,7 +362,13 @@ const reportIssueFilterTypes = computed[]>(() => { const issueTypes = new Set(allReports.value.map((report) => report.report_type)) const sortedTypes = Array.from(issueTypes).sort() - return [...base, ...sortedTypes.map((type) => ({ value: type, label: type }))] + return [ + ...base, + ...sortedTypes.map((type) => ({ + value: type, + label: formatReportType(formatMessage, type), + })), + ] }) type ReportedType = T & { report_item_count: number } diff --git a/apps/frontend/src/pages/organization/[organization].vue b/apps/frontend/src/pages/organization/[organization].vue index 88368fc6e0..609d3c8e4c 100644 --- a/apps/frontend/src/pages/organization/[organization].vue +++ b/apps/frontend/src/pages/organization/[organization].vue @@ -241,13 +241,14 @@ import { useVIntl, } from '@modrinth/ui' import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils' -import { useQuery } from '@tanstack/vue-query' +import { useQuery, useQueryClient } from '@tanstack/vue-query' import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component' import AdPlaceholder from '~/components/ui/AdPlaceholder.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import NavStack from '~/components/ui/NavStack.vue' import OrganizationPageHeader from '~/components/ui/OrganizationPageHeader.vue' +import { warmProjectCheckCaches } from '~/composables/queries/project' import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js' import { OrganizationContext, @@ -296,6 +297,7 @@ if (route.path.includes('settings')) { const routeHasSettings = computed(() => route.path.includes('settings')) const client = injectModrinthClient() +const queryClient = useQueryClient() const { data: organization, @@ -346,6 +348,14 @@ const { placeholderData: [], }) +watch( + projects, + (list) => { + warmProjectCheckCaches(queryClient, list) + }, + { immediate: true }, +) + const refresh = async () => { await Promise.all([refreshOrganization(), refreshProjects()]) } diff --git a/apps/frontend/src/pages/report.vue b/apps/frontend/src/pages/report.vue index d43a69ef14..86d58f9f43 100644 --- a/apps/frontend/src/pages/report.vue +++ b/apps/frontend/src/pages/report.vue @@ -106,7 +106,7 @@