Files
modrinth/apps/app-frontend/src/composables/use-friends.ts
T
Calum H.andsychic e58af98f21 feat: instance sharing thru shared-instances service (#6569)
* feat: implement instance share page + search_users backend call

* feat: invite players modal

* feat: use tanstack queries for friends sync across app pages

* feat: base shared instances implementation

* fix: admon style

* feat: impl instance admonitions like server panel

* fix: impl get + del usage

* feat: support modpack links

* feat: invite notif accepting

* fix: lint + fmt

* feat: impl install to play

* feat: impl usage of UpdateToPlayModal

* feat: warnings on deleting/disabling shared-instance version content

* fix: send instance name

* feat: align with backend

* feat: shared instances qa

* feat: wrong account protection

* feat: qa

* fix: smartly apply updates

* fix: install bug

* fix: 401/404 differentiation

* fix: fmt+prepr

* feat: qa

* feat: qa

* fix: signing out messes up revoke/deleted checks

* feat: qa

* fix: fmt + lint

* feat: lock content if part of shared instance

* fix: lint

* [do not merge] feat: rough invite links impl temp (#6666)

* fix: wrong cmd

* feat: invite page

* fix: server-manager DTO mismatch

* fix: drop anonymous invite link acceptance

* refactor: structured shared-instance unavailable errors

* refactor: centralise error presentations

* refactor: dedupe shared instance diff detection

* fix: logging in reqwests

* refactor: move app.vue shared instances into handler

* refactor: break up Share.vue

* refactor: split up shared instances state outside of instance index

* refactor: dedicated shared instances install/update modals + split up page

* refactor: centralized managed content

* refactor: split up install shared to own runner + shared.rs split up

* refactor: dedupe sql for instance metadata enrichmnt

* refactor: friends composable + dedupe friends logic across usages

* chore: reduced unused code

* fix: align with backend

* fix: lint

* fix: file sha changes

* fix: invite links not working due to icon signed

* feat: qa

* feat: reporting frontend dummy

* fix: try use header

* remove: file hash field

* fix: pin box

* feat: malware warning for shared instances

* fix: cache rule

* feat: config files syncing

* feat: disable config sharing

* fix: header

* fix: use mark ready

* fix: dont cause push update for configs

* fix: lint

* feat: sharing page in settings

* feat: move config + change flow

* fix: qa

* fix: lint prepr

* feat: proxy file upload thru shared instances backend

* fix: use collapisible

* fix: push config

* fix: config

* feat: swap out sign in modal for new one

* fix: report flow

* fix: exclude configs.zip from external warnings

* fix: nuxi init

* fix: config bundle downloading

* fix: error notif

* fix: polling

* fix: qa

* fix: lint + prepr

* feat: shared instances moderation frontend + hook up report flow

* fix: report copy

* fix: lint

* fix: lint

* fix: modrinth ids being undefined

* feat: instance quarantining

* fix: prepr + fmt

* fix: quarantined -> locked terminology

* fix: missing endpoint impls + fmt

* fix: missing api in build.rs

* fix: share tab jittery

* fix: fmt

*PT bug

* fix: invites count as users even if pending

* fix: prepr

* fix: invite page owner in users list

* fix: lint

* fix: qa

* fix: lint

* fix: members stale not clearing

* fix: invite use joined_at field

* fix: lint

* fix: qa

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
2026-07-24 13:06:38 +00:00

149 lines
4.9 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
import { toError } from '@/helpers/errors'
import { friend_listener } from '@/helpers/events.js'
import {
acceptCachedFriend,
add_friend,
createPendingFriend,
type FriendCacheUser,
friendsQueryKey,
type FriendWithUserData,
getFriendsWithUserData,
getFriendUserId,
matchesFriend,
remove_friend,
removeCachedFriend,
upsertCachedFriend,
} from '@/helpers/friends'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
type FriendsMutationContext = {
queryKey: ReturnType<typeof friendsQueryKey>
previousFriends?: FriendWithUserData[]
}
type AddFriendMutationVariables = {
userId: string
user: FriendCacheUser
acceptExisting: boolean
}
type RemoveFriendMutationVariables = {
userId: string
user: FriendWithUserData
}
export function useFriends(options: {
currentUserId: MaybeRefOrGetter<string | null | undefined>
getCredentials: () => ModrinthCredentials | null | Promise<ModrinthCredentials | null>
enabled?: MaybeRefOrGetter<boolean>
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const queryKey = computed(() => friendsQueryKey(toValue(options.currentUserId)))
const query = useQuery({
queryKey,
queryFn: async () => getFriendsWithUserData(await options.getCredentials()),
enabled: () => !!toValue(options.currentUserId) && toValue(options.enabled ?? true),
staleTime: 30_000,
})
const friends = computed(() => query.data.value ?? [])
function restore(context?: FriendsMutationContext) {
if (!context) return
if (context.previousFriends === undefined) {
queryClient.removeQueries({ queryKey: context.queryKey, exact: true })
return
}
queryClient.setQueryData(context.queryKey, context.previousFriends)
}
const addMutation = useMutation({
mutationFn: ({ userId }: AddFriendMutationVariables) => add_friend(userId),
onMutate: async ({ user, acceptExisting }): Promise<FriendsMutationContext> => {
const activeQueryKey = queryKey.value
await queryClient.cancelQueries({ queryKey: activeQueryKey })
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
const currentUserId = toValue(options.currentUserId)
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
acceptExisting
? acceptCachedFriend(cachedFriends, user.id, user.username, currentUserId)
: upsertCachedFriend(
cachedFriends,
createPendingFriend(user, currentUserId),
currentUserId,
),
)
return { queryKey: activeQueryKey, previousFriends }
},
onError: (error, _variables, context) => {
restore(context)
options.onError?.(toError(error))
},
onSettled: (_data, _error, _variables, context) => {
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
},
})
const removeMutation = useMutation({
mutationFn: ({ userId }: RemoveFriendMutationVariables) => remove_friend(userId),
onMutate: async ({ user, userId }): Promise<FriendsMutationContext> => {
const activeQueryKey = queryKey.value
await queryClient.cancelQueries({ queryKey: activeQueryKey })
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
removeCachedFriend(cachedFriends, userId, user.username, toValue(options.currentUserId)),
)
return { queryKey: activeQueryKey, previousFriends }
},
onError: (error, _variables, context) => {
restore(context)
options.onError?.(toError(error))
},
onSettled: (_data, _error, _variables, context) => {
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
},
})
function requestFriend(user: FriendCacheUser, acceptExisting = false) {
addMutation.mutate({ userId: user.id, user, acceptExisting })
}
function acceptFriend(friend: FriendWithUserData) {
const userId = getFriendUserId(friend, toValue(options.currentUserId))
requestFriend({ id: userId, username: friend.username, avatarUrl: friend.avatar }, true)
}
function removeFriend(friend: FriendWithUserData) {
const userId = getFriendUserId(friend, toValue(options.currentUserId))
removeMutation.mutate({ userId, user: friend })
}
function findFriend(id: string, username: string) {
return friends.value.find((friend) =>
matchesFriend(friend, id, username, toValue(options.currentUserId)),
)
}
let unlisten: (() => void) | undefined
void friend_listener(() => {
void queryClient.invalidateQueries({ queryKey: queryKey.value })
}).then((listener) => {
unlisten = listener
})
onUnmounted(() => unlisten?.())
return {
query,
queryKey,
friends,
loading: computed(() => !!toValue(options.currentUserId) && query.isLoading.value),
requestFriend,
acceptFriend,
removeFriend,
findFriend,
}
}