diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index 871b34da57..06aea1bec7 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -1,4 +1,4 @@
-import {useCallback, useState} from 'react'
+import {useCallback} from 'react'
import {View} from 'react-native'
import {
type AppBskyGraphGetStarterPacksWithMembership,
@@ -7,20 +7,18 @@ import {
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
-import {
- invalidateActorStarterPacksWithMembershipQuery,
- useActorStarterPacksWithMembershipsQuery,
-} from '#/state/queries/actor-starter-packs'
+import {isNetworkError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {useActorStarterPacksWithMembershipsQuery} from '#/state/queries/actor-starter-packs'
import {
useListMembershipAddMutation,
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
+import {useProfileQuery} from '#/state/queries/profile'
+import {atoms as a, native, platform, useTheme} from '#/alf'
import {AvatarStack} from '#/components/AvatarStack'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -29,6 +27,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/
import {StarterPack} from '#/components/icons/StarterPack'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
+import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
@@ -131,6 +130,7 @@ function StarterPackList({
}) {
const control = Dialog.useDialogContext()
const {_} = useLingui()
+ const {data: subject} = useProfileQuery({did: targetDid})
const {
data,
@@ -155,9 +155,13 @@ function StarterPackList({
const renderItem = useCallback(
({item}: {item: StarterPackWithMembership}) => (
-
+
),
- [targetDid],
+ [targetDid, subject],
)
const onClose = useCallback(() => {
@@ -168,9 +172,11 @@ function StarterPackList({
<>
Add to starter packs
@@ -181,7 +187,8 @@ function StarterPackList({
variant="ghost"
color="secondary"
size="small"
- shape="round">
+ shape="round"
+ style={{margin: -8}}>
@@ -232,7 +239,10 @@ function StarterPackList({
onEndReachedThreshold={0.1}
ListHeaderComponent={listHeader}
ListEmptyComponent={}
- style={IS_WEB ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]}
+ style={platform({
+ web: [a.px_2xl, {minHeight: 500}],
+ native: [a.px_2xl, a.pt_lg],
+ })}
/>
)
}
@@ -240,66 +250,54 @@ function StarterPackList({
function StarterPackItem({
starterPackWithMembership,
targetDid,
+ subject,
}: {
starterPackWithMembership: StarterPackWithMembership
targetDid: string
+ subject?: bsky.profile.AnyProfileView
}) {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
- const queryClient = useQueryClient()
const starterPack = starterPackWithMembership.starterPack
const isInPack = !!starterPackWithMembership.listItem
- const [isPendingRefresh, setIsPendingRefresh] = useState(false)
+ const {mutate: addMembership, isPending: isPendingAdd} =
+ useListMembershipAddMutation({
+ subject,
+ onSuccess: () => {
+ Toast.show(_(msg`Added to starter pack`))
+ },
+ onError: err => {
+ if (!isNetworkError(err)) {
+ logger.error('Failed to add to starter pack', {safeMessage: err})
+ }
+ Toast.show(_(msg`Failed to add to starter pack`), {type: 'error'})
+ },
+ })
- const {mutate: addMembership} = useListMembershipAddMutation({
- onSuccess: () => {
- 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: () => {
- Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
- setIsPendingRefresh(false)
- },
- })
+ const {mutate: removeMembership, isPending: isPendingRemove} =
+ useListMembershipRemoveMutation({
+ onSuccess: () => {
+ Toast.show(_(msg`Removed from starter pack`))
+ },
+ onError: err => {
+ if (!isNetworkError(err)) {
+ logger.error('Failed to remove from starter pack', {safeMessage: err})
+ }
+ Toast.show(_(msg`Failed to remove from starter pack`), {type: 'error'})
+ },
+ })
- const {mutate: removeMembership} = useListMembershipRemoveMutation({
- onSuccess: () => {
- 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: () => {
- Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
- setIsPendingRefresh(false)
- },
- })
+ const isPending = isPendingAdd || isPendingRemove
const handleToggleMembership = () => {
- if (!starterPack.list?.uri || isPendingRefresh) return
+ if (!starterPack.list?.uri || isPending) return
const listUri = starterPack.list.uri
const starterPackUri = starterPack.uri
- setIsPendingRefresh(true)
-
if (!isInPack) {
addMembership({
listUri: listUri,
@@ -309,7 +307,6 @@ function StarterPackItem({
} else {
if (!starterPackWithMembership.listItem?.uri) {
console.error('Cannot remove: missing membership URI')
- setIsPendingRefresh(false)
return
}
removeMembership({
@@ -344,7 +341,7 @@ function StarterPackItem({
starterPack.listItemsSample.length > 0 && (
<>
p.subject)}
@@ -375,8 +372,9 @@ function StarterPackItem({
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary_subtle'}
size="tiny"
- disabled={isPendingRefresh}
+ disabled={isPending}
onPress={handleToggleMembership}>
+ {isPending && }
{isInPack ? Remove : Add}
diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts
index d40e054535..97cc3b5e05 100644
--- a/src/state/queries/actor-starter-packs.ts
+++ b/src/state/queries/actor-starter-packs.ts
@@ -1,13 +1,4 @@
-import {
- type AppBskyGraphGetActorStarterPacks,
- type AppBskyGraphGetStarterPacksWithMembership,
-} from '@atproto/api'
-import {
- type InfiniteData,
- type QueryClient,
- type QueryKey,
- useInfiniteQuery,
-} from '@tanstack/react-query'
+import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
@@ -28,13 +19,7 @@ export function useActorStarterPacksQuery({
}) {
const agent = useAgent()
- return useInfiniteQuery<
- AppBskyGraphGetActorStarterPacks.OutputSchema,
- Error,
- InfiniteData,
- QueryKey,
- string | undefined
- >({
+ return useInfiniteQuery({
queryKey: RQKEY(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getActorStarterPacks({
@@ -59,13 +44,7 @@ export function useActorStarterPacksWithMembershipsQuery({
}) {
const agent = useAgent()
- return useInfiniteQuery<
- AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
- Error,
- InfiniteData,
- QueryKey,
- string | undefined
- >({
+ return useInfiniteQuery({
queryKey: RQKEY_WITH_MEMBERSHIP(did),
queryFn: async ({pageParam}: {pageParam?: string}) => {
const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts
index 410a613ada..dd7fde2482 100644
--- a/src/state/queries/list-memberships.ts
+++ b/src/state/queries/list-memberships.ts
@@ -14,12 +14,23 @@
* -prf
*/
-import {AtUri} from '@atproto/api'
-import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
+import {
+ type AppBskyActorDefs,
+ type AppBskyGraphGetStarterPacksWithMembership,
+ AtUri,
+} from '@atproto/api'
+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
@@ -91,9 +102,14 @@ export function getMembership(
}
export function useListMembershipAddMutation({
+ subject,
onSuccess,
onError,
}: {
+ /**
+ * Needed for optimistic update of starter pack query
+ */
+ subject?: bsky.profile.AnyProfileView
onSuccess?: (data: {uri: string; cid: string}) => void
onError?: (error: Error) => void
} = {}) {
@@ -151,6 +167,60 @@ export function useListMembershipAddMutation({
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
})
}, 1e3)
+
+ // update WITH_MEMBERSHIPS query
+
+ if (subject) {
+ queryClient.setQueryData<
+ InfiniteData
+ >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
+ if (!old) return old
+
+ return {
+ ...old,
+ pages: old.pages.map(page => ({
+ ...page,
+ starterPacksWithMembership: page.starterPacksWithMembership.map(
+ spWithMembership => {
+ if (
+ spWithMembership.starterPack.list &&
+ spWithMembership.starterPack.list?.uri === variables.listUri
+ ) {
+ return {
+ ...spWithMembership,
+ starterPack: {
+ ...spWithMembership.starterPack,
+ listItemsSample: [
+ {
+ uri: data.uri,
+ subject: subject as AppBskyActorDefs.ProfileView,
+ },
+ ...(spWithMembership.starterPack.listItemsSample?.filter(
+ item => item.subject.did !== variables.actorDid,
+ ) ?? []),
+ ],
+ list: {
+ ...spWithMembership.starterPack.list,
+ listItemCount:
+ (spWithMembership.starterPack.list.listItemCount ??
+ 0) + 1,
+ },
+ },
+ listItem: {
+ uri: data.uri,
+ subject: subject as AppBskyActorDefs.ProfileView,
+ },
+ }
+ }
+
+ return spWithMembership
+ },
+ ),
+ })),
+ }
+ })
+ }
+
onSuccess?.(data)
},
onError,
@@ -206,6 +276,50 @@ export function useListMembershipRemoveMutation({
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
})
}, 1e3)
+
+ queryClient.setQueryData<
+ InfiniteData
+ >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
+ if (!old) return old
+
+ return {
+ ...old,
+ pages: old.pages.map(page => ({
+ ...page,
+ starterPacksWithMembership: page.starterPacksWithMembership.map(
+ spWithMembership => {
+ if (
+ spWithMembership.starterPack.list &&
+ spWithMembership.starterPack.list.uri === variables.listUri
+ ) {
+ return {
+ ...spWithMembership,
+ starterPack: {
+ ...spWithMembership.starterPack,
+ listItemsSample:
+ spWithMembership.starterPack.listItemsSample?.filter(
+ item => item.subject.did !== variables.actorDid,
+ ),
+ list: {
+ ...spWithMembership.starterPack.list,
+ listItemCount: Math.max(
+ 0,
+ (spWithMembership.starterPack.list.listItemCount ??
+ 1) - 1,
+ ),
+ },
+ },
+ listItem: undefined,
+ }
+ }
+
+ return spWithMembership
+ },
+ ),
+ })),
+ }
+ })
+
onSuccess?.(data)
},
onError,
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index 8d7846d815..684dcbf0fe 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -329,15 +329,17 @@ let ProfileMenu = ({
)}
>
)}
-
-
- Add to starter packs
-
-
-
+ {!isSelf && (
+
+
+ Add to starter packs
+
+
+
+ )}