mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
feat: move users page to xplat page system (#6889)
* feat: move users page to xplat * feat: stop doing tauri plugin open for user links * feat: qa * fix: qa * fix: qa * fix: comment * fix: prepr * prepr * prepr * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -115,6 +115,7 @@ import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
|
||||
import { parse_modrinth_user_link } from '@/helpers/users'
|
||||
import {
|
||||
areUpdatesEnabled,
|
||||
enqueueUpdateForInstallation,
|
||||
@@ -265,7 +266,7 @@ providePageContext({
|
||||
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
),
|
||||
},
|
||||
openExternalUrl: (url) => openUrl(url),
|
||||
openExternalUrl: (url) => void openUrl(url),
|
||||
})
|
||||
provideModalBehavior({
|
||||
noblur: computed(() => !themeStore.advancedRendering),
|
||||
@@ -966,7 +967,7 @@ async function declineServerInviteNotification(notification) {
|
||||
|
||||
function openServerInviteInviterProfile(inviterName) {
|
||||
if (!inviterName) return
|
||||
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
|
||||
void router.push(`/user/${encodeURIComponent(inviterName)}`)
|
||||
}
|
||||
|
||||
async function handleLiveNotification(notification) {
|
||||
@@ -1407,8 +1408,11 @@ function handleClick(e) {
|
||||
!target.href.startsWith('https://tauri.localhost') &&
|
||||
!target.href.startsWith('http://tauri.localhost')
|
||||
) {
|
||||
const userPath = parse_modrinth_user_link(target.href)
|
||||
const parsed = parseModrinthLink(target.href)
|
||||
if (target.target !== '_blank' && parsed) {
|
||||
if (userPath) {
|
||||
void router.push(userPath)
|
||||
} else if (target.target !== '_blank' && parsed) {
|
||||
void openModrinthProjectLinkInApp(parsed)
|
||||
} else {
|
||||
openUrl(target.href)
|
||||
@@ -1654,7 +1658,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
:options="[
|
||||
{
|
||||
id: 'view-profile',
|
||||
action: () => openUrl('https://modrinth.com/user/' + credentials.user.username),
|
||||
action: () => router.push(`/user/${encodeURIComponent(credentials.user.username)}`),
|
||||
},
|
||||
{
|
||||
id: 'sign-out',
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
OverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -54,7 +55,7 @@ function createContextMenuOptions(friend: FriendWithUserData) {
|
||||
}
|
||||
|
||||
function openProfile(username: string) {
|
||||
openUrl('https://modrinth.com/user/' + username)
|
||||
void router.push(`/user/${encodeURIComponent(username)}`)
|
||||
}
|
||||
|
||||
const friendOptions = useTemplateRef('friendOptions')
|
||||
|
||||
@@ -186,8 +186,8 @@ import {
|
||||
type TeleportOverflowMenuItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
@@ -247,6 +247,7 @@ const messages = defineMessages({
|
||||
defaultMessage: "This instance's content is being shared to other users.",
|
||||
},
|
||||
})
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -331,7 +332,7 @@ const sharedInstanceManagerLabel = computed(() =>
|
||||
const sharedInstanceManagerAction = computed(() => {
|
||||
const manager = props.sharedInstanceManager
|
||||
if (manager?.type !== 'user') return undefined
|
||||
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(manager.name)}`)
|
||||
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
|
||||
})
|
||||
const playtimeLabel = computed(() => {
|
||||
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
|
||||
|
||||
+1
-3
@@ -6,11 +6,9 @@ import {
|
||||
injectPopupNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type Ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import {
|
||||
@@ -216,7 +214,7 @@ export function useSharedInstanceInviteHandler(
|
||||
markNotificationRead(notification).catch((error) => handleError(toError(error))),
|
||||
onOpenActor: () => {
|
||||
if (invite.invitedByUsername) {
|
||||
openUrl(`${config.siteUrl}/user/${encodeURIComponent(invite.invitedByUsername)}`)
|
||||
void router.push(`/user/${encodeURIComponent(invite.invitedByUsername)}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,56 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export type SearchUser = {
|
||||
id: string
|
||||
username: string
|
||||
avatar_url: string | null
|
||||
// Converts user profile links from rendered Markdown/any dynamic content into app routes.
|
||||
export function parse_modrinth_user_link(href: string): string | null {
|
||||
try {
|
||||
const url = new URL(href)
|
||||
if (url.hostname !== 'modrinth.com' && url.hostname !== 'www.modrinth.com') return null
|
||||
|
||||
const segments = url.pathname.split('/').filter(Boolean)
|
||||
if (segments[0]?.toLowerCase() !== 'user' || !segments[1] || segments.length > 3) return null
|
||||
|
||||
const path = `/user/${encodeURIComponent(decodeURIComponent(segments[1]))}`
|
||||
return segments[2] ? `${path}/${encodeURIComponent(decodeURIComponent(segments[2]))}` : path
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function search_user(query: string): Promise<SearchUser[]> {
|
||||
return await invoke<SearchUser[]>('plugin:users|search_user', { query })
|
||||
export async function search_user(query: string): Promise<Labrinth.Users.v3.SearchUser[]> {
|
||||
return await invoke<Labrinth.Users.v3.SearchUser[]>('plugin:users|search_user', { query })
|
||||
}
|
||||
|
||||
export async function get_user_profile(userId: string): Promise<Labrinth.Users.v3.User> {
|
||||
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
|
||||
}
|
||||
|
||||
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_user_organizations(
|
||||
userId: string,
|
||||
): Promise<Labrinth.Organizations.v3.Organization[]> {
|
||||
return await invoke<Labrinth.Organizations.v3.Organization[]>(
|
||||
'plugin:users|get_user_organizations',
|
||||
{ userId },
|
||||
)
|
||||
}
|
||||
|
||||
export async function get_user_collections(
|
||||
userId: string,
|
||||
): Promise<Labrinth.Collections.Collection[]> {
|
||||
return await invoke<Labrinth.Collections.Collection[]>('plugin:users|get_user_collections', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function patch_user(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|patch_user', { userId, patch })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="w-full pt-2">
|
||||
<UserProfilePageLayout
|
||||
:user-id="userId"
|
||||
:project-type="projectType"
|
||||
variant="app"
|
||||
site-url="https://modrinth.com"
|
||||
project-link-mode="app"
|
||||
external-navigation
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
get_user_collections,
|
||||
get_user_organizations,
|
||||
get_user_profile,
|
||||
get_user_projects,
|
||||
patch_user,
|
||||
} from '@/helpers/users'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const userProfile = provideUserProfile({
|
||||
getUser: get_user_profile,
|
||||
getProjects: get_user_projects,
|
||||
getOrganizations: get_user_organizations,
|
||||
getCollections: get_user_collections,
|
||||
patchUser: patch_user,
|
||||
})
|
||||
|
||||
const userId = computed(() => {
|
||||
const value = route.params.user
|
||||
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
|
||||
})
|
||||
const projectType = computed(() => {
|
||||
const value = route.params.projectType
|
||||
return Array.isArray(value) ? value[0] : value
|
||||
})
|
||||
|
||||
async function ensureUserProfileData(id: string): Promise<void> {
|
||||
if (!id) return
|
||||
|
||||
let breadcrumbName = id
|
||||
try {
|
||||
const user = await queryClient.ensureQueryData({
|
||||
queryKey: ['user', id],
|
||||
queryFn: () => userProfile.getUser(id),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
breadcrumbName = user.username
|
||||
} catch {
|
||||
// Let the mounted layout's useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
|
||||
await Promise.allSettled([
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: ['user', id, 'projects'],
|
||||
queryFn: () => userProfile.getProjects(id),
|
||||
staleTime: 30_000,
|
||||
}),
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: ['user', id, 'organizations'],
|
||||
queryFn: () => userProfile.getOrganizations(id),
|
||||
staleTime: 30_000,
|
||||
}),
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: ['user', id, 'collections'],
|
||||
queryFn: () => userProfile.getCollections(id),
|
||||
staleTime: 30_000,
|
||||
}),
|
||||
])
|
||||
|
||||
breadcrumbs.setName('User', breadcrumbName)
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
const value = to.params.user
|
||||
const id = Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
|
||||
await ensureUserProfileData(id)
|
||||
})
|
||||
|
||||
breadcrumbs.setName('User', userId.value)
|
||||
await ensureUserProfileData(userId.value)
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: computed(() => ['user', userId.value]),
|
||||
queryFn: () => userProfile.getUser(userId.value),
|
||||
enabled: false,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
watch(
|
||||
[userId, user],
|
||||
([currentUserId, value]) => {
|
||||
breadcrumbs.setName('User', value?.username ?? currentUserId)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ServersManageAccessPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
@@ -29,7 +28,7 @@ try {
|
||||
}
|
||||
|
||||
function userProfileLink(username: string) {
|
||||
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
|
||||
return `/user/${encodeURIComponent(username)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Browse from './Browse.vue'
|
||||
import Index from './Index.vue'
|
||||
import Servers from './Servers.vue'
|
||||
import Skins from './Skins.vue'
|
||||
import User from './User.vue'
|
||||
import Worlds from './Worlds.vue'
|
||||
|
||||
export { Browse, Index, Servers, Skins, Worlds }
|
||||
export { Browse, Index, Servers, Skins, User, Worlds }
|
||||
|
||||
@@ -198,6 +198,13 @@ const messages = defineMessages({
|
||||
|
||||
let savedModalState: ModpackContentModalState | null = null
|
||||
|
||||
function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']> {
|
||||
if (owner.type === 'user') return `/user/${encodeURIComponent(owner.id)}`
|
||||
return () => {
|
||||
void openUrl(`https://modrinth.com/organization/${owner.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
|
||||
@@ -1390,10 +1397,7 @@ provideContentManager({
|
||||
owner: linkedModpackOwner.value
|
||||
? {
|
||||
...linkedModpackOwner.value,
|
||||
link: () =>
|
||||
openUrl(
|
||||
`https://modrinth.com/${linkedModpackOwner.value!.type}/${linkedModpackOwner.value!.id}`,
|
||||
),
|
||||
link: contentOwnerLink(linkedModpackOwner.value),
|
||||
}
|
||||
: undefined,
|
||||
categories: linkedModpackCategories.value,
|
||||
@@ -1491,7 +1495,7 @@ provideContentManager({
|
||||
owner: item.owner
|
||||
? {
|
||||
...item.owner,
|
||||
link: () => openUrl(`https://modrinth.com/${item.owner!.type}/${item.owner!.id}`),
|
||||
link: contentOwnerLink(item.owner),
|
||||
}
|
||||
: undefined,
|
||||
enabled: canMutateContent(item) ? item.enabled : undefined,
|
||||
|
||||
@@ -138,7 +138,6 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
|
||||
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
|
||||
@@ -342,9 +341,7 @@ function removeMember(row: ShareRow) {
|
||||
members.remove(row.id)
|
||||
}
|
||||
function userProfileLink(username: string) {
|
||||
return !username || username.includes('@')
|
||||
? undefined
|
||||
: () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
|
||||
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
|
||||
}
|
||||
async function requestAuth(flow: ModrinthAuthFlow) {
|
||||
await auth.requestSignIn(`/instance/${encodeURIComponent(props.instance.id)}/share`, flow, {
|
||||
|
||||
@@ -157,7 +157,6 @@ import {
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -294,9 +293,7 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
function userProfileLink(username: string) {
|
||||
return !username || username.includes('@')
|
||||
? undefined
|
||||
: () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
|
||||
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
|
||||
}
|
||||
function setUsernameRef(id: string, element: Element | null) {
|
||||
usernameRefs.value[id] = element instanceof HTMLElement ? element : null
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
:organization="organization"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
:user-link="(username) => `/user/${encodeURIComponent(username)}`"
|
||||
link-target="_blank"
|
||||
:user-link-target="null"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
|
||||
@@ -475,7 +475,7 @@ export function createContentInstall(opts: {
|
||||
name: owner.user.username,
|
||||
iconUrl: owner.user.avatar_url,
|
||||
circle: true,
|
||||
link: () => openUrl(`https://modrinth.com/user/${owner.user.username}`),
|
||||
link: `/user/${encodeURIComponent(owner.user.username)}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
|
||||
id: ownerId,
|
||||
name: project.author,
|
||||
type: 'user' as const,
|
||||
link: `https://modrinth.com/user/${ownerId}`,
|
||||
link: `/user/${encodeURIComponent(ownerId)}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ async function getQueuedInstallOwner(
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `https://modrinth.com/user/${owner.username}`,
|
||||
link: `/user/${encodeURIComponent(owner.username)}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -100,6 +100,14 @@ export default new createRouter({
|
||||
breadcrumb: [{ name: 'Skin selector' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user/:user/:projectType?',
|
||||
name: 'User',
|
||||
component: Pages.User,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?User' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: 'Library',
|
||||
|
||||
Reference in New Issue
Block a user