fix: better offline handling

This commit is contained in:
Calum H. (IMB11)
2026-07-27 15:39:52 +01:00
parent c8b21fe120
commit 08bb4de9dc
12 changed files with 156 additions and 34 deletions
@@ -72,7 +72,6 @@ import {
ButtonStyled,
CopyCode,
defineMessages,
injectNotificationManager,
Table,
type TableColumn,
useFormatDateTime,
@@ -86,10 +85,11 @@ import ConfirmRevokeSharedInstanceInviteModal from '@/components/ui/shared-insta
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
import { config } from '@/config'
import { type SharedInstanceInvite, unpublish_shared_instance } from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import { injectInstanceSettings } from '@/providers/instance-settings'
const { instance, offline, onUnlinked } = injectInstanceSettings()
const { handleError } = injectNotificationManager()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const unpublishing = ref(false)
@@ -162,7 +162,7 @@ async function unpublishSharedInstance() {
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
onUnlinked()
} catch (error) {
handleError(error)
notifySharedInstanceError(error)
} finally {
unpublishing.value = false
}
@@ -19,6 +19,7 @@ import {
install_shared_instance,
} from '@/helpers/install'
import { list } from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import { useTheming } from '@/store/state'
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
@@ -47,6 +48,8 @@ export function useSharedInstanceInviteHandler(
const auth = injectAuth()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const { notifySharedInstanceConnectionError, notifySharedInstanceError } =
useSharedInstanceErrors()
const popupNotificationManager = injectPopupNotificationManager()
const queryClient = useQueryClient()
const router = useRouter()
@@ -78,7 +81,10 @@ export function useSharedInstanceInviteHandler(
!invite.invitedByUsername && invite.invitedById
? get_user(invite.invitedById, 'bypass').catch(() => null)
: null,
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => null),
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => {
notifySharedInstanceConnectionError()
return null
}),
])
return {
@@ -177,7 +183,7 @@ export function useSharedInstanceInviteHandler(
() => markNotificationRead(notification),
)
} catch (error) {
handleError(toError(error))
notifySharedInstanceError(error)
}
}
@@ -267,7 +273,7 @@ export function useSharedInstanceInviteHandler(
await queryClient.invalidateQueries({ queryKey: ['instances'] })
})
} catch (error) {
handleError(toError(error))
notifySharedInstanceError(error)
}
}
+5
View File
@@ -102,6 +102,7 @@ export interface SharedInstanceUpdateDiff {
}
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
@@ -113,6 +114,10 @@ export function isSharedInstanceUnavailableError(error: unknown) {
return getSharedInstanceUnavailableReason(error) !== null
}
export function isSharedInstancesApiError(error: unknown) {
return isRecord(error) && error.code === SHARED_INSTANCES_API_ERROR_CODE
}
export function getSharedInstanceUnavailableReason(
error: unknown,
): SharedInstanceUnavailableReason | null {
@@ -1,6 +1,10 @@
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { getErrorMessage, type SharedInstanceUnavailableReason } from '@/helpers/install'
import {
getErrorMessage,
isSharedInstancesApiError,
type SharedInstanceUnavailableReason,
} from '@/helpers/install'
export const sharedInstanceErrorMessages = defineMessages({
unavailableTitle: {
@@ -39,6 +43,14 @@ export const sharedInstanceErrorMessages = defineMessages({
id: 'instance.shared-instance.error.title',
defaultMessage: 'Something has gone wrong',
},
networkErrorTitle: {
id: 'instance.shared-instance.network-error.title',
defaultMessage: 'Network error',
},
networkErrorText: {
id: 'instance.shared-instance.network-error.text',
defaultMessage: 'Unable to connect to shared instances API',
},
})
export function sharedInstanceUnavailableTextMessage(
@@ -82,7 +94,20 @@ export function useSharedInstanceErrors() {
})
}
function notifySharedInstanceConnectionError() {
addNotification({
type: 'error',
title: formatMessage(sharedInstanceErrorMessages.networkErrorTitle),
text: formatMessage(sharedInstanceErrorMessages.networkErrorText),
})
}
function notifySharedInstanceError(error: unknown) {
if (isSharedInstancesApiError(error)) {
notifySharedInstanceConnectionError()
return
}
addNotification({
type: 'error',
title: formatMessage(sharedInstanceErrorMessages.errorTitle),
@@ -92,6 +117,7 @@ export function useSharedInstanceErrors() {
return {
formatSharedInstanceUnavailable,
notifySharedInstanceConnectionError,
notifySharedInstanceError,
notifySharedInstanceUnavailable,
}
@@ -590,17 +590,16 @@ const startInstance = async (context: string) => {
!!instance.value.shared_instance && !sharedInstanceActionsLocked.value && !offline.value
if (canCheckSharedInstanceUpdate) {
let preview: Awaited<ReturnType<typeof refreshSharedInstanceUpdatePreview>>
let preview: Awaited<ReturnType<typeof refreshSharedInstanceUpdatePreview>> = null
checkingSharedInstanceLaunch.value = true
try {
preview = await refreshSharedInstanceUpdatePreview()
} catch (error) {
if (isSharedInstanceUnavailableError(error)) {
await handleSharedInstanceUnavailable(getSharedInstanceUnavailableReason(error))
} else {
notifySharedInstanceError(error)
return
}
return
notifySharedInstanceError(error)
} finally {
checkingSharedInstanceLaunch.value = false
}
@@ -707,7 +706,7 @@ async function reportSharedInstance(event?: MouseEvent, closeUpdateModal = false
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
sharedInstanceReportModal.value?.showReport(preview, event)
} catch (error) {
handleError(error as Error)
notifySharedInstanceError(error)
}
}
@@ -47,7 +47,13 @@
@state-change="publishState = $event"
/>
<div v-if="membersTableLoading" class="h-64" aria-hidden="true" />
<SharedInstanceShareEmptyState
v-if="unableToConnect"
:heading="formatMessage(messages.unableToConnectHeading)"
:description="formatMessage(messages.unableToConnectDescription)"
/>
<div v-else-if="membersTableLoading" class="h-64" aria-hidden="true" />
<SharedInstanceMembersTable
v-else-if="showMembersTable"
@@ -150,7 +156,7 @@ import {
type InvitePlayersUser,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref, toRef, watch } from 'vue'
@@ -158,9 +164,10 @@ import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountR
import SharedInstancePublishModal from '@/components/ui/shared-instances/SharedInstancePublishModal.vue'
import {
getSharedInstanceUnavailableReason,
isSharedInstancesApiError,
isSharedInstanceUnavailableError,
} from '@/helpers/install'
import { edit } from '@/helpers/instance'
import { can_current_user_use_shared_instances, edit } from '@/helpers/instance'
import type { ModrinthAuthFlow } from '@/helpers/mr_auth.ts'
import {
sharedInstanceErrorMessages,
@@ -196,6 +203,7 @@ const actionsLocked = sharedInstanceState.shareActionsLocked
const sharedInstanceActionsLocked = actionsLocked
const currentUserId = computed(() => auth.user.value?.id ?? null)
const isSignedIn = computed(() => !!auth.session_token.value)
const sharedInstancesApiUnavailable = ref(false)
const accountRequiredModal = ref<InstanceType<typeof ModrinthAccountRequiredModal>>()
const invitePlayersModal = ref<InstanceType<typeof InvitePlayersModal>>()
const unlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
@@ -212,10 +220,22 @@ function notifyOperationError(error: unknown) {
sharedInstanceState.unavailableManager.value,
)
} else {
if (isSharedInstancesApiError(error)) sharedInstancesApiUnavailable.value = true
notifySharedInstanceError(error)
}
}
const eligibilityQuery = useQuery({
queryKey: computed(() => ['shared-instance-eligibility', currentUserId.value]),
queryFn: can_current_user_use_shared_instances,
enabled: () => isSignedIn.value && !!currentUserId.value,
retry: false,
staleTime: Infinity,
refetchOnMount: 'always',
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
const members = useSharedInstanceMembers({
instance,
currentUserId,
@@ -258,6 +278,12 @@ const lockedActionButton = computed(() =>
const sharedInstanceUnavailableReason = sharedInstanceState.unavailableReason
const sharedInstanceUnavailable = computed(() => !!sharedInstanceUnavailableReason.value)
const sharedInstanceUnavailableManager = sharedInstanceState.unavailableManager
const unableToConnect = computed(
() =>
sharedInstancesApiUnavailable.value ||
isSharedInstancesApiError(eligibilityQuery.error.value) ||
isSharedInstancesApiError(members.query.error.value),
)
const membersTableLoading = computed(
() =>
members.rows.value.length === 0 &&
@@ -289,6 +315,15 @@ const importedModpackBackupTip = computed(() =>
const messages = defineMessages({
signInButton: { id: 'app.instance.share.sign-in.button', defaultMessage: 'Sign in' },
unableToConnectHeading: {
id: 'app.instance.share.unable-to-connect.heading',
defaultMessage: 'Unable to connect',
},
unableToConnectDescription: {
id: 'app.instance.share.unable-to-connect.description',
defaultMessage:
'The shared instances service is not accessible at the moment, please try again later',
},
noFriendsInvitedHeading: {
id: 'app.instance.share.empty.heading',
defaultMessage: 'No friends invited',
@@ -396,6 +431,23 @@ function signInToShare(event?: MouseEvent) {
void accountRequiredModal.value?.show(event)
}
watch(
[eligibilityQuery.error, members.query.error],
(errors) => {
for (const error of errors) {
if (isSharedInstancesApiError(error)) notifyOperationError(error)
}
},
{ immediate: true },
)
watch(
[eligibilityQuery.data, members.query.data],
([eligibility, memberRows]) => {
if (eligibility !== undefined && memberRows !== undefined) {
sharedInstancesApiUnavailable.value = false
}
},
)
watch(
() => props.instance.id,
() => {
@@ -58,6 +58,7 @@ export function useSharedInstanceMembers(options: {
queryFn: ({ queryKey }) => fetchRows(queryKey),
enabled: () =>
options.isSignedIn.value && !!options.instance.value.id && !options.actionsLocked.value,
retry: false,
staleTime: Infinity,
refetchOnMount: 'always',
refetchOnReconnect: false,