Replace Add User to Lists modal with modern Dialog component
Migrate from deprecated modal system to modern Dialog system using the efficient getListsWithMembership() API instead of the inefficient useDangerousListMembershipsQuery(). Adds optimistic updates for faster UI feedback on add/remove operations. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,15 +3,6 @@ import {createContext, useContext, useEffect, useMemo, useState} from 'react'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
|
||||
export interface UserAddRemoveListsModal {
|
||||
name: 'user-add-remove-lists'
|
||||
subject: string
|
||||
handle: string
|
||||
displayName: string
|
||||
onAdd?: (listUri: string) => void
|
||||
onRemove?: (listUri: string) => void
|
||||
}
|
||||
|
||||
export interface ContentLanguagesSettingsModal {
|
||||
name: 'content-languages-settings'
|
||||
}
|
||||
@@ -21,10 +12,7 @@ export interface ContentLanguagesSettingsModal {
|
||||
*/
|
||||
export type Modal =
|
||||
// Curation
|
||||
| ContentLanguagesSettingsModal
|
||||
|
||||
// Lists
|
||||
| UserAddRemoveListsModal
|
||||
ContentLanguagesSettingsModal
|
||||
|
||||
const ModalContext = createContext<{
|
||||
isModalActive: boolean
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
/**
|
||||
* 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 {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyGraphGetStarterPacksWithMembership,
|
||||
@@ -22,85 +6,14 @@ import {
|
||||
import {
|
||||
type InfiniteData,
|
||||
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'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs'
|
||||
|
||||
// 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({
|
||||
subject,
|
||||
onSuccess,
|
||||
@@ -133,43 +46,18 @@ export function useListMembershipAddMutation({
|
||||
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)
|
||||
// use a timeout to wait for the appview
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||
})
|
||||
}, 1e3)
|
||||
|
||||
// update WITH_MEMBERSHIPS query
|
||||
|
||||
// update WITH_MEMBERSHIPS query for starter packs
|
||||
if (subject) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
||||
@@ -251,32 +139,17 @@ export function useListMembershipRemoveMutation({
|
||||
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)
|
||||
// use a timeout to wait for the appview
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||
})
|
||||
}, 1e3)
|
||||
|
||||
// update WITH_MEMBERSHIPS query for starter packs
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
|
||||
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyGraphGetListsWithMembership,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
export type ListWithMembership =
|
||||
AppBskyGraphGetListsWithMembership.ListWithMembership
|
||||
|
||||
const RQKEY_ROOT = 'lists-with-membership'
|
||||
export const RQKEY = (actor: string) => [RQKEY_ROOT, actor]
|
||||
|
||||
export function useListsWithMembershipQuery({
|
||||
actor,
|
||||
enabled = true,
|
||||
}: {
|
||||
actor: string
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useInfiniteQuery<
|
||||
AppBskyGraphGetListsWithMembership.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>,
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
queryKey: RQKEY(actor),
|
||||
queryFn: async ({pageParam}: {pageParam?: string}) => {
|
||||
const res = await agent.app.bsky.graph.getListsWithMembership({
|
||||
actor,
|
||||
limit: 50,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
enabled: Boolean(actor) && enabled,
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateListMembershipOptimistically({
|
||||
queryClient,
|
||||
actor,
|
||||
listUri,
|
||||
membershipUri,
|
||||
subject,
|
||||
}: {
|
||||
queryClient: QueryClient
|
||||
actor: string
|
||||
listUri: string
|
||||
membershipUri: string
|
||||
subject: AppBskyActorDefs.ProfileView
|
||||
}) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
|
||||
>(RQKEY(actor), old => {
|
||||
if (!old) return old
|
||||
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map(page => ({
|
||||
...page,
|
||||
listsWithMembership: page.listsWithMembership.map(lwm => {
|
||||
if (lwm.list.uri === listUri) {
|
||||
return {
|
||||
...lwm,
|
||||
listItem: {
|
||||
uri: membershipUri,
|
||||
subject,
|
||||
},
|
||||
}
|
||||
}
|
||||
return lwm
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function removeListMembershipOptimistically({
|
||||
queryClient,
|
||||
actor,
|
||||
listUri,
|
||||
}: {
|
||||
queryClient: QueryClient
|
||||
actor: string
|
||||
listUri: string
|
||||
}) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<AppBskyGraphGetListsWithMembership.OutputSchema>
|
||||
>(RQKEY(actor), old => {
|
||||
if (!old) return old
|
||||
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map(page => ({
|
||||
...page,
|
||||
listsWithMembership: page.listsWithMembership.map(lwm => {
|
||||
if (lwm.list.uri === listUri) {
|
||||
return {
|
||||
...lwm,
|
||||
listItem: undefined,
|
||||
}
|
||||
}
|
||||
return lwm
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user