Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)}`)
}
},
},
Expand Down
57 changes: 51 additions & 6 deletions apps/app-frontend/src/helpers/users.ts
Original file line number Diff line number Diff line change
@@ -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<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 search_user(query: string): Promise<SearchUser[]> {
return await invoke<SearchUser[]>('plugin:users|search_user', { query })
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 })
}
108 changes: 108 additions & 0 deletions apps/app-frontend/src/pages/User.vue
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 1 addition & 2 deletions apps/app-frontend/src/pages/hosting/manage/Access.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -29,7 +28,7 @@ try {
}

function userProfileLink(username: string) {
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
return `/user/${encodeURIComponent(username)}`
}
</script>

Expand Down
3 changes: 2 additions & 1 deletion apps/app-frontend/src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
14 changes: 9 additions & 5 deletions apps/app-frontend/src/pages/instance/Mods.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 1 addition & 4 deletions apps/app-frontend/src/pages/instance/share/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref } from 'vue'

import {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading