mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1/**
2 * NOTE
3 *
4 * This query is a temporary solution to our lack of server API for
5 * querying user membership in an API. It is extremely inefficient.
6 *
7 * THIS SHOULD ONLY BE USED IN MODALS FOR MODIFYING A USER'S LIST MEMBERSHIP!
8 * Use the list-members query for rendering a list's members.
9 *
10 * It works by fetching *all* of the user's list item records and querying
11 * or manipulating that cache. For users with large lists, it will fall
12 * down completely, so be very conservative about how you use it.
13 *
14 * -prf
15 */
16
17import {AtUri} from '@atproto/api'
18import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
19
20import {useSession, getAgent} from '#/state/session'
21import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
22import {STALE} from '#/state/queries'
23
24// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
25const SANITY_PAGE_LIMIT = 1000
26const PAGE_SIZE = 100
27// ...which comes 100,000k list members
28
29export const RQKEY = () => ['list-memberships']
30
31export interface ListMembersip {
32 membershipUri: string
33 listUri: string
34 actorDid: string
35}
36
37/**
38 * This API is dangerous! Read the note above!
39 */
40export function useDangerousListMembershipsQuery() {
41 const {currentAccount} = useSession()
42 return useQuery<ListMembersip[]>({
43 staleTime: STALE.MINUTES.FIVE,
44 queryKey: RQKEY(),
45 async queryFn() {
46 if (!currentAccount) {
47 return []
48 }
49 let cursor
50 let arr: ListMembersip[] = []
51 for (let i = 0; i < SANITY_PAGE_LIMIT; i++) {
52 const res = await getAgent().app.bsky.graph.listitem.list({
53 repo: currentAccount.did,
54 limit: PAGE_SIZE,
55 cursor,
56 })
57 arr = arr.concat(
58 res.records.map(r => ({
59 membershipUri: r.uri,
60 listUri: r.value.list,
61 actorDid: r.value.subject,
62 })),
63 )
64 cursor = res.cursor
65 if (!cursor) {
66 break
67 }
68 }
69 return arr
70 },
71 })
72}
73
74/**
75 * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record)
76 */
77export function getMembership(
78 memberships: ListMembersip[] | undefined,
79 list: string,
80 actor: string,
81): string | false | undefined {
82 if (!memberships) {
83 return undefined
84 }
85 const membership = memberships.find(
86 m => m.listUri === list && m.actorDid === actor,
87 )
88 return membership ? membership.membershipUri : false
89}
90
91export function useListMembershipAddMutation() {
92 const {currentAccount} = useSession()
93 const queryClient = useQueryClient()
94 return useMutation<
95 {uri: string; cid: string},
96 Error,
97 {listUri: string; actorDid: string}
98 >({
99 mutationFn: async ({listUri, actorDid}) => {
100 if (!currentAccount) {
101 throw new Error('Not logged in')
102 }
103 const res = await getAgent().app.bsky.graph.listitem.create(
104 {repo: currentAccount.did},
105 {
106 subject: actorDid,
107 list: listUri,
108 createdAt: new Date().toISOString(),
109 },
110 )
111 // TODO
112 // we need to wait for appview to update, but there's not an efficient
113 // query for that, so we use a timeout below
114 // -prf
115 return res
116 },
117 onSuccess(data, variables) {
118 // manually update the cache; a refetch is too expensive
119 let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
120 if (memberships) {
121 memberships = memberships
122 // avoid dups
123 .filter(
124 m =>
125 !(
126 m.actorDid === variables.actorDid &&
127 m.listUri === variables.listUri
128 ),
129 )
130 .concat([
131 {
132 ...variables,
133 membershipUri: data.uri,
134 },
135 ])
136 queryClient.setQueryData(RQKEY(), memberships)
137 }
138 // invalidate the members queries (used for rendering the listings)
139 // use a timeout to wait for the appview (see above)
140 setTimeout(() => {
141 queryClient.invalidateQueries({
142 queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
143 })
144 }, 1e3)
145 },
146 })
147}
148
149export function useListMembershipRemoveMutation() {
150 const {currentAccount} = useSession()
151 const queryClient = useQueryClient()
152 return useMutation<
153 void,
154 Error,
155 {listUri: string; actorDid: string; membershipUri: string}
156 >({
157 mutationFn: async ({membershipUri}) => {
158 if (!currentAccount) {
159 throw new Error('Not logged in')
160 }
161 const membershipUrip = new AtUri(membershipUri)
162 await getAgent().app.bsky.graph.listitem.delete({
163 repo: currentAccount.did,
164 rkey: membershipUrip.rkey,
165 })
166 // TODO
167 // we need to wait for appview to update, but there's not an efficient
168 // query for that, so we use a timeout below
169 // -prf
170 },
171 onSuccess(data, variables) {
172 // manually update the cache; a refetch is too expensive
173 let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
174 if (memberships) {
175 memberships = memberships.filter(
176 m =>
177 !(
178 m.actorDid === variables.actorDid &&
179 m.listUri === variables.listUri
180 ),
181 )
182 queryClient.setQueryData(RQKEY(), memberships)
183 }
184 // invalidate the members queries (used for rendering the listings)
185 // use a timeout to wait for the appview (see above)
186 setTimeout(() => {
187 queryClient.invalidateQueries({
188 queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
189 })
190 }, 1e3)
191 },
192 })
193}