mirror of
https://github.com/modrinth/code.git
synced 2026-09-05 06:19:11 +00:00
project disclosures frontend (#6955)
* begin project disclosures * new project settings header * begin disclosure settings, edit content rules * togglecards * update phrasing of the rules, prepr * more disclosure settings structuring * add functionality toggle * implement functionality with staging api * improve project settings head titles * feat(labrinth): project disclosures model * feat(labrinth): project disclosures database model * feat(labrinth): project disclosures get endpoint * feat(labrinth): censor user ids if set by moderator * feat(labrinth): wrap disclosures in struct * feat(labrinth): edit project disclosures endpoint * style(labrinth): cargo fmt * style(labrinth): fix typo * feat(labrinth): add fields for ai content disclosure * fix(labrinth): field typo * chore(labrinth): update query cache * feat(labrinth): index disclosures in search * refactor(labrinth): use enum instead of bools * feat(labrinth): trigger incremental index on disclosure change * feat(labrinth): change archived status to disclosure * feat(labrinth): use disclosure for archival status * fix(labrinth): error type for deserialization * fix(labrinth): migration timestamp * fix(labrinth): add disclosures to elasticsearch schema * disclosures in search * update photosensitivity warning copy * update archiving, move general project settings to v3 (start redesign there), use project id lookups for stable cache keys, advanced in server search * blue archive banner * display AI use types * improve labels * add modrinth to link * add placeholder missing disclosure report type * add AI metadata checking to block image uploads with notice * update copy * update section 4 of rules * Update rule 6 layout * fix(labrinth): don't remove archival disclosure when changing status via v3 api * feat(labrinth): derive str for ai usages and telemtry consent * feat(labrinth): include ai usages and telemetry consent in disclosure types * Update moderation checklist (#7056) * fix: missing delphi severity (#7054) * Utils nav, new disclosures stage, ai button in rules * add prefix to collect * Finish up new disclosures stage * update r4 messages * new r4 msg, update showcase clarity message * fix: version upload failing to detect mrpack loader (#7063) * fix: mrpack exporting with zip64 (#7064) * fix: action bar max width (#7048) * fix: action bar max width * fix: width * changelog * Rule placeholders with subsections & anchors, update messages. * update new messages to account for new rule and placeholder layout * Add nag for content disclosures --------- Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech> Co-authored-by: chyzman <chyzalt@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> * prepr * refactor(labrinth): move disclosure parsing to document creation * fix(labrinth): prevent removing moderator added archival disclosures * fix(labrinth): dedupe ai usages * fix(labrinth): auth logic on archived disclosure removal * feat(labrinth): add interactions field * style(labrinth): cargo fmt * feat(labrinth): granular disclosure locking * add 5.8, move 5.9, update messages and placeholders accordingly. * move disclosures stage earlier * include consent model info in telemetry disclosure msgs * prepr + update date * typo * qa pass * search suboptions * add empty state * checkboxes in column * persistent advanced filters * blog draft * prepr * support soft deletion * reload instead of optimistically updating * update blog * TOS update + minor verbiage adjustment * 45 day grace --------- Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com> Co-authored-by: coolbot <76798835+coolbot100s@users.noreply.github.com> Co-authored-by: ThatGravyBoat <gravy@thatgravyboat.tech> Co-authored-by: chyzman <chyzalt@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
This commit is contained in:
co-authored by
ThatGravyBoat
chyzman
Truman Gao
sychic
coolbot
parent
06b7c44dcc
commit
755825b09a
+22
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { InfoIcon } from '@modrinth/assets'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
persistenceNote: {
|
||||
id: 'browse.advanced-filters.persistence-note',
|
||||
defaultMessage:
|
||||
'Advanced exclusions are saved across sessions. Opening a shared link with filters will override them.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-2 flex gap-2 px-3 text-sm text-secondary opacity-80">
|
||||
<InfoIcon class="mt-0.5 size-4 shrink-0" />
|
||||
<span>{{ formatMessage(messages.persistenceNote) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,15 @@ import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import {
|
||||
compatibleAdvancedFilters,
|
||||
getAdvancedOptionIds,
|
||||
getAvailableAdvancedIds,
|
||||
mergeAdvancedPrefs,
|
||||
replaceAdvancedFilters,
|
||||
sameOptionIds,
|
||||
useAdvancedPrefs,
|
||||
} from '#ui/utils/advanced-filter-preferences'
|
||||
import type {
|
||||
EnvironmentSearchOverride,
|
||||
FilterType,
|
||||
@@ -66,6 +75,9 @@ export interface BrowseSearchState {
|
||||
setPage: (page: number) => Promise<void>
|
||||
clearSearch: () => void
|
||||
onFilterChange: () => void
|
||||
|
||||
linkOverridesAdvancedPrefs: Ref<boolean>
|
||||
applySavedAdvancedPrefs: () => void
|
||||
}
|
||||
|
||||
export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchState {
|
||||
@@ -197,6 +209,76 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
const effectiveCurrentFilters = computed(() =>
|
||||
isServerType.value ? serverCurrentFilters.value : currentFilters.value,
|
||||
)
|
||||
const effectiveFilterTypes = computed(() =>
|
||||
isServerType.value ? serverFilterTypes.value : filters.value,
|
||||
)
|
||||
|
||||
const advancedPrefs = useAdvancedPrefs()
|
||||
const linkOverridesAdvancedPrefs = ref(false)
|
||||
const selectedAdvancedIds = computed(() => getAdvancedOptionIds(effectiveCurrentFilters.value))
|
||||
|
||||
function hasSearchQuery(): boolean {
|
||||
return Object.keys(route.query).some((key) => !options.persistentQueryParams.includes(key))
|
||||
}
|
||||
|
||||
function getCompatiblePrefs(): FilterValue[] {
|
||||
return compatibleAdvancedFilters(advancedPrefs.value, effectiveFilterTypes.value)
|
||||
}
|
||||
|
||||
function setEffectiveFilters(nextFilters: FilterValue[]) {
|
||||
if (isServerType.value) {
|
||||
serverCurrentFilters.value = nextFilters
|
||||
} else {
|
||||
currentFilters.value = nextFilters
|
||||
}
|
||||
}
|
||||
|
||||
function applyAdvancedPrefs(prefs: FilterValue[]) {
|
||||
setEffectiveFilters(replaceAdvancedFilters(effectiveCurrentFilters.value, prefs))
|
||||
}
|
||||
|
||||
function syncLinkOverride() {
|
||||
const prefIds = getAdvancedOptionIds(getCompatiblePrefs())
|
||||
linkOverridesAdvancedPrefs.value =
|
||||
prefIds.length > 0 && !sameOptionIds(selectedAdvancedIds.value, prefIds)
|
||||
}
|
||||
|
||||
function initAdvancedPrefs() {
|
||||
const prefs = getCompatiblePrefs()
|
||||
|
||||
if (!hasSearchQuery()) {
|
||||
if (prefs.length > 0) {
|
||||
applyAdvancedPrefs(prefs)
|
||||
}
|
||||
linkOverridesAdvancedPrefs.value = false
|
||||
return
|
||||
}
|
||||
|
||||
syncLinkOverride()
|
||||
}
|
||||
|
||||
function applySavedAdvancedPrefs() {
|
||||
applyAdvancedPrefs(getCompatiblePrefs())
|
||||
linkOverridesAdvancedPrefs.value = false
|
||||
}
|
||||
|
||||
initAdvancedPrefs()
|
||||
|
||||
watch(selectedAdvancedIds, (selected, previous) => {
|
||||
if (previous && sameOptionIds(selected, previous)) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextPrefs = mergeAdvancedPrefs(
|
||||
advancedPrefs.value,
|
||||
selected,
|
||||
getAvailableAdvancedIds(effectiveFilterTypes.value),
|
||||
)
|
||||
if (!sameOptionIds(advancedPrefs.value, nextPrefs)) {
|
||||
advancedPrefs.value = nextPrefs
|
||||
}
|
||||
linkOverridesAdvancedPrefs.value = false
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
@@ -340,6 +422,10 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
effectiveSortTypes.value.find((sortType) => sortType.name === 'relevance') ??
|
||||
effectiveSortTypes.value[0]
|
||||
query.value = ''
|
||||
|
||||
void nextTick(() => {
|
||||
initAdvancedPrefs()
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -369,5 +455,7 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
setPage,
|
||||
clearSearch,
|
||||
onFilterChange,
|
||||
linkOverridesAdvancedPrefs,
|
||||
applySavedAdvancedPrefs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { SearchIcon } from '@modrinth/assets'
|
||||
import { RotateCounterClockwiseIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { computed, ref, toValue } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import { Button, IconButton } from '#ui/components/base/buttons'
|
||||
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
|
||||
import LoadingIndicator from '#ui/components/base/LoadingIndicator.vue'
|
||||
@@ -66,6 +67,14 @@ const messages = defineMessages({
|
||||
id: 'browse.no-results',
|
||||
defaultMessage: 'No results found for your query!',
|
||||
},
|
||||
linkOverridingPreferences: {
|
||||
id: 'browse.advanced-filters.link-overriding-preferences',
|
||||
defaultMessage: "This link's filters differ from your saved advanced exclusions",
|
||||
},
|
||||
applySavedPreferences: {
|
||||
id: 'browse.advanced-filters.apply-saved-preferences',
|
||||
defaultMessage: 'Apply saved preferences',
|
||||
},
|
||||
})
|
||||
|
||||
function cardActionType(action: CardAction) {
|
||||
@@ -151,6 +160,21 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp
|
||||
@clear="ctx.clearSearch()"
|
||||
/>
|
||||
|
||||
<Admonition
|
||||
v-if="ctx.linkOverridesAdvancedPrefs.value"
|
||||
type="info"
|
||||
:header="formatMessage(messages.linkOverridingPreferences)"
|
||||
inline-actions
|
||||
center-content
|
||||
>
|
||||
<template #actions>
|
||||
<Button type="colored" color="blue" @click="ctx.applySavedAdvancedPrefs()">
|
||||
<RotateCounterClockwiseIcon />
|
||||
{{ formatMessage(messages.applySavedPreferences) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Combobox
|
||||
:model-value="ctx.effectiveCurrentSortType.value"
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface BrowseManagerContext {
|
||||
setPage: (page: number) => Promise<void>
|
||||
clearSearch: () => void
|
||||
onFilterChange: () => void
|
||||
linkOverridesAdvancedPrefs: Ref<boolean>
|
||||
applySavedAdvancedPrefs: () => void
|
||||
|
||||
getProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) => string | RouteLocationRaw
|
||||
getServerProjectLink: (
|
||||
@@ -74,6 +76,7 @@ export interface BrowseManagerContext {
|
||||
serverOnlyLabel?: ComputedRef<string>
|
||||
hiddenFilterTypes?: ComputedRef<string[]>
|
||||
advancedFiltersCollapsed?: Ref<boolean>
|
||||
dismissedPhotosensitivityFilterWarning?: Ref<boolean>
|
||||
onInstalled?: (projectId: string) => void
|
||||
|
||||
displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'>
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { InfoIcon, XIcon } from '@modrinth/assets'
|
||||
import { computed, toValue } from 'vue'
|
||||
import { computed, nextTick, toValue, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import { IconButton } from '#ui/components/base/buttons'
|
||||
import Toggle from '#ui/components/base/Toggle.vue'
|
||||
import PhotosensitivityWarningModal from '#ui/components/modal/PhotosensitivityWarningModal.vue'
|
||||
import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { useAdvancedPrefs } from '#ui/utils/advanced-filter-preferences'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import AdvancedFiltersPersistenceNote from './components/AdvancedFiltersPersistenceNote.vue'
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
|
||||
const PHOTOSENSITIVITY_FILTER_OPTION = 'epilepsy_triggers'
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const advancedPrefs = useAdvancedPrefs()
|
||||
|
||||
const isApp = computed(() => ctx.variant === 'app')
|
||||
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
||||
const hiddenFilterTypes = computed(() => ctx.hiddenFilterTypes?.value ?? [])
|
||||
|
||||
const advancedFiltersCollapsed = computed(() => ctx.advancedFiltersCollapsed?.value ?? true)
|
||||
const photosensitivityWarningModal = useTemplateRef('photosensitivityWarningModal')
|
||||
|
||||
function setAdvancedFiltersCollapsed(collapsed: boolean) {
|
||||
if (ctx.advancedFiltersCollapsed) {
|
||||
@@ -25,6 +32,40 @@ function setAdvancedFiltersCollapsed(collapsed: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function isPhotosensitivityExclusionSelected(filters: { type: string; option: string }[]) {
|
||||
return filters.some(
|
||||
(filter) => filter.type === 'advanced' && filter.option === PHOTOSENSITIVITY_FILTER_OPTION,
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
selected: isPhotosensitivityExclusionSelected(
|
||||
ctx.isServerType.value ? ctx.serverCurrentFilters.value : ctx.currentFilters.value,
|
||||
),
|
||||
saved: advancedPrefs.value.includes(PHOTOSENSITIVITY_FILTER_OPTION),
|
||||
}),
|
||||
(current, previous) => {
|
||||
if (!previous) {
|
||||
return
|
||||
}
|
||||
|
||||
const selected = current.selected && !previous.selected
|
||||
const alreadySaved = previous.saved
|
||||
const dismissed = ctx.dismissedPhotosensitivityFilterWarning?.value
|
||||
|
||||
if (selected && !alreadySaved && !dismissed) {
|
||||
nextTick(() => photosensitivityWarningModal.value?.show())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function onPhotosensitivityWarningDismiss(dontShowAgain: boolean) {
|
||||
if (dontShowAgain && ctx.dismissedPhotosensitivityFilterWarning) {
|
||||
ctx.dismissedPhotosensitivityFilterWarning.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function closeFiltersMenu() {
|
||||
if (ctx.filtersMenuOpen) {
|
||||
ctx.filtersMenuOpen.value = false
|
||||
@@ -88,6 +129,11 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
<template>
|
||||
<slot name="prepend" />
|
||||
|
||||
<PhotosensitivityWarningModal
|
||||
ref="photosensitivityWarningModal"
|
||||
@dismiss="onPhotosensitivityWarningDismiss"
|
||||
/>
|
||||
|
||||
<div v-if="ctx.filtersMenuOpen?.value" class="fixed inset-0 z-40 bg-bg" />
|
||||
|
||||
<div
|
||||
@@ -173,12 +219,17 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
:content-class="contentClass"
|
||||
:inner-panel-class="innerPanelClass"
|
||||
:open-by-default="getFilterOpenByDefault(filterType.id)"
|
||||
@on-open="() => filterType.id === 'advanced' && setAdvancedFiltersCollapsed(false)"
|
||||
@on-close="() => filterType.id === 'advanced' && setAdvancedFiltersCollapsed(true)"
|
||||
>
|
||||
<template #header>
|
||||
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-base font-semibold'">
|
||||
{{ filterType.formatted_name }}
|
||||
</h3>
|
||||
</template>
|
||||
<template v-if="filterType.id === 'advanced'" #prefix>
|
||||
<AdvancedFiltersPersistenceNote />
|
||||
</template>
|
||||
</SearchSidebarFilter>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -205,8 +256,11 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
{{ filter.formatted_name }}
|
||||
</h3>
|
||||
</template>
|
||||
<template v-if="filter.id === 'advanced'" #prefix>
|
||||
<AdvancedFiltersPersistenceNote />
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
v-else-if="
|
||||
lockedMessages?.gameVersionShaderMessage &&
|
||||
ctx.projectType.value === 'shader' &&
|
||||
filter.id === 'game_version'
|
||||
@@ -215,7 +269,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
>
|
||||
<div class="mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue">
|
||||
<InfoIcon class="mt-1 size-4" />
|
||||
<span>{{ lockedMessages.gameVersionShaderMessage }}</span>
|
||||
<span>{{ lockedMessages?.gameVersionShaderMessage }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="lockedMessages?.gameVersion" #locked-game_version>
|
||||
|
||||
Reference in New Issue
Block a user