From 1e49d7da7a141e5d9e5790e0264003824b56705c Mon Sep 17 00:00:00 2001 From: ThatGravyBoat Date: Sun, 2 Aug 2026 19:53:03 -0230 Subject: [PATCH 01/28] fix: missing auth providers in account details (#6965) * fix: missing auth providers in account details This was missing due to a bad merge in #6889 not merging in #6897 correctly * chore: run intl:extract --- .../layouts/shared/user-profile/layout.vue | 112 +++++++++++++++++- packages/ui/src/locales/en-US/index.json | 15 +++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/layouts/shared/user-profile/layout.vue b/packages/ui/src/layouts/shared/user-profile/layout.vue index be4f1644b9..9c3b008a6b 100644 --- a/packages/ui/src/layouts/shared/user-profile/layout.vue +++ b/packages/ui/src/layouts/shared/user-profile/layout.vue @@ -111,7 +111,33 @@ {{ formatMessage(messages.authProvidersLabel) }} - {{ user.auth_providers?.join(', ') || '—' }} +
@@ -460,7 +486,13 @@ import ProjectCardList from '#ui/components/project/ProjectCardList.vue' import UserBadges from '#ui/components/user/UserBadges.vue' import UserPageHeader from '#ui/components/user/UserPageHeader.vue' import { defineMessages, useVIntl } from '#ui/composables' -import { injectAuth, injectNotificationManager, injectPageContext, injectTags } from '#ui/providers' +import { + injectAuth, + injectModrinthClient, + injectNotificationManager, + injectPageContext, + injectTags, +} from '#ui/providers' import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils' import { blockedUsersQueryKey, injectUserProfile } from './providers' @@ -520,6 +552,7 @@ const auth = injectAuth() const tags = injectTags(null) const pageContext = injectPageContext() const notificationManager = injectNotificationManager() +const client = injectModrinthClient() const queryClient = useQueryClient() const route = useRoute() const router = useRouter() @@ -567,6 +600,26 @@ const messages = defineMessages({ id: 'profile.details.label.auth-providers', defaultMessage: 'Auth providers', }, + viewGithubProfileLabel: { + id: 'profile.details.label.view-github-profile', + defaultMessage: 'View profile', + }, + loadingGithubProfileLabel: { + id: 'profile.details.label.loading-github-profile', + defaultMessage: 'Loading...', + }, + githubProfileErrorTitle: { + id: 'profile.details.error.github-profile-title', + defaultMessage: 'Unable to open GitHub profile', + }, + githubProfileErrorMessage: { + id: 'profile.details.error.github-profile-message', + defaultMessage: 'The GitHub profile could not be retrieved. Please try again.', + }, + githubPopupBlockedMessage: { + id: 'profile.details.error.github-popup-blocked', + defaultMessage: 'Allow pop-ups for Modrinth, then try again.', + }, paymentMethodsLabel: { id: 'profile.details.label.payment-methods', defaultMessage: 'Payment methods', @@ -853,6 +906,17 @@ const showCollectionsEmptyState = computed( const normalizedSiteUrl = computed(() => props.siteUrl.replace(/\/$/, '')) const editProfileLink = computed(() => props.editProfileLink ?? linkTarget('/settings/profile')) +const authProviderNames = { + github: 'GitHub', + discord: 'Discord', + microsoft: 'Microsoft', + gitlab: 'GitLab', + google: 'Google', + steam: 'Steam', + paypal: 'PayPal', +} +const isLoadingGithubProfile = ref(false) + function externalUrl(path: string): string { return `${normalizedSiteUrl.value}${path.startsWith('/') ? path : `/${path}`}` } @@ -896,6 +960,50 @@ async function copyPermalink(): Promise { } } +async function openGithubProfile() { + const githubId = user.value?.github_id + if (!githubId || isLoadingGithubProfile.value) return + + const profileWindow = window.open('about:blank', '_blank') + if (!profileWindow) { + notificationManager.addNotification({ + type: 'error', + title: formatMessage(messages.githubProfileErrorTitle), + text: formatMessage(messages.githubPopupBlockedMessage), + }) + return + } + + profileWindow.opener = null + isLoadingGithubProfile.value = true + + try { + const githubUser = await client.request<{ login?: string }>(`/${githubId}`, { + api: 'https://api.github.com', + version: 'user', + method: 'GET', + headers: { 'Content-Type': '' }, + skipAuth: true, + }) + + if (!githubUser?.login) { + throw new Error('GitHub user response did not include a login') + } + + profileWindow.location.replace(`https://github.com/${encodeURIComponent(githubUser.login)}`) + } catch (error) { + profileWindow.close() + console.error('Failed to retrieve GitHub profile:', error) + notificationManager.addNotification({ + type: 'error', + title: formatMessage(messages.githubProfileErrorTitle), + text: formatMessage(messages.githubProfileErrorMessage), + }) + } finally { + isLoadingGithubProfile.value = false + } +} + function reportProfile(): void { if (!user.value) return const reportPath = `/report?item=user&itemID=${encodeURIComponent(user.value.id)}` diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index e1c3941001..f842c47c3d 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -2921,6 +2921,15 @@ "profile.collection.projects-count": { "defaultMessage": "{count, plural, one {# project} other {# projects}}" }, + "profile.details.error.github-popup-blocked": { + "defaultMessage": "Allow pop-ups for Modrinth, then try again." + }, + "profile.details.error.github-profile-message": { + "defaultMessage": "The GitHub profile could not be retrieved. Please try again." + }, + "profile.details.error.github-profile-title": { + "defaultMessage": "Unable to open GitHub profile" + }, "profile.details.label.auth-providers": { "defaultMessage": "Auth providers" }, @@ -2933,9 +2942,15 @@ "profile.details.label.has-totp": { "defaultMessage": "Has TOTP" }, + "profile.details.label.loading-github-profile": { + "defaultMessage": "Loading..." + }, "profile.details.label.payment-methods": { "defaultMessage": "Payment methods" }, + "profile.details.label.view-github-profile": { + "defaultMessage": "View profile" + }, "profile.details.title": { "defaultMessage": "User details" }, From ae13d37edc2487fd0ebfd0f9c9aa096a63a99386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?coco=20=F0=9F=90=BE?= <44563370+cocoelacanth@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:49:19 -0700 Subject: [PATCH 02/28] docs: corrections to GetLatestVersionFromHash and GetLatestVersionsFromHashes documentation (#6966) correct version_files documentation --- apps/docs/public/openapi.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/docs/public/openapi.yaml b/apps/docs/public/openapi.yaml index be4c40ede2..0e098712ea 100644 --- a/apps/docs/public/openapi.yaml +++ b/apps/docs/public/openapi.yaml @@ -563,12 +563,18 @@ components: type: array items: type: string - example: [fabric] + example: [fabric] game_versions: type: array items: type: string example: ['1.18', 1.18.1] + version_types: + type: array + items: + type: string + enum: [release, alpha, beta] + example: [release] required: - loaders - game_versions @@ -612,6 +618,12 @@ components: items: type: string example: ['1.18', 1.18.1] + version_types: + type: array + items: + type: string + enum: [release, alpha, beta] + example: [release] required: - loaders - game_versions @@ -2865,7 +2877,7 @@ paths: $ref: '#/components/schemas/HashList' /version_files/update: post: - summary: Latest versions of multiple project from hashes, loader(s), and game version(s) + summary: Latest versions of multiple projects from hashes, loader(s), and game version(s) description: This is the same as [`/version_file/{hash}/update`](#operation/getLatestVersionFromHash) except it accepts multiple hashes. operationId: getLatestVersionsFromHashes tags: From 779edf03337114da11a2521776bbf364f4f51848 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:00:54 -0700 Subject: [PATCH 03/28] Fix server ping race condition before protocol version is available (#6935) * fix race condition with server ping lookup before protocol resolved * fix refresh button on worlds page --- apps/app-frontend/src/helpers/worlds.ts | 16 ++++-- .../app-frontend/src/locales/en-US/index.json | 3 ++ .../src/pages/instance/Worlds.vue | 54 ++++++++++++++++--- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/apps/app-frontend/src/helpers/worlds.ts b/apps/app-frontend/src/helpers/worlds.ts index 3b5285f51b..5eb3385497 100644 --- a/apps/app-frontend/src/helpers/worlds.ts +++ b/apps/app-frontend/src/helpers/worlds.ts @@ -435,11 +435,12 @@ export async function refreshServerData( } } -export function refreshServers( +export async function refreshServers( worlds: World[], serverData: Record, protocolVersion: ProtocolVersion | null, -) { + ping = true, +): Promise { const servers = worlds.filter(isServerWorld) servers.forEach((server) => { if (!serverData[server.address]) { @@ -451,9 +452,14 @@ export function refreshServers( } }) - // noinspection ES6MissingAwait - handled by refreshServerData - Object.keys(serverData).forEach((address) => - refreshServerData(serverData[address], protocolVersion, address), + if (!ping) { + return + } + + await Promise.all( + Object.keys(serverData).map((address) => + refreshServerData(serverData[address], protocolVersion, address), + ), ) } diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 91ff2a64ff..475e1027ca 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -590,6 +590,9 @@ "app.instance.worlds.no-worlds-heading": { "message": "No servers or worlds added" }, + "app.instance.worlds.refreshing": { + "message": "Refreshing..." + }, "app.instance.worlds.remove-server-modal.remove-button": { "message": "Remove server" }, diff --git a/apps/app-frontend/src/pages/instance/Worlds.vue b/apps/app-frontend/src/pages/instance/Worlds.vue index 1ddcb5e943..4a5bd6192d 100644 --- a/apps/app-frontend/src/pages/instance/Worlds.vue +++ b/apps/app-frontend/src/pages/instance/Worlds.vue @@ -75,7 +75,11 @@
@@ -242,6 +246,10 @@ const messages = defineMessages({ id: 'app.instance.worlds.filter-offline', defaultMessage: 'Offline', }, + refreshingButton: { + id: 'app.instance.worlds.refreshing', + defaultMessage: 'Refreshing...', + }, }) const { formatMessage } = useVIntl() @@ -328,7 +336,7 @@ const isLinux = platform() === 'linux' const linuxRefreshCount = ref(0) const protocolVersion = ref(null) - +const protocolVersionReady = ref(false) const gameVersions = ref([]) const supportsServerQuickPlay = computed(() => hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version), @@ -342,8 +350,16 @@ watch( (data) => { if (data) { worlds.value = [...data] - refreshServers(worlds.value, serverData.value, protocolVersion.value) hadNoWorlds.value = worlds.value.length === 0 + // Manual refresh handles its own server pings to avoid double-pinging + if (!refreshingAll.value) { + void refreshServers( + worlds.value, + serverData.value, + protocolVersion.value, + protocolVersionReady.value, + ) + } } }, { immediate: true }, @@ -443,9 +459,14 @@ async function initWorldsTab() { unlistenInstance = _unlistenInstance protocolVersion.value = resolvedProtocolVersion gameVersions.value = resolvedGameVersions + protocolVersionReady.value = true + + if (worlds.value.length > 0) { + refreshServers(worlds.value, serverData.value, protocolVersion.value) + } } -await initWorldsTab() +void initWorldsTab() async function refreshServer(address: string) { if (!serverData.value[address]) { @@ -453,6 +474,7 @@ async function refreshServer(address: string) { refreshing: true, } } + if (!protocolVersionReady.value) return await refreshServerData(serverData.value[address], protocolVersion.value, address) } @@ -463,8 +485,28 @@ async function refreshAllWorlds() { } refreshingAll.value = true - await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.id] }) - refreshingAll.value = false + try { + // Show loading on server rows immediately while the list refreshes + for (const world of worlds.value) { + if (world.type === 'server') { + if (!serverData.value[world.address]) { + serverData.value[world.address] = { refreshing: true } + } else { + serverData.value[world.address].refreshing = true + } + } + } + + await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.id] }) + await refreshServers( + worlds.value, + serverData.value, + protocolVersion.value, + protocolVersionReady.value, + ) + } finally { + refreshingAll.value = false + } } async function addServer(server: ServerWorld) { From 2e43f6a42bd10cbc31cc3ccc704e2b11674b4ef3 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:03:45 -0700 Subject: [PATCH 04/28] collapse offline friends by default, save collapse value (#6937) --- .../src/components/ui/friends/FriendsList.vue | 38 +++++++++++++++++-- .../components/ui/friends/FriendsSection.vue | 7 ++++ apps/app-frontend/src/store/theme.ts | 4 ++ packages/app-lib/src/state/settings.rs | 4 ++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/components/ui/friends/FriendsList.vue index 71c8c1c8fe..1b954325ac 100644 --- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue +++ b/apps/app-frontend/src/components/ui/friends/FriendsList.vue @@ -17,17 +17,40 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { useFriends } from '@/composables/use-friends' import type { FriendWithUserData } from '@/helpers/friends.ts' import type { ModrinthCredentials } from '@/helpers/mr_auth' +import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' +import { useTheming } from '@/store/state' const { formatMessage } = useVIntl() const { handleError } = injectNotificationManager() const formatRelativeTime = useRelativeTime() +const themeStore = useTheming() const props = defineProps<{ credentials: ModrinthCredentials | null signIn: () => void }>() +type FriendsSectionCollapsedFlag = + | 'friends_active_collapsed' + | 'friends_online_collapsed' + | 'friends_offline_collapsed' + | 'friends_pending_collapsed' + +function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) { + return themeStore.getFeatureFlag(flag) +} + +function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) { + themeStore.featureFlags[flag] = collapsed + getSettings() + .then((settings) => { + settings.feature_flags[flag] = collapsed + return setSettings(settings) + }) + .catch(handleError) +} + const userCredentials = computed(() => props.credentials) const { friends: userFriends, @@ -331,33 +354,42 @@ const messages = defineMessages({

{{ formatMessage(messages.noFriendsMatch, { query: search }) }} diff --git a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue index f9efeb79e6..ec453e6c38 100644 --- a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue +++ b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue @@ -31,6 +31,11 @@ const props = withDefaults( }, ) +const emit = defineEmits<{ + onOpen: [] + onClose: [] +}>() + function createContextMenuOptions(friend: FriendWithUserData) { if (friend.accepted) { return [ @@ -112,6 +117,8 @@ const messages = defineMessages({ ? '' : ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all') " + @on-open="emit('onOpen')" + @on-close="emit('onClose')" >

- - - +
@@ -95,8 +97,9 @@ import { import { Accordion, Avatar, - ButtonStyled, + Button, defineMessages, + IconButton, injectNotificationManager, useVIntl, } from '@modrinth/ui' diff --git a/apps/app-frontend/src/components/ui/AddContentButton.vue b/apps/app-frontend/src/components/ui/AddContentButton.vue index cdea116e87..65f6397a9e 100644 --- a/apps/app-frontend/src/components/ui/AddContentButton.vue +++ b/apps/app-frontend/src/components/ui/AddContentButton.vue @@ -1,6 +1,6 @@ diff --git a/apps/frontend/src/components/ui/NotificationItem.vue b/apps/frontend/src/components/ui/NotificationItem.vue index 1bbac8bf38..4f3cfa001b 100644 --- a/apps/frontend/src/components/ui/NotificationItem.vue +++ b/apps/frontend/src/components/ui/NotificationItem.vue @@ -46,18 +46,18 @@ class="flex flex-wrap items-center gap-3" :class="{ 'gap-2': compact }" > - - - - - - + +
+ + + +
+
+
+ - - - -
-
-
- - - - +
- - - - Open link - - - - - - - - + + + Open link + + +
@@ -390,10 +399,12 @@ import { } from '@modrinth/assets' import { Avatar, - ButtonStyled, + Button, + ButtonLink, Categories, CopyCode, DoubleIcon, + IconButton, injectModrinthClient, injectNotificationManager, ProjectStatusBadge, diff --git a/apps/frontend/src/components/ui/OrganizationPageHeader.vue b/apps/frontend/src/components/ui/OrganizationPageHeader.vue index c5f74542df..6063423a50 100644 --- a/apps/frontend/src/components/ui/OrganizationPageHeader.vue +++ b/apps/frontend/src/components/ui/OrganizationPageHeader.vue @@ -38,21 +38,19 @@ @@ -68,18 +66,17 @@ import { SettingsIcon, UsersIcon, } from '@modrinth/assets' +import { ButtonLink, TeleportOverflowMenu } from '@modrinth/ui' import { Avatar, - ButtonStyled, commonMessages, defineMessages, + type OverflowMenuOption, PageHeader, PageHeaderActions, PageHeaderBadgeItem, PageHeaderMetadata, PageHeaderMetadataNumberItem, - TeleportOverflowMenu, - type TeleportOverflowMenuItem, useFormatNumber, useVIntl, } from '@modrinth/ui' @@ -135,7 +132,7 @@ const emit = defineEmits<{ const { formatMessage } = useVIntl() const formatNumber = useFormatNumber() -const moreActions = computed(() => [ +const moreActions = computed(() => [ { id: 'manage-projects', label: formatMessage(messages.manageProjects), @@ -143,10 +140,7 @@ const moreActions = computed(() => [ action: () => emit('manageProjects'), shown: props.canManage, }, - { - divider: true, - shown: props.canManage, - }, + { type: 'divider', shown: props.canManage }, { id: 'copy-id', label: formatMessage(commonMessages.copyIdButton), diff --git a/apps/frontend/src/components/ui/OrganizationProjectTransferModal.vue b/apps/frontend/src/components/ui/OrganizationProjectTransferModal.vue index 6f66486fd0..46524dbff5 100644 --- a/apps/frontend/src/components/ui/OrganizationProjectTransferModal.vue +++ b/apps/frontend/src/components/ui/OrganizationProjectTransferModal.vue @@ -65,38 +65,37 @@
- - - +
- - - - diff --git a/packages/ui/src/components/base/ButtonStyled.vue b/packages/ui/src/components/base/ButtonStyled.vue deleted file mode 100644 index f546054c16..0000000000 --- a/packages/ui/src/components/base/ButtonStyled.vue +++ /dev/null @@ -1,397 +0,0 @@ - - - - - diff --git a/packages/ui/src/components/base/Card.vue b/packages/ui/src/components/base/Card.vue index f52f4533b0..c481f17eb0 100644 --- a/packages/ui/src/components/base/Card.vue +++ b/packages/ui/src/components/base/Card.vue @@ -2,7 +2,7 @@ import { DropdownIcon } from '@modrinth/assets' import { reactive } from 'vue' -import ButtonStyled from './ButtonStyled.vue' +import { IconButton } from '#ui/components/base/buttons' const props = defineProps({ collapsible: { @@ -33,11 +33,9 @@ function toggleCollapsed() {
- - - + + +
diff --git a/packages/ui/src/components/base/Chips.vue b/packages/ui/src/components/base/Chips.vue index e1359c469e..e7daddc930 100644 --- a/packages/ui/src/components/base/Chips.vue +++ b/packages/ui/src/components/base/Chips.vue @@ -15,7 +15,11 @@ }" @click="toggleItem(item)" > - +