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 {STALE} from '#/state/queries'
21import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
22import {useAgent, useSession} from '#/state/session'
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
29const RQKEY_ROOT = 'list-memberships'
30export const RQKEY = () => [RQKEY_ROOT]
31
32export interface ListMembersip {
33 membershipUri: string
34 listUri: string
35 actorDid: string
36}
37
38/**
39 * This API is dangerous! Read the note above!
40 */
41export function useDangerousListMembershipsQuery() {
42 const {currentAccount} = useSession()
43 const agent = useAgent()
44 return useQuery<ListMembersip[]>({
45 staleTime: STALE.MINUTES.FIVE,
46 queryKey: RQKEY(),
47 async queryFn() {
48 if (!currentAccount) {
49 return []
50 }
51 let cursor
52 let arr: ListMembersip[] = []
53 for (let i = 0; i < SANITY_PAGE_LIMIT; i++) {
54 const res = await agent.app.bsky.graph.listitem.list({
55 repo: currentAccount.did,
56 limit: PAGE_SIZE,
57 cursor,
58 })
59 arr = arr.concat(
60 res.records.map(r => ({
61 membershipUri: r.uri,
62 listUri: r.value.list,
63 actorDid: r.value.subject,
64 })),
65 )
66 cursor = res.cursor
67 if (!cursor) {
68 break
69 }
70 }
71 return arr
72 },
73 })
74}
75
76/**
77 * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record)
78 */
79export function getMembership(
80 memberships: ListMembersip[] | undefined,
81 list: string,
82 actor: string,
83): string | false | undefined {
84 if (!memberships) {
85 return undefined
86 }
87 const membership = memberships.find(
88 m => m.listUri === list && m.actorDid === actor,
89 )
90 return membership ? membership.membershipUri : false
91}
92
93export function useListMembershipAddMutation() {
94 const {currentAccount} = useSession()
95 const agent = useAgent()
96 const queryClient = useQueryClient()
97 return useMutation<
98 {uri: string; cid: string},
99 Error,
100 {listUri: string; actorDid: string}
101 >({
102 mutationFn: async ({listUri, actorDid}) => {
103 if (!currentAccount) {
104 throw new Error('Not logged in')
105 }
106 const res = await agent.app.bsky.graph.listitem.create(
107 {repo: currentAccount.did},
108 {
109 subject: actorDid,
110 list: listUri,
111 createdAt: new Date().toISOString(),
112 },
113 )
114 // TODO
115 // we need to wait for appview to update, but there's not an efficient
116 // query for that, so we use a timeout below
117 // -prf
118 return res
119 },
120 onSuccess(data, variables) {
121 // manually update the cache; a refetch is too expensive
122 let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
123 if (memberships) {
124 memberships = memberships
125 // avoid dups
126 .filter(
127 m =>
128 !(
129 m.actorDid === variables.actorDid &&
130 m.listUri === variables.listUri
131 ),
132 )
133 .concat([
134 {
135 ...variables,
136 membershipUri: data.uri,
137 },
138 ])
139 queryClient.setQueryData(RQKEY(), memberships)
140 }
141 // invalidate the members queries (used for rendering the listings)
142 // use a timeout to wait for the appview (see above)
143 setTimeout(() => {
144 queryClient.invalidateQueries({
145 queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
146 })
147 }, 1e3)
148 },
149 })
150}
151
152export function useListMembershipRemoveMutation() {
153 const {currentAccount} = useSession()
154 const agent = useAgent()
155 const queryClient = useQueryClient()
156 return useMutation<
157 void,
158 Error,
159 {listUri: string; actorDid: string; membershipUri: string}
160 >({
161 mutationFn: async ({membershipUri}) => {
162 if (!currentAccount) {
163 throw new Error('Not logged in')
164 }
165 const membershipUrip = new AtUri(membershipUri)
166 await agent.app.bsky.graph.listitem.delete({
167 repo: currentAccount.did,
168 rkey: membershipUrip.rkey,
169 })
170 // TODO
171 // we need to wait for appview to update, but there's not an efficient
172 // query for that, so we use a timeout below
173 // -prf
174 },
175 onSuccess(data, variables) {
176 // manually update the cache; a refetch is too expensive
177 let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
178 if (memberships) {
179 memberships = memberships.filter(
180 m =>
181 !(
182 m.actorDid === variables.actorDid &&
183 m.listUri === variables.listUri
184 ),
185 )
186 queryClient.setQueryData(RQKEY(), memberships)
187 }
188 // invalidate the members queries (used for rendering the listings)
189 // use a timeout to wait for the appview (see above)
190 setTimeout(() => {
191 queryClient.invalidateQueries({
192 queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
193 })
194 }, 1e3)
195 },
196 })
197}