diff --git a/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue b/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue index e3839dd4a6..7a9f3ddded 100644 --- a/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue +++ b/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue @@ -65,11 +65,17 @@ export interface SharedInstanceReportDetails { other_instances_loaded?: boolean } -const props = defineProps<{ - details: SharedInstanceReportDetails - banPending?: boolean - loadVersionContent?: (instanceId: string, version: number) => Promise -}>() +const props = withDefaults( + defineProps<{ + details: SharedInstanceReportDetails + banPending?: boolean + loadVersionContent?: (instanceId: string, version: number) => Promise + contextType?: 'report' | 'moderation' + }>(), + { + contextType: 'report', + }, +) const emit = defineEmits<{ banOwner: [owner: SharedInstanceReportUser] @@ -223,7 +229,7 @@ function formattedLoader(version: SharedInstanceReportVersion) { - Report context + {{ contextType === 'report' ? 'Report context' : 'Moderation context' }} @@ -310,7 +316,10 @@ function formattedLoader(version: SharedInstanceReportVersion) {
Version {{ version.version }} Reported @@ -333,7 +342,11 @@ function formattedLoader(version: SharedInstanceReportVersion) {
- No version was attached to this report. + {{ + contextType === 'report' + ? 'No version was attached to this report.' + : 'No version history is available.' + }} diff --git a/apps/frontend/src/helpers/shared-instance-content.ts b/apps/frontend/src/helpers/shared-instance-content.ts new file mode 100644 index 0000000000..78fe4547cb --- /dev/null +++ b/apps/frontend/src/helpers/shared-instance-content.ts @@ -0,0 +1,170 @@ +import type { AbstractModrinthClient, Labrinth, SharedInstances } from '@modrinth/api-client' +import type { ContentItem } from '@modrinth/ui' + +type SharedInstanceVersionDependency = Labrinth.Versions.v2.Dependency & { + project_id?: string + version_id?: string +} + +export function createSharedInstanceContentLoader(client: AbstractModrinthClient) { + const instanceVersions = new Map() + + function cacheVersion(instanceId: string, version: SharedInstances.Instances.v1.InstanceVersion) { + instanceVersions.set(`${instanceId}:${version.version}`, version) + } + + async function getVersion( + instanceId: string, + versionNumber: number, + ): Promise { + const cacheKey = `${instanceId}:${versionNumber}` + const cachedVersion = instanceVersions.get(cacheKey) + if (cachedVersion) return cachedVersion + + const version = await client.sharedinstances.instances_v1.getVersion(instanceId, versionNumber) + instanceVersions.set(cacheKey, version) + return version + } + + async function loadVersionContent( + instanceId: string, + versionNumber: number, + ): Promise { + const instanceVersion = await getVersion(instanceId, versionNumber) + const modpackVersionId = instanceVersion.modpack_id + const directVersionIds = (instanceVersion.modrinth_ids ?? []).filter( + (versionId) => versionId !== modpackVersionId, + ) + const modpackVersion = modpackVersionId + ? await client.labrinth.versions_v2.getVersion(modpackVersionId) + : null + const modpackDependencies = (modpackVersion?.dependencies ?? + []) as SharedInstanceVersionDependency[] + const dependencyVersionIds = modpackDependencies.flatMap((dependency) => + dependency.version_id ? [dependency.version_id] : [], + ) + const uniqueVersionIds = [...new Set([...directVersionIds, ...dependencyVersionIds])] + const versions = uniqueVersionIds.length + ? await client.labrinth.versions_v2.getVersions(uniqueVersionIds) + : [] + const dependencyProjectIds = modpackDependencies.flatMap((dependency) => + dependency.project_id ? [dependency.project_id] : [], + ) + const projectIds = [ + ...new Set([ + ...versions.map((version) => version.project_id), + ...dependencyProjectIds, + ...(modpackVersion ? [modpackVersion.project_id] : []), + ]), + ] + const projects = projectIds.length + ? await client.labrinth.projects_v2.getMultiple(projectIds) + : [] + const versionsById = new Map(versions.map((version) => [version.id, version])) + const projectsById = new Map(projects.map((project) => [project.id, project])) + const modpackProject = modpackVersion ? projectsById.get(modpackVersion.project_id) : undefined + + const directContent: ContentItem[] = [...new Set(directVersionIds)].flatMap((versionId) => { + const version = versionsById.get(versionId) + if (!version) return [] + + const project = projectsById.get(version.project_id) + return [sharedInstanceContentItem(version, project)] + }) + + const modpackContent: ContentItem[] = modpackDependencies.map((dependency) => { + const version = dependency.version_id ? versionsById.get(dependency.version_id) : undefined + const project = dependency.project_id + ? projectsById.get(dependency.project_id) + : version + ? projectsById.get(version.project_id) + : undefined + const primaryFile = version + ? (version.files.find((file) => file.primary) ?? version.files[0]) + : undefined + const fileName = + primaryFile?.filename ?? + dependency.file_name ?? + project?.title ?? + version?.name ?? + 'Unknown' + + const item = sharedInstanceContentItem( + version, + project, + fileName, + dependency.project_id ?? fileName, + !project && !version, + ) + return modpackProject + ? { + ...item, + source: { project: modpackProject }, + } + : item + }) + + const externalContent: ContentItem[] = instanceVersion.external_files.map((file, index) => ({ + id: `external:${file.file_type}:${file.file_name}:${index}`, + file_name: file.file_name, + size: file.file_size, + project_type: file.file_type, + has_update: false, + update_version_id: null, + source_kind: 'shared_instance', + external: true, + external_url: file.url, + project: { + id: file.file_name, + slug: file.file_name, + title: file.file_name, + icon_url: undefined, + }, + })) + + return [...externalContent, ...modpackContent, ...directContent] + } + + return { cacheVersion, getVersion, loadVersionContent } +} + +function sharedInstanceContentItem( + version: Labrinth.Versions.v2.Version | undefined, + project: Labrinth.Projects.v2.Project | undefined, + fallbackFileName?: string, + fallbackProjectId = version?.project_id ?? fallbackFileName ?? 'unknown', + external = false, +): ContentItem { + const primaryFile = version + ? (version.files.find((file) => file.primary) ?? version.files[0]) + : undefined + const fileName = + primaryFile?.filename ?? fallbackFileName ?? project?.title ?? version?.name ?? 'Unknown' + + return { + id: version?.id ?? project?.id ?? fileName, + file_name: fileName, + size: primaryFile?.size, + project_type: project?.project_type ?? 'mod', + has_update: false, + update_version_id: null, + source_kind: 'shared_instance', + external, + project: { + id: project?.id ?? fallbackProjectId, + slug: project?.slug ?? fallbackProjectId, + title: project?.title ?? version?.name ?? fileName, + icon_url: project?.icon_url ?? undefined, + }, + ...(version + ? { + version: { + id: version.id, + version_number: version.version_number, + file_name: fileName, + date_published: version.date_published, + }, + } + : {}), + } +} diff --git a/apps/frontend/src/pages/admin/shared-instances/[user].vue b/apps/frontend/src/pages/admin/shared-instances/[user].vue new file mode 100644 index 0000000000..aaf964e467 --- /dev/null +++ b/apps/frontend/src/pages/admin/shared-instances/[user].vue @@ -0,0 +1,566 @@ + + + diff --git a/apps/frontend/src/pages/share/[inviteId].vue b/apps/frontend/src/pages/share/[inviteId].vue index d32602bf28..36c0a1b631 100644 --- a/apps/frontend/src/pages/share/[inviteId].vue +++ b/apps/frontend/src/pages/share/[inviteId].vue @@ -106,6 +106,18 @@ {{ formatMessage(messages.acceptInvite) }} + + + View shared instances + + +
{{ formatMessage(messages.knowThisUser) @@ -124,14 +136,17 @@