fix: pre-release shared instances fixes (#6856)

* fix: dedupe notifications

* fix: notifs + members btn robustness

* fix: temp fix for external file wrongly showing updated

* fix: fmt
This commit is contained in:
Calum H.
2026-07-24 16:47:08 +00:00
committed by GitHub
parent cd56b98975
commit fbe70c6938
14 changed files with 179 additions and 34 deletions
+24 -2
View File
@@ -290,6 +290,9 @@ const {
const news = ref([])
const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const serverInvitePopupNotificationIds = new Set()
let liveNotificationGeneration = 0
let liveNotificationsEnabled = true
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -786,16 +789,19 @@ async function validateSession(sessionToken) {
}
async function fetchCredentials() {
const hadSession = !!credentials.value?.session
const refreshId = ++credentialsRefreshId
credentials.value = undefined
const creds = await getCreds().catch(handleError)
if (refreshId !== credentialsRefreshId) return
if (!creds && hadSession) clearLiveNotifications()
if (creds && creds.user_id) {
if (creds.session && !(await validateSession(creds.session))) {
if (refreshId !== credentialsRefreshId) return
clearLiveNotifications()
await logout().catch(handleError)
if (refreshId !== credentialsRefreshId) return
@@ -806,6 +812,7 @@ async function fetchCredentials() {
if (refreshId !== credentialsRefreshId) return
}
credentials.value = creds ?? null
liveNotificationsEnabled = !!creds?.session
}
async function signIn(flow = 'sign-in') {
@@ -841,6 +848,7 @@ async function logOut() {
async function performLogOut() {
credentialsRefreshId++
credentials.value = undefined
clearLiveNotifications()
await logout().catch(handleError)
await fetchCredentials()
@@ -962,12 +970,13 @@ function openServerInviteInviterProfile(inviterName) {
}
async function handleLiveNotification(notification) {
if (!notification?.body || notification.read) return
if (!liveNotificationsEnabled || !notification?.body || notification.read) return
if (await sharedInstanceInviteHandler.value?.handleNotification(notification)) return
if (notification.body.type === 'server_invite') {
if (displayedServerInviteNotifications.has(notification.id)) return
const generation = liveNotificationGeneration
displayedServerInviteNotifications.add(notification.id)
const serverName =
@@ -975,8 +984,9 @@ async function handleLiveNotification(notification) {
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
if (generation !== liveNotificationGeneration) return
addPopupNotification({
const popupNotification = addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
@@ -989,9 +999,21 @@ async function handleLiveNotification(notification) {
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
serverInvitePopupNotificationIds.add(popupNotification.id)
}
}
function clearLiveNotifications() {
liveNotificationGeneration++
liveNotificationsEnabled = false
for (const id of serverInvitePopupNotificationIds) {
popupNotificationManager.removeNotification(id)
}
displayedServerInviteNotifications.clear()
serverInvitePopupNotificationIds.clear()
sharedInstanceInviteHandler.value?.clearNotifications()
}
async function handleCommand(e) {
if (!e) return
@@ -56,16 +56,19 @@ const { formatMessage } = useVIntl()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const diffs = computed<ContentDiffItem[]>(
() =>
preview.value?.diffs.map((diff) => ({
type: diff.type,
projectName: diff.projectName ?? undefined,
fileName: diff.fileName ?? undefined,
currentVersionName: diff.currentVersionName ?? undefined,
newVersionName: diff.newVersionName ?? undefined,
fileCount: diff.configFileCount ?? undefined,
disabled: diff.disabled,
external: diff.type === 'added' && !diff.projectId && !!diff.fileName,
})) ?? [],
preview.value?.diffs
// TODO: This is TEMP!!! Hashing needs to be done on backend
.filter((diff) => !(diff.type === 'updated' && !diff.projectId && diff.fileName))
.map((diff) => ({
type: diff.type,
projectName: diff.projectName ?? undefined,
fileName: diff.fileName ?? undefined,
currentVersionName: diff.currentVersionName ?? undefined,
newVersionName: diff.newVersionName ?? undefined,
fileCount: diff.configFileCount ?? undefined,
disabled: diff.disabled,
external: diff.type === 'added' && !diff.projectId && !!diff.fileName,
})) ?? [],
)
async function update() {
@@ -28,6 +28,7 @@ const accountRequiredModal = ref<InstanceType<typeof ModrinthAccountRequiredModa
const {
handleNotification,
installFromInviteId,
clearNotifications,
handleAlreadyInstalledCancel,
handleAlreadyInstalledGoToInstance,
handleAlreadyInstalledInstallAnyway,
@@ -42,5 +43,6 @@ async function requestAuth(flow: ModrinthAuthFlow) {
defineExpose<SharedInstanceInviteHandler>({
handleNotification,
installFromInviteId,
clearNotifications,
})
</script>
@@ -26,4 +26,5 @@ export type SharedInstanceInvite = {
export type SharedInstanceInviteHandler = {
handleNotification(notification: AppNotification): Promise<boolean>
installFromInviteId(inviteId: string): Promise<void>
clearNotifications(): void
}
@@ -47,11 +47,14 @@ export function useSharedInstanceInviteHandler(
const auth = injectAuth()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const { addPopupNotification } = injectPopupNotificationManager()
const popupNotificationManager = injectPopupNotificationManager()
const queryClient = useQueryClient()
const router = useRouter()
const themeStore = useTheming()
const displayedNotifications = new Set<string | number>()
const displayedNotificationKeys = new Set<string>()
const popupNotificationIds = new Set<string | number>()
let notificationGeneration = 0
let pendingAlreadyInstalled:
| {
instanceId: string
@@ -183,9 +186,23 @@ export function useSharedInstanceInviteHandler(
if (!parsedInvite) return false
if (displayedNotifications.has(notification.id)) return true
const generation = notificationGeneration
displayedNotifications.add(notification.id)
const invite = await resolveInvite(parsedInvite)
addPopupNotification({
if (generation !== notificationGeneration) return true
const notificationKey = JSON.stringify([
invite.invitedById ?? invite.invitedByUsername,
invite.sharedInstanceName,
invite.instanceIconUrl,
])
if (displayedNotificationKeys.has(notificationKey)) {
await markNotificationRead(notification).catch((error) => handleError(toError(error)))
return true
}
displayedNotificationKeys.add(notificationKey)
const popupNotification = popupNotificationManager.addPopupNotification({
title: invite.sharedInstanceName,
autoCloseMs: null,
toast: {
@@ -204,9 +221,21 @@ export function useSharedInstanceInviteHandler(
},
},
})
popupNotificationIds.add(popupNotification.id)
return true
}
function clearNotifications() {
notificationGeneration++
for (const id of popupNotificationIds) {
popupNotificationManager.removeNotification(id)
}
displayedNotifications.clear()
displayedNotificationKeys.clear()
popupNotificationIds.clear()
pendingAlreadyInstalled = undefined
}
async function requireAccount() {
if (!auth.isReady?.value) {
await new Promise<void>((resolve) => {
@@ -245,6 +274,7 @@ export function useSharedInstanceInviteHandler(
return {
handleNotification,
installFromInviteId,
clearNotifications,
handleAlreadyInstalledCancel,
handleAlreadyInstalledGoToInstance,
handleAlreadyInstalledInstallAnyway,
@@ -11,6 +11,7 @@
:link-max-uses="inviteLink.details.value?.maxUses"
:update-invite-link="inviteLink.update"
:user-profile-link="userProfileLink"
:can-invite="!members.exclusiveMutationPending.value && !inviteLink.pending.value"
@invite="invitePlayer"
@cancel="cancelInvite"
/>
@@ -1,6 +1,6 @@
import type { InvitePlayersUser } from '@modrinth/ui'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type Ref } from 'vue'
import { computed, type Ref, ref } from 'vue'
import { get_user_many } from '@/helpers/cache.js'
import {
@@ -26,12 +26,14 @@ type OptimisticChange = {
type InviteVariables = {
user: InvitePlayersUser
change: OptimisticChange
exclusive: boolean
}
type RemoveVariables = {
id: string
hasPendingRecipients: boolean
change: OptimisticChange
exclusive: boolean
}
export function useSharedInstanceMembers(options: {
@@ -45,6 +47,7 @@ export function useSharedInstanceMembers(options: {
const queryKey = computed(() => ['sharedInstanceUsers', options.instance.value.id] as const)
const invitingUserIds = new Set<string>()
const removingUserIds = new Set<string>()
const exclusiveMutationPending = ref(false)
const query = useQuery({
queryKey,
@@ -65,8 +68,9 @@ export function useSharedInstanceMembers(options: {
rollback(change)
options.onError(error)
},
onSettled: (_data, _error, { user }) => {
onSettled: (_data, _error, { user, exclusive }) => {
invitingUserIds.delete(normalizeInviteKey(user.id))
if (exclusive) exclusiveMutationPending.value = false
},
})
@@ -77,8 +81,9 @@ export function useSharedInstanceMembers(options: {
rollback(change)
options.onError(error)
},
onSettled: (_data, _error, { id }) => {
onSettled: (_data, _error, { id, exclusive }) => {
removingUserIds.delete(normalizeInviteKey(id))
if (exclusive) exclusiveMutationPending.value = false
},
})
@@ -133,23 +138,34 @@ export function useSharedInstanceMembers(options: {
const normalizedId = normalizeInviteKey(user.id)
if (
options.actionsLocked.value ||
exclusiveMutationPending.value ||
invitingUserIds.has(normalizedId) ||
find(user.id, user.username)
) {
return
}
const exclusive = rows.value.length === 0
invitingUserIds.add(normalizedId)
if (exclusive) exclusiveMutationPending.value = true
const change = beginOptimisticChange(user.id)
updateRows(change.queryKey, (currentRows) => [...currentRows, inviteUserToRow(user)])
inviteMutation.mutate({ user, change })
inviteMutation.mutate({ user, change, exclusive })
}
function remove(id: string) {
const normalizedId = normalizeInviteKey(id)
if (options.actionsLocked.value || removingUserIds.has(normalizedId)) return
if (
options.actionsLocked.value ||
exclusiveMutationPending.value ||
removingUserIds.has(normalizedId)
) {
return
}
const exclusive = rows.value.length === 1
removingUserIds.add(normalizedId)
if (exclusive) exclusiveMutationPending.value = true
const hasPendingRecipients = rows.value.some(
(row) => row.pending && normalizeInviteKey(row.id) !== normalizedId,
)
@@ -157,7 +173,7 @@ export function useSharedInstanceMembers(options: {
updateRows(change.queryKey, (currentRows) =>
currentRows.filter((row) => normalizeInviteKey(row.id) !== normalizedId),
)
removeMutation.mutate({ id, hasPendingRecipients, change })
removeMutation.mutate({ id, hasPendingRecipients, change, exclusive })
}
function beginOptimisticChange(userId: string): OptimisticChange {
@@ -197,7 +213,7 @@ export function useSharedInstanceMembers(options: {
queryClient.setQueryData<ShareRow[]>(activeQueryKey, (currentRows = []) => update(currentRows))
}
return { rows, query, find, invite, remove }
return { rows, query, exclusiveMutationPending, find, invite, remove }
}
function userEntries(users: SharedInstanceUsers): SharedInstanceUser[] {