mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
* feat: blocking api interfaces * feat: block action on user pages + shared instance flows * feat: safety settings subpage * feat: interaction source settings * feat: finish social settings * feat: profile settings xplat * fix: prepr * feat: click on friends * default instance -> game options & fix feature flag width * fix: scroll indicators --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
319 lines
9.4 KiB
TypeScript
319 lines
9.4 KiB
TypeScript
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<ReturnType<typeof install_get_shared_instance_preview>>,
|
|
install: () => Promise<void>,
|
|
creator?: SharedInstanceCreator,
|
|
): void
|
|
}
|
|
|
|
type SharedInstanceCreator = {
|
|
id: string | null
|
|
username: string
|
|
avatarUrl: string | null
|
|
}
|
|
|
|
type AccountRequiredModal = {
|
|
show(event?: MouseEvent): Promise<boolean>
|
|
}
|
|
|
|
type AlreadyInstalledModal = {
|
|
show(instanceName: string): void
|
|
}
|
|
|
|
export function useSharedInstanceInviteHandler(
|
|
installModal: Ref<InstallModal | undefined>,
|
|
alreadyInstalledModal: Ref<AlreadyInstalledModal | undefined>,
|
|
accountRequiredModal: Ref<AccountRequiredModal | undefined>,
|
|
) {
|
|
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<string | number>()
|
|
const displayedNotificationKeys = new Set<string>()
|
|
const popupNotificationIds = new Set<string | number>()
|
|
let notificationGeneration = 0
|
|
let pendingAlreadyInstalled:
|
|
| {
|
|
instanceId: string
|
|
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>
|
|
install: () => Promise<void>
|
|
creator?: SharedInstanceCreator
|
|
onGoToInstance?: () => void | Promise<void>
|
|
}
|
|
| 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<ReturnType<typeof install_get_shared_instance_preview>>,
|
|
install: () => Promise<void>,
|
|
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<ReturnType<typeof install_get_shared_instance_preview>>,
|
|
install: () => Promise<void>,
|
|
creator?: SharedInstanceCreator,
|
|
onGoToInstance?: () => void | Promise<void>,
|
|
) {
|
|
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<void>((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,
|
|
}
|
|
}
|