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