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 e3127d456a..b1ede855a4 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, @@ -328,9 +329,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, @@ -540,18 +541,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 830d28558f..f925df8b5a 100644 --- a/apps/app-frontend/src/locales/da-DK/index.json +++ b/apps/app-frontend/src/locales/da-DK/index.json @@ -470,9 +470,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 2287ffb102..c7e9a16c25 100644 --- a/apps/app-frontend/src/locales/de-CH/index.json +++ b/apps/app-frontend/src/locales/de-CH/index.json @@ -704,15 +704,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 368db919e5..6838202a8b 100644 --- a/apps/app-frontend/src/locales/de-DE/index.json +++ b/apps/app-frontend/src/locales/de-DE/index.json @@ -704,15 +704,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 2d69b77eed..b7bf112680 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -710,15 +710,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 ebac5e2e5f..6f7b3a8532 100644 --- a/apps/app-frontend/src/locales/es-419/index.json +++ b/apps/app-frontend/src/locales/es-419/index.json @@ -704,15 +704,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 c211e432f0..94558454fe 100644 --- a/apps/app-frontend/src/locales/fr-FR/index.json +++ b/apps/app-frontend/src/locales/fr-FR/index.json @@ -704,15 +704,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 92dccd017d..bc081ef302 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 ez az adott példány?" }, - "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 példány 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 a43823a1a4..bf001fb3e3 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 96238bfa95..eefa24eab0 100644 --- a/apps/app-frontend/src/locales/ja-JP/index.json +++ b/apps/app-frontend/src/locales/ja-JP/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/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 1d7c91c661..087958660c 100644 --- a/apps/app-frontend/src/locales/ms-MY/index.json +++ b/apps/app-frontend/src/locales/ms-MY/index.json @@ -245,9 +245,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 ddef943c54..aa9ba62999 100644 --- a/apps/app-frontend/src/locales/nl-NL/index.json +++ b/apps/app-frontend/src/locales/nl-NL/index.json @@ -704,15 +704,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 d6c98e9068..7c43099a89 100644 --- a/apps/app-frontend/src/locales/pl-PL/index.json +++ b/apps/app-frontend/src/locales/pl-PL/index.json @@ -668,15 +668,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 6826ef1414..dba47c4940 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 0f0d7d0ce3..59d4ca1003 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 0ee4a37823..68eae1f747 100644 --- a/apps/app-frontend/src/locales/sv-SE/index.json +++ b/apps/app-frontend/src/locales/sv-SE/index.json @@ -683,15 +683,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 f8f49fd86b..cff44feee3 100644 --- a/apps/app-frontend/src/locales/tr-TR/index.json +++ b/apps/app-frontend/src/locales/tr-TR/index.json @@ -665,15 +665,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 3da3934fef..bf4e39ff34 100644 --- a/apps/app-frontend/src/locales/uk-UA/index.json +++ b/apps/app-frontend/src/locales/uk-UA/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-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index a65c62a8e7..ee7c33c136 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 095aa8b80c..718a9b7a67 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 fc536765e9..156c472201 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..764875a8f7 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue @@ -0,0 +1,86 @@ + + + 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..118b45c5c5 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue @@ -0,0 +1,125 @@ + + + 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..feeb534347 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/ArchivedDisclosureCard.vue @@ -0,0 +1,86 @@ + + + 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..2ab6664a59 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue @@ -0,0 +1,179 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureToggleCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureToggleCard.vue new file mode 100644 index 0000000000..feeb1f4a39 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureToggleCard.vue @@ -0,0 +1,146 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureUpdatedBy.vue b/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureUpdatedBy.vue new file mode 100644 index 0000000000..cf1be3b624 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/DisclosureUpdatedBy.vue @@ -0,0 +1,74 @@ + + + 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..2fc186bea6 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue @@ -0,0 +1,100 @@ + + + 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..4224a04205 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue @@ -0,0 +1,68 @@ + + + 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..d3466a0a4c --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue @@ -0,0 +1,69 @@ + + + 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..d369f4217d --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue @@ -0,0 +1,132 @@ + + + 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..0e7d7324a9 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/form.ts @@ -0,0 +1,322 @@ +import type { Labrinth } from '@modrinth/api-client' +import { + isActiveDisclosure, + isDisclosureCompatibleWithProjectTypes, + PROJECT_DISCLOSURE_TYPES, +} from '@modrinth/ui' + +import type { + DisclosureFormState, + DisclosureLockStatus, + 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' | 'archived' + +function createNoteModel( + disclosures: ProjectDisclosureData[], + type: NoteDisclosureType, +): NoteDisclosure { + const disclosure = findDisclosure(disclosures, type) + return { + enabled: isActiveDisclosure(disclosure), + note: disclosure?.note ?? '', + } +} + +function nonemptyOrPlaceholder(values: string[]): string[] { + return values.length > 0 ? [...values] : [''] +} + +function createLockStatuses( + disclosures: ProjectDisclosureData[], +): Record { + const lockStatuses = Object.fromEntries( + PROJECT_DISCLOSURE_TYPES.map((type) => [type, 'unlocked']), + ) as Record + + for (const disclosure of disclosures) { + lockStatuses[disclosure.type] = disclosure.lock_status + } + + return lockStatuses +} + +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') + const systemInteractions = findDisclosure(disclosures, 'system_interactions') + + return { + ai: { + enabled: isActiveDisclosure(ai), + uses: ai ? [...(ai.uses ?? [])] : [], + note: ai?.note ?? '', + }, + advertising: createNoteModel(disclosures, 'advertisements'), + paidFeatures: { + enabled: isActiveDisclosure(paidFeatures), + features: paidFeatures ? nonemptyOrPlaceholder(paidFeatures.features) : [], + }, + telemetry: { + enabled: isActiveDisclosure(telemetry), + consent: telemetry?.consent ?? 'opt_in', + entries: telemetry ? nonemptyOrPlaceholder(telemetry.data_collected) : [], + }, + derivative: { + enabled: isActiveDisclosure(derivative), + sources: derivative ? derivative.sources.map((source) => ({ ...source })) : [], + }, + photosensitivity: createNoteModel(disclosures, 'epilepsy_triggers'), + systemInteractions: { + enabled: isActiveDisclosure(systemInteractions), + note: systemInteractions?.note ?? '', + interactions: systemInteractions ? [...systemInteractions.interactions] : [], + }, + archived: createNoteModel(disclosures, 'archived'), + lockStatuses: createLockStatuses(disclosures), + } +} + +export function findDisclosureData( + disclosures: ProjectDisclosureData[] | undefined, + type: T, +): DisclosureOf | undefined { + if (!disclosures) return undefined + return findDisclosure(disclosures, type) +} + +export function formToDisclosure( + form: DisclosureFormState, + type: DisclosureType, +): ProjectDisclosure { + switch (type) { + case 'ai_content': + return { + type: 'ai_content', + uses: [...form.ai.uses], + note: form.ai.note.trim() || null, + } + case 'advertisements': + return { type: 'advertisements', note: form.advertising.note.trim() || null } + case 'paid_features': + return { + type: 'paid_features', + features: form.paidFeatures.features.map((feature) => feature.trim()).filter(Boolean), + } + case 'telemetry': + return { + type: 'telemetry', + consent: form.telemetry.consent, + data_collected: form.telemetry.entries.map((entry) => entry.trim()).filter(Boolean), + } + case 'derivative_work': + return { + type: 'derivative_work', + sources: form.derivative.sources.map((source) => ({ + label: source.label.trim(), + link: source.link?.trim() || null, + note: source.note?.trim() || null, + })), + } + case 'epilepsy_triggers': + return { type: 'epilepsy_triggers', note: form.photosensitivity.note.trim() || null } + case 'system_interactions': + return { + type: 'system_interactions', + interactions: [...form.systemInteractions.interactions], + note: form.systemInteractions.note.trim() || null, + } + case 'archived': + return { type: 'archived', note: form.archived.note.trim() || null } + } +} + +export function formToDisclosures(form: DisclosureFormState): ProjectDisclosure[] { + const set: ProjectDisclosure[] = [] + + if (form.ai.enabled) { + set.push(formToDisclosure(form, 'ai_content')) + } + if (form.advertising.enabled) { + set.push(formToDisclosure(form, 'advertisements')) + } + if (form.paidFeatures.enabled) { + set.push(formToDisclosure(form, 'paid_features')) + } + if (form.telemetry.enabled) { + set.push(formToDisclosure(form, 'telemetry')) + } + if (form.derivative.enabled) { + set.push(formToDisclosure(form, 'derivative_work')) + } + if (form.photosensitivity.enabled) { + set.push(formToDisclosure(form, 'epilepsy_triggers')) + } + if (form.systemInteractions.enabled) { + set.push(formToDisclosure(form, 'system_interactions')) + } + if (form.archived.enabled) { + set.push(formToDisclosure(form, 'archived')) + } + + return set +} + +export function toModifyRequests( + form: DisclosureFormState, + previous: DisclosureFormState, +): Labrinth.Projects.v3.ModifyProjectDisclosures[] { + const next = formToDisclosures(form) + const previousDisclosures = formToDisclosures(previous) + const previousByType = new Map( + previousDisclosures.map((disclosure) => [disclosure.type, disclosure]), + ) + const nextTypes = new Set(next.map((disclosure) => disclosure.type)) + + const remove: DisclosureType[] = [] + const contentOnly: ProjectDisclosure[] = [] + const lockChangeGroups = new Map() + const disabledLockGroups = new Map() + + for (const disclosure of next) { + const existing = previousByType.get(disclosure.type) + const contentChanged = !existing || JSON.stringify(existing) !== JSON.stringify(disclosure) + const previousLock = previous.lockStatuses[disclosure.type] ?? 'unlocked' + const nextLock = form.lockStatuses[disclosure.type] ?? 'unlocked' + const lockChanged = previousLock !== nextLock + + if (!contentChanged && !lockChanged) { + continue + } + + if (lockChanged) { + const group = lockChangeGroups.get(nextLock) ?? [] + group.push(disclosure) + lockChangeGroups.set(nextLock, group) + } else { + contentOnly.push(disclosure) + } + } + + for (const type of PROJECT_DISCLOSURE_TYPES) { + if (nextTypes.has(type)) { + continue + } + + const previousLock = previous.lockStatuses[type] ?? 'unlocked' + const nextLock = form.lockStatuses[type] ?? 'unlocked' + const lockChanged = previousLock !== nextLock + const wasEnabled = previousByType.has(type) + + if (wasEnabled && !lockChanged) { + remove.push(type) + continue + } + + if (!lockChanged) { + continue + } + + const group = disabledLockGroups.get(nextLock) ?? [] + group.push(formToDisclosure(form, type)) + disabledLockGroups.set(nextLock, group) + } + + const requests: Labrinth.Projects.v3.ModifyProjectDisclosures[] = [] + + if (remove.length > 0) { + requests.push({ set: [], remove }) + } + + if (contentOnly.length > 0) { + requests.push({ set: contentOnly, remove: [] }) + } + + for (const [lockStatus, set] of lockChangeGroups) { + requests.push({ set, remove: [], lock_status: lockStatus }) + } + + for (const [lockStatus, set] of disabledLockGroups) { + requests.push({ set, remove: [], lock_status: lockStatus }) + requests.push({ set: [], remove: set.map((disclosure) => disclosure.type) }) + } + + return requests +} + +export function getDisclosureFormSnapshot(form: DisclosureFormState) { + return { + disclosures: formToDisclosures(form), + lockStatuses: { ...form.lockStatuses }, + } +} + +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, + projectTypes?: readonly string[], +): DisclosureFormIssue[] { + const issues: DisclosureFormIssue[] = [] + const missingNote = (model: NoteDisclosure) => model.enabled && !model.note.trim() + const compatible = (type: DisclosureType) => + !projectTypes || isDisclosureCompatibleWithProjectTypes(type, projectTypes) + + if (compatible('advertisements') && missingNote(form.advertising)) { + issues.push('advertising-note') + } + if ( + compatible('paid_features') && + form.paidFeatures.enabled && + !form.paidFeatures.features.some((feature) => feature.trim()) + ) { + issues.push('paid-features-empty') + } + if ( + compatible('telemetry') && + form.telemetry.enabled && + !form.telemetry.entries.some((entry) => entry.trim()) + ) { + issues.push('telemetry-empty') + } + if (compatible('derivative_work') && 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 (compatible('epilepsy_triggers') && missingNote(form.photosensitivity)) { + issues.push('photosensitivity-note') + } + if ( + compatible('system_interactions') && + form.systemInteractions.enabled && + !form.systemInteractions.note.trim() + ) { + 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..085bf22204 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/index.ts @@ -0,0 +1,12 @@ +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 { default as DisclosureToggleCard } from './DisclosureToggleCard.vue' +export { default as DisclosureUpdatedBy } from './DisclosureUpdatedBy.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..6983314a98 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/types.ts @@ -0,0 +1,71 @@ +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 DisclosureLockStatus = Labrinth.Projects.v3.DisclosureLockStatus +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 SystemInteractionsDisclosure = { + enabled: boolean + note: string + interactions: 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: SystemInteractionsDisclosure + archived: NoteDisclosure + lockStatuses: Record +} + +export type DisclosureUpdatedByUser = { + id: string + username: string + avatar_url?: string | null +} + +export type DisclosureCardMetaProps = { + disabled?: boolean + toggleDisabled?: boolean + updatedAt?: string | null + updatedBy?: DisclosureUpdatedByUser | null + setByModerator?: boolean + lockStatus?: DisclosureLockStatus | null + showLockControls?: boolean +} 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/legal/terms.vue b/apps/frontend/src/pages/legal/terms.vue index 14b9d8f43c..1c76a6e15f 100644 --- a/apps/frontend/src/pages/legal/terms.vue +++ b/apps/frontend/src/pages/legal/terms.vue @@ -1,7 +1,7 @@