add actual optimistic updates

This commit is contained in:
Samuel Newman
2025-12-01 12:52:44 +02:00
parent 8b57006fdb
commit 2b3e6edb4b
3 changed files with 109 additions and 73 deletions
+22 -37
View File
@@ -1,4 +1,4 @@
import {useCallback, useState} from 'react' import {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyGraphGetStarterPacksWithMembership, type AppBskyGraphGetStarterPacksWithMembership,
@@ -7,14 +7,12 @@ import {
import {msg, Plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import { import {isNetworkError} from '#/lib/strings/errors'
invalidateActorStarterPacksWithMembershipQuery, import {logger} from '#/logger'
useActorStarterPacksWithMembershipsQuery, import {useActorStarterPacksWithMembershipsQuery} from '#/state/queries/actor-starter-packs'
} from '#/state/queries/actor-starter-packs'
import { import {
useListMembershipAddMutation, useListMembershipAddMutation,
useListMembershipRemoveMutation, useListMembershipRemoveMutation,
@@ -253,51 +251,38 @@ function StarterPackItem({
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient()
const starterPack = starterPackWithMembership.starterPack const starterPack = starterPackWithMembership.starterPack
const isInPack = !!starterPackWithMembership.listItem const isInPack = !!starterPackWithMembership.listItem
const [isPendingRefresh, setIsPendingRefresh] = useState(false) const {mutate: addMembership, isPending: isPendingAdd} =
useListMembershipAddMutation({
const {mutate: addMembership} = useListMembershipAddMutation({
onSuccess: () => { onSuccess: () => {
Toast.show(_(msg`Added to starter pack`)) Toast.show(_(msg`Added to starter pack`))
// Use a timeout to wait for the appview to update, matching the pattern
// in list-memberships.ts
setTimeout(() => {
invalidateActorStarterPacksWithMembershipQuery({
queryClient,
did: targetDid,
})
setIsPendingRefresh(false)
}, 1e3)
}, },
onError: () => { onError: err => {
Toast.show(_(msg`Failed to add to starter pack`), 'xmark') if (!isNetworkError(err)) {
setIsPendingRefresh(false) logger.error('Failed to remove from starter pack', {safeMessage: err})
}
Toast.show(_(msg`Failed to add to starter pack`), {type: 'error'})
}, },
}) })
const {mutate: removeMembership} = useListMembershipRemoveMutation({ const {mutate: removeMembership, isPending: isPendingRemove} =
useListMembershipRemoveMutation({
onSuccess: () => { onSuccess: () => {
Toast.show(_(msg`Removed from starter pack`)) Toast.show(_(msg`Removed from starter pack`))
// Use a timeout to wait for the appview to update, matching the pattern
// in list-memberships.ts
setTimeout(() => {
invalidateActorStarterPacksWithMembershipQuery({
queryClient,
did: targetDid,
})
setIsPendingRefresh(false)
}, 1e3)
}, },
onError: () => { onError: err => {
Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') if (!isNetworkError(err)) {
setIsPendingRefresh(false) logger.error('Failed to remove from starter pack', {safeMessage: err})
}
Toast.show(_(msg`Failed to remove from starter pack`), {type: 'error'})
}, },
}) })
const isPending = isPendingAdd || isPendingRemove
const handleToggleMembership = () => { const handleToggleMembership = () => {
if (!starterPack.list?.uri || isPending) return if (!starterPack.list?.uri || isPending) return
@@ -313,7 +298,6 @@ function StarterPackItem({
} else { } else {
if (!starterPackWithMembership.listItem?.uri) { if (!starterPackWithMembership.listItem?.uri) {
console.error('Cannot remove: missing membership URI') console.error('Cannot remove: missing membership URI')
setIsPendingRefresh(false)
return return
} }
removeMembership({ removeMembership({
@@ -379,8 +363,9 @@ function StarterPackItem({
label={isInPack ? _(msg`Remove`) : _(msg`Add`)} label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary_subtle'} color={isInPack ? 'secondary' : 'primary_subtle'}
size="tiny" size="tiny"
disabled={isPendingRefresh} disabled={isPending}
onPress={handleToggleMembership}> onPress={handleToggleMembership}>
{isPending && <ButtonIcon icon={Loader} />}
<ButtonText> <ButtonText>
{isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>} {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
</ButtonText> </ButtonText>
+3 -24
View File
@@ -1,13 +1,4 @@
import { import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query'
type AppBskyGraphGetActorStarterPacks,
type AppBskyGraphGetStarterPacksWithMembership,
} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@@ -28,13 +19,7 @@ export function useActorStarterPacksQuery({
}) { }) {
const agent = useAgent() const agent = useAgent()
return useInfiniteQuery< return useInfiniteQuery({
AppBskyGraphGetActorStarterPacks.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema>,
QueryKey,
string | undefined
>({
queryKey: RQKEY(did), queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => { queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({ const res = await agent.app.bsky.graph.getActorStarterPacks({
@@ -59,13 +44,7 @@ export function useActorStarterPacksWithMembershipsQuery({
}) { }) {
const agent = useAgent() const agent = useAgent()
return useInfiniteQuery< return useInfiniteQuery({
AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>,
QueryKey,
string | undefined
>({
queryKey: RQKEY_WITH_MEMBERSHIP(did), queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => { queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({ const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
+75 -3
View File
@@ -14,12 +14,22 @@
* -prf * -prf
*/ */
import {AtUri} from '@atproto/api' import {
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' type AppBskyActorDefs,
type AppBskyGraphGetStarterPacksWithMembership,
AtUri,
} from '@atproto/api'
import {
type InfiniteData,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members' import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records // sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000 const SANITY_PAGE_LIMIT = 1000
@@ -123,7 +133,7 @@ export function useListMembershipAddMutation({
// -prf // -prf
return res return res
}, },
onSuccess: (data, variables) => { onSuccess: async (data, variables) => {
// manually update the cache; a refetch is too expensive // manually update the cache; a refetch is too expensive
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY()) let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
if (memberships) { if (memberships) {
@@ -151,6 +161,42 @@ export function useListMembershipAddMutation({
queryKey: LIST_MEMBERS_RQKEY(variables.listUri), queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
}) })
}, 1e3) }, 1e3)
// update WITH_MEMBERSHIPS query
const subject = await agent
.getProfile({actor: variables.actorDid})
.then(res => res.data)
if (subject) {
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
if (!old) return old
return {
...old,
pages: old.pages.map(page => ({
...page,
starterPacksWithMembership: page.starterPacksWithMembership.map(
list => {
if (list.starterPack.list?.uri === variables.listUri) {
return {
...list,
listItem: {
uri: data.uri,
subject: subject as AppBskyActorDefs.ProfileView,
},
}
}
return list
},
),
})),
}
})
}
onSuccess?.(data) onSuccess?.(data)
}, },
onError, onError,
@@ -206,6 +252,32 @@ export function useListMembershipRemoveMutation({
queryKey: LIST_MEMBERS_RQKEY(variables.listUri), queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
}) })
}, 1e3) }, 1e3)
queryClient.setQueryData<
InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
if (!old) return old
return {
...old,
pages: old.pages.map(page => ({
...page,
starterPacksWithMembership: page.starterPacksWithMembership.map(
list => {
if (list.starterPack.list?.uri === variables.listUri) {
return {
...list,
listItem: undefined,
}
}
return list
},
),
})),
}
})
onSuccess?.(data) onSuccess?.(data)
}, },
onError, onError,