import { ModrinthApiError } from '@modrinth/api-client' import { injectAuth, injectModrinthClient, injectNotificationManager, injectPopupNotificationManager, } from '@modrinth/ui' import { useQueryClient } from '@tanstack/vue-query' import { type Ref, watch } from 'vue' import { useRouter } from 'vue-router' import { get_user } from '@/helpers/cache' import { toError } from '@/helpers/errors' import { install_accept_shared_instance_invite, install_get_shared_instance_preview, 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' import type { AppNotification, SharedInstanceInvite } from './shared-instance-invite-types' type InstallModal = { show( preview: Awaited>, install: () => Promise, creator?: SharedInstanceCreator, ): void } type SharedInstanceCreator = { id: string | null username: string avatarUrl: string | null } type AccountRequiredModal = { show(event?: MouseEvent): Promise } type AlreadyInstalledModal = { show(instanceName: string): void } export function useSharedInstanceInviteHandler( installModal: Ref, alreadyInstalledModal: Ref, accountRequiredModal: Ref, ) { const auth = injectAuth() const client = injectModrinthClient() const { handleError } = injectNotificationManager() const { notifySharedInstanceConnectionError, notifySharedInstanceError } = useSharedInstanceErrors() const popupNotificationManager = injectPopupNotificationManager() const queryClient = useQueryClient() const router = useRouter() const themeStore = useTheming() const displayedNotifications = new Set() const displayedNotificationKeys = new Set() const popupNotificationIds = new Set() let notificationGeneration = 0 let pendingAlreadyInstalled: | { instanceId: string preview: Awaited> install: () => Promise creator?: SharedInstanceCreator onGoToInstance?: () => void | Promise } | undefined async function markNotificationRead(notification: AppNotification) { try { await client.labrinth.notifications_v2.markAsRead(String(notification.id)) } catch (error) { if (error instanceof ModrinthApiError && error.statusCode === 404) return throw error } } async function resolveInvite(invite: SharedInstanceInvite) { const [invitedBy, sharedInstance] = await Promise.all([ (!invite.invitedByUsername || !invite.invitedByAvatarUrl) && invite.invitedById ? get_user(invite.invitedById, 'bypass').catch(() => null) : null, client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => { notifySharedInstanceConnectionError() return null }), ]) return { ...invite, invitedByUsername: invite.invitedByUsername ?? invitedBy?.username ?? null, invitedByAvatarUrl: invite.invitedByAvatarUrl ?? invitedBy?.avatar_url ?? null, instanceIconUrl: sharedInstance ? sharedInstance.icon : invite.instanceIconUrl, } } function showInstall( preview: Awaited>, install: () => Promise, creator?: SharedInstanceCreator, ) { if (!installModal.value) throw new Error('Shared instance install modal is not available.') installModal.value.show(preview, install, creator) } async function showInstallOrAlreadyInstalled( sharedInstanceId: string, preview: Awaited>, install: () => Promise, creator?: SharedInstanceCreator, onGoToInstance?: () => void | Promise, ) { const existingInstance = (await list()).find( (instance) => instance.shared_instance?.id === sharedInstanceId, ) if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) { showInstall(preview, install, creator) return } if (!alreadyInstalledModal.value) { throw new Error('Shared instance already installed modal is not available.') } pendingAlreadyInstalled = { instanceId: existingInstance.id, preview, install, creator, onGoToInstance, } alreadyInstalledModal.value.show(existingInstance.name) } function handleAlreadyInstalledCancel() { pendingAlreadyInstalled = undefined } async function handleAlreadyInstalledGoToInstance() { const pending = pendingAlreadyInstalled pendingAlreadyInstalled = undefined if (!pending) return if (pending.onGoToInstance) { try { await pending.onGoToInstance() } catch (error) { handleError(toError(error)) } } await router.push(`/instance/${encodeURIComponent(pending.instanceId)}/`) } function handleAlreadyInstalledInstallAnyway() { const pending = pendingAlreadyInstalled pendingAlreadyInstalled = undefined if (!pending) return showInstall(pending.preview, pending.install, pending.creator) } async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) { try { const preview = await install_get_shared_instance_preview( invite.sharedInstanceId, invite.sharedInstanceName, ) if (invite.instanceIconUrl) preview.iconUrl = invite.instanceIconUrl await showInstallOrAlreadyInstalled( invite.sharedInstanceId, preview, async () => { await install_shared_instance( invite.sharedInstanceId, invite.sharedInstanceName, invite.invitedById, null, null, invite.instanceIconUrl, ) await markNotificationRead(notification) await queryClient.invalidateQueries({ queryKey: ['instances'] }) }, invite.invitedByUsername ? { id: invite.invitedById, username: invite.invitedByUsername, avatarUrl: invite.invitedByAvatarUrl, } : undefined, () => markNotificationRead(notification), ) } catch (error) { notifySharedInstanceError(error) } } async function handleNotification(notification: AppNotification) { const parsedInvite = parseSharedInstanceInviteNotification(notification) if (!parsedInvite) return false if (displayedNotifications.has(notification.id)) return true const generation = notificationGeneration displayedNotifications.add(notification.id) const invite = await resolveInvite(parsedInvite) 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: { type: 'instance-invite', actorName: invite.invitedByUsername, actorAvatarUrl: invite.invitedByAvatarUrl ?? undefined, entityName: invite.sharedInstanceName, entityIconUrl: invite.instanceIconUrl ?? undefined, onAccept: () => acceptNotification(notification, invite), onDecline: () => markNotificationRead(notification).catch((error) => handleError(toError(error))), onOpenActor: () => { if (invite.invitedByUsername) { void router.push(`/user/${encodeURIComponent(invite.invitedByUsername)}`) } }, }, }) 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((resolve) => { const stop = watch(auth.isReady!, (ready) => { if (ready) { stop() resolve() } }) }) } if (auth.session_token.value) return true return (await accountRequiredModal.value?.show()) ?? false } async function installFromInviteId(inviteId: string) { try { if (!(await requireAccount())) return const invite = await install_accept_shared_instance_invite(inviteId) const manager = invite.managerId ? await get_user(invite.managerId, 'bypass').catch(() => null) : null await showInstallOrAlreadyInstalled( invite.sharedInstanceId, invite.preview, async () => { await install_shared_instance( invite.sharedInstanceId, invite.preview.name, invite.managerId, invite.serverManagerName, invite.serverManagerIconUrl, invite.instanceIconUrl, ) await queryClient.invalidateQueries({ queryKey: ['instances'] }) }, manager ? { id: manager.id, username: manager.username, avatarUrl: manager.avatar_url ?? null, } : undefined, ) } catch (error) { notifySharedInstanceError(error) } } return { handleNotification, installFromInviteId, clearNotifications, handleAlreadyInstalledCancel, handleAlreadyInstalledGoToInstance, handleAlreadyInstalledInstallAnyway, } }