mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
323 lines
9.3 KiB
TypeScript
323 lines
9.3 KiB
TypeScript
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<T extends DisclosureType>(
|
|
disclosures: ProjectDisclosureData[],
|
|
type: T,
|
|
): DisclosureOf<T> | undefined {
|
|
return disclosures.find((disclosure): disclosure is DisclosureOf<T> => 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<DisclosureType, DisclosureLockStatus> {
|
|
const lockStatuses = Object.fromEntries(
|
|
PROJECT_DISCLOSURE_TYPES.map((type) => [type, 'unlocked']),
|
|
) as Record<DisclosureType, DisclosureLockStatus>
|
|
|
|
for (const disclosure of disclosures) {
|
|
lockStatuses[disclosure.type] = disclosure.lock_status ?? 'unlocked'
|
|
}
|
|
|
|
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<T extends DisclosureType>(
|
|
disclosures: ProjectDisclosureData[] | undefined,
|
|
type: T,
|
|
): DisclosureOf<T> | 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<DisclosureLockStatus, ProjectDisclosure[]>()
|
|
const disabledLockGroups = new Map<DisclosureLockStatus, ProjectDisclosure[]>()
|
|
|
|
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
|
|
}
|