diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
index de19b0bce..73aee28f4 100644
--- a/src/components/StarterPack/ProfileStarterPacks.tsx
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -180,7 +180,7 @@ function CreateAnother() {
color="secondary"
size="small"
style={[a.self_center]}
- onPress={() => navigation.navigate('StarterPackWizard')}>
+ onPress={() => navigation.navigate('StarterPackWizard', {})}>
Create another
@@ -238,7 +238,7 @@ function Empty() {
],
})
const navToWizard = useCallback(() => {
- navigation.navigate('StarterPackWizard')
+ navigation.navigate('StarterPackWizard', {})
}, [navigation])
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
instructions: [
@@ -322,7 +322,7 @@ function Empty() {
color="secondary"
cta={_(msg`Let me choose`)}
onPress={() => {
- navigation.navigate('StarterPackWizard')
+ navigation.navigate('StarterPackWizard', {})
}}
/>
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
new file mode 100644
index 000000000..efd157723
--- /dev/null
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -0,0 +1,388 @@
+import React from 'react'
+import {View} from 'react-native'
+import {
+ type AppBskyGraphGetStarterPacksWithMembership,
+ AppBskyGraphStarterpack,
+} from '@atproto/api'
+import {msg, 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 {
+ RQKEY_WITH_MEMBERSHIP,
+ useActorStarterPacksWithMembershipsQuery,
+} from '#/state/queries/actor-starter-packs'
+import {
+ useListMembershipAddMutation,
+ useListMembershipRemoveMutation,
+} from '#/state/queries/list-memberships'
+import {List} from '#/view/com/util/List'
+import * as Toast from '#/view/com/util/Toast'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Divider} from '#/components/Divider'
+import {Text} from '#/components/Typography'
+import * as bsky from '#/types/bsky'
+import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
+import {StarterPack} from '../icons/StarterPack'
+import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
+
+type StarterPackWithMembership =
+ AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
+
+// Simple module-level state for dialog coordination
+let dialogCallbacks: {
+ onSuccess?: () => void
+} = {}
+
+export function notifyDialogSuccess() {
+ if (dialogCallbacks.onSuccess) {
+ dialogCallbacks.onSuccess()
+ }
+}
+
+export type StarterPackDialogProps = {
+ control: Dialog.DialogControlProps
+ accountDid: string
+ targetDid: string
+ enabled?: boolean
+}
+
+export function StarterPackDialog({
+ control,
+ accountDid: _accountDid,
+ targetDid,
+ enabled,
+}: StarterPackDialogProps) {
+ const {_} = useLingui()
+ const navigation = useNavigation()
+ const requireEmailVerification = useRequireEmailVerification()
+
+ React.useEffect(() => {
+ dialogCallbacks.onSuccess = () => {
+ if (!control.isOpen) {
+ control.open()
+ }
+ }
+ }, [control])
+
+ const navToWizard = React.useCallback(() => {
+ control.close()
+ navigation.navigate('StarterPackWizard', {fromDialog: true})
+ }, [navigation, control])
+
+ const wrappedNavToWizard = requireEmailVerification(navToWizard, {
+ instructions: [
+
+ Before creating a starter pack, you must first verify your email.
+ ,
+ ],
+ })
+
+ const onClose = React.useCallback(() => {
+ // setCurrentView('initial')
+ control.close()
+ }, [control])
+
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+
+ Add to starter packs
+
+
+
+
+
+
+
+
+ )
+}
+
+function Empty({onStartWizard}: {onStartWizard: () => void}) {
+ const {_} = useLingui()
+ const t = useTheme()
+
+ return (
+
+
+
+
+ You have no starter packs.
+
+
+
+
+
+ )
+}
+
+function StarterPackList({
+ onStartWizard,
+ targetDid,
+ enabled,
+}: {
+ onStartWizard: () => void
+ targetDid: string
+ enabled?: boolean
+}) {
+ const {_} = useLingui()
+
+ const {
+ data,
+ refetch,
+ isError,
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ } = useActorStarterPacksWithMembershipsQuery({did: targetDid, enabled})
+
+ const membershipItems =
+ data?.pages.flatMap(page => page.starterPacksWithMembership) || []
+
+ const _onRefresh = React.useCallback(async () => {
+ try {
+ await refetch()
+ } catch (err) {
+ // Error handling is optional since this is just a refresh
+ }
+ }, [refetch])
+
+ const _onEndReached = React.useCallback(async () => {
+ if (isFetchingNextPage || !hasNextPage || isError) return
+ try {
+ await fetchNextPage()
+ } catch (err) {
+ // Error handling is optional since this is just pagination
+ }
+ }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
+
+ const renderItem = React.useCallback(
+ ({item}: {item: StarterPackWithMembership}) => (
+
+ ),
+ [targetDid],
+ )
+
+ const ListHeaderComponent = React.useCallback(
+ () => (
+ <>
+
+
+ New starter pack
+
+
+
+
+ >
+ ),
+ [_, onStartWizard],
+ )
+
+ return (
+
+ item.starterPack.uri || index.toString()
+ }
+ refreshing={false}
+ onRefresh={_onRefresh}
+ onEndReached={_onEndReached}
+ onEndReachedThreshold={0.1}
+ ListHeaderComponent={
+ membershipItems.length > 0 ? ListHeaderComponent : null
+ }
+ ListEmptyComponent={}
+ />
+ )
+}
+
+function StarterPackItem({
+ starterPackWithMembership,
+ targetDid,
+}: {
+ starterPackWithMembership: StarterPackWithMembership
+ targetDid: string
+}) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const queryClient = useQueryClient()
+ const [isUpdating, setIsUpdating] = React.useState(false)
+
+ const starterPack = starterPackWithMembership.starterPack
+ const isInPack = !!starterPackWithMembership.listItem
+ console.log('StarterPackItem render. 111', {
+ starterPackWithMembership: starterPackWithMembership.listItem?.subject,
+ })
+
+ console.log('StarterPackItem render', {
+ starterPackWithMembership,
+ })
+
+ const {mutateAsync: addMembership} = useListMembershipAddMutation({
+ onSuccess: () => {
+ Toast.show(_(msg`Added to starter pack`))
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
+ },
+ })
+
+ const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({
+ onSuccess: () => {
+ Toast.show(_(msg`Removed from starter pack`))
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
+ },
+ })
+
+ const handleToggleMembership = async () => {
+ if (!starterPack.list?.uri || isUpdating) return
+
+ const listUri = starterPack.list.uri
+ setIsUpdating(true)
+
+ try {
+ if (!isInPack) {
+ await addMembership({
+ listUri: listUri,
+ actorDid: targetDid,
+ })
+ } else {
+ if (!starterPackWithMembership.listItem?.uri) {
+ console.error('Cannot remove: missing membership URI')
+ return
+ }
+ await removeMembership({
+ listUri: listUri,
+ actorDid: targetDid,
+ membershipUri: starterPackWithMembership.listItem.uri,
+ })
+ }
+
+ await Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: RQKEY_WITH_MEMBERSHIP(targetDid),
+ }),
+ ])
+ } catch (error) {
+ console.error('Failed to toggle membership:', error)
+ } finally {
+ setIsUpdating(false)
+ }
+ }
+
+ const {record} = starterPack
+
+ if (
+ !bsky.dangerousIsType(
+ record,
+ AppBskyGraphStarterpack.isRecord,
+ )
+ ) {
+ return null
+ }
+
+ return (
+
+
+
+ {record.name}
+
+
+
+ {starterPack.listItemsSample &&
+ starterPack.listItemsSample.length > 0 && (
+ <>
+ {starterPack.listItemsSample?.slice(0, 4).map((p, index) => (
+ 0 ? -2 : 0,
+ borderWidth: 0.5,
+ borderColor: t.atoms.bg.backgroundColor,
+ },
+ ]}
+ />
+ ))}
+
+ {starterPack.list?.listItemCount &&
+ starterPack.list.listItemCount > 4 && (
+
+ {`+${starterPack.list.listItemCount - 4} more`}
+
+ )}
+ >
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index b1db5caa6..6eb5cb609 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -79,7 +79,7 @@ export type CommonNavigatorParams = {
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
- StarterPackWizard: undefined
+ StarterPackWizard: {fromDialog?: boolean}
StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext
}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 8256349df..b918e8baf 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -54,6 +54,7 @@ import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
+import {notifyDialogSuccess} from '#/components/dialogs/StarterPackDialog'
import * as Layout from '#/components/Layout'
import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
@@ -68,7 +69,9 @@ export function Wizard({
CommonNavigatorParams,
'StarterPackEdit' | 'StarterPackWizard'
>) {
- const {rkey} = route.params ?? {}
+ const params = route.params ?? {}
+ const rkey = 'rkey' in params ? params.rkey : undefined
+ const fromDialog = 'fromDialog' in params ? params.fromDialog : false
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
@@ -133,6 +136,7 @@ export function Wizard({
currentListItems={listItems}
profile={profile}
moderationOpts={moderationOpts}
+ fromDialog={fromDialog}
/>
@@ -144,17 +148,20 @@ function WizardInner({
currentListItems,
profile,
moderationOpts,
+ fromDialog,
}: {
currentStarterPack?: AppBskyGraphDefs.StarterPackView
currentListItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
+ fromDialog?: boolean
}) {
const navigation = useNavigation()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [state, dispatch] = useWizardState()
const {currentAccount} = useSession()
+
const {data: currentProfile} = useProfileQuery({
did: currentAccount?.did,
staleTime: 0,
@@ -213,24 +220,38 @@ function WizardInner({
})
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
dispatch({type: 'SetProcessing', processing: false})
- navigation.replace('StarterPack', {
- name: currentAccount!.handle,
- rkey,
- new: true,
- })
- }
- const onSuccessEdit = () => {
- if (navigation.canGoBack()) {
+ // If launched from ProfileMenu dialog, notify the dialog and go back
+ if (fromDialog) {
navigation.goBack()
+ notifyDialogSuccess()
} else {
+ // Original behavior for other entry points
navigation.replace('StarterPack', {
name: currentAccount!.handle,
- rkey: parsed!.rkey,
+ rkey,
+ new: true,
})
}
}
+ const onSuccessEdit = () => {
+ // If launched from ProfileMenu dialog, go back to stay on profile page
+ if (fromDialog) {
+ navigation.goBack()
+ } else {
+ // Original behavior for other entry points
+ if (navigation.canGoBack()) {
+ navigation.goBack()
+ } else {
+ navigation.replace('StarterPack', {
+ name: currentAccount!.handle,
+ rkey: parsed!.rkey,
+ })
+ }
+ }
+ }
+
const {mutate: createStarterPack} = useCreateStarterPackMutation({
onSuccess: onSuccessCreate,
onError: e => {
diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts
index 670544dfe..bde719743 100644
--- a/src/state/queries/actor-starter-packs.ts
+++ b/src/state/queries/actor-starter-packs.ts
@@ -1,15 +1,23 @@
-import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
import {
- InfiniteData,
- QueryClient,
- QueryKey,
+ type AppBskyGraphGetActorStarterPacks,
+ type AppBskyGraphGetStarterPacksWithMembership,
+} from '@atproto/api'
+import {
+ type InfiniteData,
+ type QueryClient,
+ type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
export const RQKEY_ROOT = 'actor-starter-packs'
+export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
+export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [
+ RQKEY_WITH_MEMBERSHIP_ROOT,
+ did,
+]
export function useActorStarterPacksQuery({
did,
@@ -42,6 +50,37 @@ export function useActorStarterPacksQuery({
})
}
+export function useActorStarterPacksWithMembershipsQuery({
+ did,
+ enabled = true,
+}: {
+ did?: string
+ enabled?: boolean
+}) {
+ const agent = useAgent()
+
+ return useInfiniteQuery<
+ AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
+ Error,
+ InfiniteData,
+ QueryKey,
+ string | undefined
+ >({
+ queryKey: RQKEY_WITH_MEMBERSHIP(did),
+ queryFn: async ({pageParam}: {pageParam?: string}) => {
+ const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
+ actor: did!,
+ limit: 10,
+ cursor: pageParam,
+ })
+ return res.data
+ },
+ enabled: Boolean(did) && enabled,
+ initialPageParam: undefined,
+ getNextPageParam: lastPage => lastPage.cursor,
+ })
+}
+
export async function invalidateActorStarterPacksQuery({
queryClient,
did,
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index 879bf22f9..569823da6 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -27,6 +27,7 @@ import {EventStopper} from '#/view/com/util/EventStopper'
import * as Toast from '#/view/com/util/Toast'
import {Button, ButtonIcon} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
+import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
@@ -45,6 +46,7 @@ import {
} from '#/components/icons/Person'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
+import {StarterPack} from '#/components/icons/StarterPack'
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
import * as Menu from '#/components/Menu'
@@ -88,6 +90,7 @@ let ProfileMenu = ({
const blockPromptControl = Prompt.usePromptControl()
const loggedOutWarningPromptControl = Prompt.usePromptControl()
const goLiveDialogControl = useDialogControl()
+ const addToStarterPacksDialogControl = useDialogControl()
const showLoggedOutWarning = React.useMemo(() => {
return (
@@ -300,6 +303,15 @@ let ProfileMenu = ({
)}
>
)}
+
+
+ Add to starter packs
+
+
+
+ {currentAccount && (
+
+ )}
+
Date: Fri, 8 Aug 2025 16:10:35 -0700
Subject: [PATCH 2/8] parameterize the initial profile for starter pack profile
select wizard screen
---
.../Wizard/WizardEditListDialog.tsx | 6 +-
.../StarterPack/Wizard/WizardListCard.tsx | 11 +-
src/components/dialogs/StarterPackDialog.tsx | 112 ++++++++----------
src/lib/generate-starterpack.ts | 23 ++--
src/lib/routes/types.ts | 2 +-
src/screens/StarterPack/Wizard/State.tsx | 17 +--
src/screens/StarterPack/Wizard/index.tsx | 47 +++++---
src/state/queries/actor-starter-packs.ts | 10 ++
8 files changed, 116 insertions(+), 112 deletions(-)
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index 731323f7f..7c3d1a40a 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -48,7 +48,6 @@ export function WizardEditListDialog({
}) {
const {_} = useLingui()
const t = useTheme()
- const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender()
const listRef = useRef(null)
@@ -56,10 +55,7 @@ export function WizardEditListDialog({
const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds
- return [
- profile,
- ...state.profiles.filter(p => p.did !== currentAccount?.did),
- ]
+ return [profile, ...state.profiles.filter(p => p.did !== profile.did)]
}
const renderItem = ({item}: ListRenderItemInfo) =>
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index fbaa185a9..09c265d78 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -131,10 +131,13 @@ export function WizardProfileCard({
}) {
const {currentAccount} = useSession()
- const isMe = profile.did === currentAccount?.did
- const included = isMe || state.profiles.some(p => p.did === profile.did)
+ // Determine the "main" profile for this starter pack - either targetDid or current account
+ const targetProfileDid = state.targetDid || currentAccount?.did
+ const isTarget = profile.did === targetProfileDid
+ const included = isTarget || state.profiles.some(p => p.did === profile.did)
const disabled =
- isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
+ isTarget ||
+ (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName)
@@ -144,7 +147,7 @@ export function WizardProfileCard({
if (disabled) return
Keyboard.dismiss()
- if (profile.did === currentAccount?.did) return
+ if (profile.did === targetProfileDid) return
if (!included) {
dispatch({type: 'AddProfile', profile})
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index efd157723..0570859c4 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -12,7 +12,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
import {
- RQKEY_WITH_MEMBERSHIP,
+ invalidateActorStarterPacksWithMembershipQuery,
useActorStarterPacksWithMembershipsQuery,
} from '#/state/queries/actor-starter-packs'
import {
@@ -35,7 +35,6 @@ import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
type StarterPackWithMembership =
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
-// Simple module-level state for dialog coordination
let dialogCallbacks: {
onSuccess?: () => void
} = {}
@@ -48,14 +47,12 @@ export function notifyDialogSuccess() {
export type StarterPackDialogProps = {
control: Dialog.DialogControlProps
- accountDid: string
targetDid: string
enabled?: boolean
}
export function StarterPackDialog({
control,
- accountDid: _accountDid,
targetDid,
enabled,
}: StarterPackDialogProps) {
@@ -73,8 +70,11 @@ export function StarterPackDialog({
const navToWizard = React.useCallback(() => {
control.close()
- navigation.navigate('StarterPackWizard', {fromDialog: true})
- }, [navigation, control])
+ navigation.navigate('StarterPackWizard', {
+ fromDialog: true,
+ targetDid: targetDid,
+ })
+ }, [navigation, control, targetDid])
const wrappedNavToWizard = requireEmailVerification(navToWizard, {
instructions: [
@@ -85,7 +85,6 @@ export function StarterPackDialog({
})
const onClose = React.useCallback(() => {
- // setCurrentView('initial')
control.close()
}, [control])
@@ -252,69 +251,60 @@ function StarterPackItem({
const {_} = useLingui()
const t = useTheme()
const queryClient = useQueryClient()
- const [isUpdating, setIsUpdating] = React.useState(false)
const starterPack = starterPackWithMembership.starterPack
const isInPack = !!starterPackWithMembership.listItem
- console.log('StarterPackItem render. 111', {
- starterPackWithMembership: starterPackWithMembership.listItem?.subject,
- })
- console.log('StarterPackItem render', {
- starterPackWithMembership,
- })
+ const {mutate: addMembership, isPending: isAddingPending} =
+ useListMembershipAddMutation({
+ onSuccess: () => {
+ Toast.show(_(msg`Added to starter pack`))
+ invalidateActorStarterPacksWithMembershipQuery({
+ queryClient,
+ did: targetDid,
+ })
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
+ },
+ })
+
+ const {mutate: removeMembership, isPending: isRemovingPending} =
+ useListMembershipRemoveMutation({
+ onSuccess: () => {
+ Toast.show(_(msg`Removed from starter pack`))
+ invalidateActorStarterPacksWithMembershipQuery({
+ queryClient,
+ did: targetDid,
+ })
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
+ },
+ })
- const {mutateAsync: addMembership} = useListMembershipAddMutation({
- onSuccess: () => {
- Toast.show(_(msg`Added to starter pack`))
- },
- onError: () => {
- Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
- },
- })
+ const isMutating = isAddingPending || isRemovingPending
- const {mutateAsync: removeMembership} = useListMembershipRemoveMutation({
- onSuccess: () => {
- Toast.show(_(msg`Removed from starter pack`))
- },
- onError: () => {
- Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
- },
- })
-
- const handleToggleMembership = async () => {
- if (!starterPack.list?.uri || isUpdating) return
+ const handleToggleMembership = () => {
+ if (!starterPack.list?.uri || isMutating) return
const listUri = starterPack.list.uri
- setIsUpdating(true)
- try {
- if (!isInPack) {
- await addMembership({
- listUri: listUri,
- actorDid: targetDid,
- })
- } else {
- if (!starterPackWithMembership.listItem?.uri) {
- console.error('Cannot remove: missing membership URI')
- return
- }
- await removeMembership({
- listUri: listUri,
- actorDid: targetDid,
- membershipUri: starterPackWithMembership.listItem.uri,
- })
+ if (!isInPack) {
+ addMembership({
+ listUri: listUri,
+ actorDid: targetDid,
+ })
+ } else {
+ if (!starterPackWithMembership.listItem?.uri) {
+ console.error('Cannot remove: missing membership URI')
+ return
}
-
- await Promise.all([
- queryClient.invalidateQueries({
- queryKey: RQKEY_WITH_MEMBERSHIP(targetDid),
- }),
- ])
- } catch (error) {
- console.error('Failed to toggle membership:', error)
- } finally {
- setIsUpdating(false)
+ removeMembership({
+ listUri: listUri,
+ actorDid: targetDid,
+ membershipUri: starterPackWithMembership.listItem.uri,
+ })
}
}
@@ -377,7 +367,7 @@ function StarterPackItem({
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary'}
size="tiny"
- disabled={isUpdating}
+ disabled={isMutating}
onPress={handleToggleMembership}>
{isInPack ? Remove : Add}
diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts
index 11e334329..76bef3fbe 100644
--- a/src/lib/generate-starterpack.ts
+++ b/src/lib/generate-starterpack.ts
@@ -1,10 +1,10 @@
import {
- $Typed,
- AppBskyActorDefs,
- AppBskyGraphGetStarterPack,
- BskyAgent,
- ComAtprotoRepoApplyWrites,
- Facet,
+ type $Typed,
+ type AppBskyActorDefs,
+ type AppBskyGraphGetStarterPack,
+ type BskyAgent,
+ type ComAtprotoRepoApplyWrites,
+ type Facet,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {useAgent} from '#/state/session'
-import * as bsky from '#/types/bsky'
+import type * as bsky from '#/types/bsky'
export const createStarterPackList = async ({
name,
@@ -46,14 +46,7 @@ export const createStarterPackList = async ({
if (!list) throw new Error('List creation failed')
await agent.com.atproto.repo.applyWrites({
repo: agent.session!.did,
- writes: [
- createListItem({did: agent.session!.did, listUri: list.uri}),
- ].concat(
- profiles
- // Ensure we don't have ourselves in this list twice
- .filter(p => p.did !== agent.session!.did)
- .map(p => createListItem({did: p.did, listUri: list.uri})),
- ),
+ writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})),
})
return list
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 6eb5cb609..f7e7c7eed 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -79,7 +79,7 @@ export type CommonNavigatorParams = {
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
- StarterPackWizard: {fromDialog?: boolean}
+ StarterPackWizard: {fromDialog?: boolean; targetDid?: string}
StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext
}
diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx
index 7fae8ca6d..f34218219 100644
--- a/src/screens/StarterPack/Wizard/State.tsx
+++ b/src/screens/StarterPack/Wizard/State.tsx
@@ -7,7 +7,6 @@ import {
import {msg, plural} from '@lingui/macro'
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
-import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import * as bsky from '#/types/bsky'
@@ -37,6 +36,7 @@ interface State {
processing: boolean
error?: string
transitionDirection: 'Backward' | 'Forward'
+ targetDid?: string
}
type TStateContext = [State, (action: Action) => void]
@@ -118,15 +118,17 @@ function reducer(state: State, action: Action): State {
export function Provider({
starterPack,
listItems,
+ targetProfile,
children,
}: {
starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[]
+ targetProfile: bsky.profile.AnyProfileView
children: React.ReactNode
}) {
- const {currentAccount} = useSession()
-
const createInitialState = (): State => {
+ const targetDid = targetProfile?.did
+
if (
starterPack &&
bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
@@ -136,23 +138,22 @@ export function Provider({
currentStep: 'Details',
name: starterPack.record.name,
description: starterPack.record.description,
- profiles:
- listItems
- ?.map(i => i.subject)
- .filter(p => p.did !== currentAccount?.did) ?? [],
+ profiles: listItems?.map(i => i.subject) ?? [],
feeds: starterPack.feeds ?? [],
processing: false,
transitionDirection: 'Forward',
+ targetDid,
}
}
return {
canNext: true,
currentStep: 'Details',
- profiles: [],
+ profiles: [targetProfile],
feeds: [],
processing: false,
transitionDirection: 'Forward',
+ targetDid,
}
}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index b918e8baf..b2f74257b 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -72,11 +72,15 @@ export function Wizard({
const params = route.params ?? {}
const rkey = 'rkey' in params ? params.rkey : undefined
const fromDialog = 'fromDialog' in params ? params.fromDialog : false
+ const targetDid = 'targetDid' in params ? params.targetDid : undefined
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const {_} = useLingui()
+ // Use targetDid if provided (from dialog), otherwise use current account
+ const profileDid = targetDid || currentAccount!.did
+
const {
data: starterPack,
isLoading: isLoadingStarterPack,
@@ -94,7 +98,7 @@ export function Wizard({
data: profile,
isLoading: isLoadingProfile,
isError: isErrorProfile,
- } = useProfileQuery({did: currentAccount?.did})
+ } = useProfileQuery({did: profileDid})
const isEdit = Boolean(rkey)
const isReady =
@@ -130,7 +134,10 @@ export function Wizard({
-
+
1) ||
@@ -413,20 +418,15 @@ function Container({children}: {children: React.ReactNode}) {
function Footer({
onNext,
nextBtnText,
- profile,
}: {
onNext: () => void
nextBtnText: string
- profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const t = useTheme()
const [state] = useWizardState()
const {bottom: bottomInset} = useSafeAreaInsets()
-
- const items =
- state.currentStep === 'Profiles'
- ? [profile, ...state.profiles]
- : state.feeds
+ const {currentAccount} = useSession()
+ const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
const minimumItems = state.currentStep === 'Profiles' ? 8 : 0
@@ -493,12 +493,23 @@ function Footer({
{
items.length < 2 ? (
- It's just you right now! Add more people to your starter pack
- by searching above.
+ It's just{' '}
+
+ {currentAccount?.did === items[0].did
+ ? 'you'
+ : getName(items[0])}{' '}
+
+ right now! Add more people to your starter pack by searching
+ above.
) : items.length === 2 ? (
- You and
+
+ {currentAccount?.did === items[0].did
+ ? 'you'
+ : getName(items[0])}
+ {' '}
+ and
{getName(items[1] /* [0] is self, skip it */)}{' '}
diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts
index bde719743..d40e05453 100644
--- a/src/state/queries/actor-starter-packs.ts
+++ b/src/state/queries/actor-starter-packs.ts
@@ -90,3 +90,13 @@ export async function invalidateActorStarterPacksQuery({
}) {
await queryClient.invalidateQueries({queryKey: RQKEY(did)})
}
+
+export async function invalidateActorStarterPacksWithMembershipQuery({
+ queryClient,
+ did,
+}: {
+ queryClient: QueryClient
+ did: string
+}) {
+ await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)})
+}
--
2.51.2
From e32f280f472a6793c10f23d6363e3577dfef39db Mon Sep 17 00:00:00 2001
From: Chenyu Huang
Date: Tue, 19 Aug 2025 15:59:12 -0700
Subject: [PATCH 3/8] clean up onSuccess callback
---
src/components/dialogs/StarterPackDialog.tsx | 23 +++++---------------
src/lib/routes/types.ts | 6 ++++-
src/screens/StarterPack/Wizard/index.tsx | 13 +++++------
3 files changed, 16 insertions(+), 26 deletions(-)
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index 0570859c4..9c36be84d 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -35,16 +35,6 @@ import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
type StarterPackWithMembership =
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
-let dialogCallbacks: {
- onSuccess?: () => void
-} = {}
-
-export function notifyDialogSuccess() {
- if (dialogCallbacks.onSuccess) {
- dialogCallbacks.onSuccess()
- }
-}
-
export type StarterPackDialogProps = {
control: Dialog.DialogControlProps
targetDid: string
@@ -60,19 +50,16 @@ export function StarterPackDialog({
const navigation = useNavigation()
const requireEmailVerification = useRequireEmailVerification()
- React.useEffect(() => {
- dialogCallbacks.onSuccess = () => {
- if (!control.isOpen) {
- control.open()
- }
- }
- }, [control])
-
const navToWizard = React.useCallback(() => {
control.close()
navigation.navigate('StarterPackWizard', {
fromDialog: true,
targetDid: targetDid,
+ onSuccess: () => {
+ if (!control.isOpen) {
+ control.open()
+ }
+ },
})
}, [navigation, control, targetDid])
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index f7e7c7eed..1725fdfb4 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -79,7 +79,11 @@ export type CommonNavigatorParams = {
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
- StarterPackWizard: {fromDialog?: boolean; targetDid?: string}
+ StarterPackWizard: {
+ fromDialog?: boolean
+ targetDid?: string
+ onSuccess?: () => void
+ }
StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext
}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index b2f74257b..a871a68a4 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -54,7 +54,6 @@ import {StepProfiles} from '#/screens/StarterPack/Wizard/StepProfiles'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
-import {notifyDialogSuccess} from '#/components/dialogs/StarterPackDialog'
import * as Layout from '#/components/Layout'
import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
@@ -73,6 +72,7 @@ export function Wizard({
const rkey = 'rkey' in params ? params.rkey : undefined
const fromDialog = 'fromDialog' in params ? params.fromDialog : false
const targetDid = 'targetDid' in params ? params.targetDid : undefined
+ const onSuccess = 'onSuccess' in params ? params.onSuccess : undefined
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
@@ -144,6 +144,7 @@ export function Wizard({
profile={profile}
moderationOpts={moderationOpts}
fromDialog={fromDialog}
+ onSuccess={onSuccess}
/>
@@ -156,12 +157,14 @@ function WizardInner({
profile,
moderationOpts,
fromDialog,
+ onSuccess,
}: {
currentStarterPack?: AppBskyGraphDefs.StarterPackView
currentListItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
fromDialog?: boolean
+ onSuccess?: () => void
}) {
const navigation = useNavigation()
const {_} = useLingui()
@@ -231,7 +234,7 @@ function WizardInner({
// If launched from ProfileMenu dialog, notify the dialog and go back
if (fromDialog) {
navigation.goBack()
- notifyDialogSuccess()
+ onSuccess?.()
} else {
// Original behavior for other entry points
navigation.replace('StarterPack', {
@@ -366,11 +369,7 @@ function WizardInner({
{state.currentStep !== 'Details' && (
-
+
)}
Date: Tue, 19 Aug 2025 16:29:18 -0700
Subject: [PATCH 4/8] loading state
---
src/components/dialogs/StarterPackDialog.tsx | 67 +++++++++++++-------
src/screens/StarterPack/Wizard/index.tsx | 18 ++----
src/view/com/profile/ProfileMenu.tsx | 11 ++--
3 files changed, 54 insertions(+), 42 deletions(-)
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index 9c36be84d..f9076001f 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -26,6 +26,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
+import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
@@ -153,6 +154,7 @@ function StarterPackList({
data,
refetch,
isError,
+ isLoading,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
@@ -209,6 +211,14 @@ function StarterPackList({
[_, onStartWizard],
)
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
return (
{
- Toast.show(_(msg`Added to starter pack`))
+ const [isPendingRefresh, setIsPendingRefresh] = React.useState(false)
+
+ 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,
})
- },
- onError: () => {
- Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
- },
- })
+ setIsPendingRefresh(false)
+ }, 1e3)
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to add to starter pack`), 'xmark')
+ setIsPendingRefresh(false)
+ },
+ })
- const {mutate: removeMembership, isPending: isRemovingPending} =
- useListMembershipRemoveMutation({
- onSuccess: () => {
- Toast.show(_(msg`Removed from starter pack`))
+ 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,
})
- },
- onError: () => {
- Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
- },
- })
-
- const isMutating = isAddingPending || isRemovingPending
+ setIsPendingRefresh(false)
+ }, 1e3)
+ },
+ onError: () => {
+ Toast.show(_(msg`Failed to remove from starter pack`), 'xmark')
+ setIsPendingRefresh(false)
+ },
+ })
const handleToggleMembership = () => {
- if (!starterPack.list?.uri || isMutating) return
+ if (!starterPack.list?.uri || isPendingRefresh) return
const listUri = starterPack.list.uri
+ setIsPendingRefresh(true)
+
if (!isInPack) {
addMembership({
listUri: listUri,
@@ -285,6 +307,7 @@ function StarterPackItem({
} else {
if (!starterPackWithMembership.listItem?.uri) {
console.error('Cannot remove: missing membership URI')
+ setIsPendingRefresh(false)
return
}
removeMembership({
@@ -354,7 +377,7 @@ function StarterPackItem({
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
color={isInPack ? 'secondary' : 'primary'}
size="tiny"
- disabled={isMutating}
+ disabled={isPendingRefresh}
onPress={handleToggleMembership}>
{isInPack ? Remove : Add}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index a871a68a4..e823f8bb7 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -231,12 +231,10 @@ function WizardInner({
Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
dispatch({type: 'SetProcessing', processing: false})
- // If launched from ProfileMenu dialog, notify the dialog and go back
if (fromDialog) {
navigation.goBack()
onSuccess?.()
} else {
- // Original behavior for other entry points
navigation.replace('StarterPack', {
name: profile!.handle,
rkey,
@@ -246,19 +244,13 @@ function WizardInner({
}
const onSuccessEdit = () => {
- // If launched from ProfileMenu dialog, go back to stay on profile page
- if (fromDialog) {
+ if (navigation.canGoBack()) {
navigation.goBack()
} else {
- // Original behavior for other entry points
- if (navigation.canGoBack()) {
- navigation.goBack()
- } else {
- navigation.replace('StarterPack', {
- name: profile!.handle,
- rkey: parsed!.rkey,
- })
- }
+ navigation.replace('StarterPack', {
+ name: currentAccount!.handle,
+ rkey: parsed!.rkey,
+ })
}
}
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index 569823da6..df8b2e481 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -452,13 +452,10 @@ let ProfileMenu = ({
- {currentAccount && (
-
- )}
+
Date: Wed, 20 Aug 2025 12:31:12 -0700
Subject: [PATCH 5/8] use Dialog.InnerFlatlist
---
src/components/dialogs/StarterPackDialog.tsx | 171 ++++++++++---------
1 file changed, 89 insertions(+), 82 deletions(-)
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index f9076001f..a8e775958 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -19,7 +19,6 @@ import {
useListMembershipAddMutation,
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
-import {List} from '#/view/com/util/List'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
@@ -72,38 +71,15 @@ export function StarterPackDialog({
],
})
- const onClose = React.useCallback(() => {
- control.close()
- }, [control])
-
- const t = useTheme()
-
return (
-
-
-
-
- Add to starter packs
-
-
-
-
-
-
-
+
)
}
@@ -113,8 +89,7 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
const t = useTheme()
return (
-
+
void}) {
-
+
+
+
)
}
function StarterPackList({
+ control,
onStartWizard,
targetDid,
enabled,
}: {
+ control: Dialog.DialogControlProps
onStartWizard: () => void
targetDid: string
enabled?: boolean
}) {
const {_} = useLingui()
+ const t = useTheme()
const {
data,
@@ -187,54 +167,81 @@ function StarterPackList({
[targetDid],
)
- const ListHeaderComponent = React.useCallback(
- () => (
- <>
-
-
- New starter pack
-
-
-
-
- >
- ),
- [_, onStartWizard],
+ const onClose = React.useCallback(() => {
+ control.close()
+ }, [control])
+
+ const listHeader = (
+ <>
+
+
+ Add to starter packs
+
+
+
+ {membershipItems.length > 0 && (
+ <>
+
+
+ New starter pack
+
+
+
+
+ >
+ )}
+ >
)
if (isLoading) {
return (
-
-
-
+ <>
+
+
+
+
+ >
)
}
return (
-
- item.starterPack.uri || index.toString()
- }
- refreshing={false}
- onRefresh={_onRefresh}
- onEndReached={_onEndReached}
- onEndReachedThreshold={0.1}
- ListHeaderComponent={
- membershipItems.length > 0 ? ListHeaderComponent : null
- }
- ListEmptyComponent={}
- />
+ <>
+
+
+ item.starterPack.uri || index.toString()
+ }
+ refreshing={false}
+ onRefresh={_onRefresh}
+ onEndReached={_onEndReached}
+ onEndReachedThreshold={0.1}
+ ListHeaderComponent={listHeader}
+ ListEmptyComponent={}
+ contentContainerStyle={[a.px_2xl, a.pt_lg]}
+ />
+ >
)
}
--
2.51.2
From 0474a3b6f7706d9881f8f7544be343459ecc4035 Mon Sep 17 00:00:00 2001
From: Chenyu Huang
Date: Wed, 20 Aug 2025 15:00:06 -0700
Subject: [PATCH 6/8] address feedback
---
src/components/dialogs/StarterPackDialog.tsx | 41 +++++++------
src/screens/StarterPack/Wizard/index.tsx | 60 ++++++++++++--------
2 files changed, 57 insertions(+), 44 deletions(-)
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index a8e775958..443b84205 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -4,7 +4,7 @@ import {
type AppBskyGraphGetStarterPacksWithMembership,
AppBskyGraphStarterpack,
} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -20,7 +20,6 @@ import {
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
import * as Toast from '#/view/com/util/Toast'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -28,6 +27,7 @@ import {Divider} from '#/components/Divider'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
+import {AvatarStack} from '../AvatarStack'
import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
import {StarterPack} from '../icons/StarterPack'
import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
@@ -107,7 +107,9 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
size="small"
onPress={onStartWizard}>
- Create
+
+ Create
+
@@ -200,7 +202,9 @@ function StarterPackList({
size="small"
onPress={onStartWizard}>
- Create
+
+ Create
+
@@ -347,22 +351,12 @@ function StarterPackItem({
{starterPack.listItemsSample &&
starterPack.listItemsSample.length > 0 && (
<>
- {starterPack.listItemsSample?.slice(0, 4).map((p, index) => (
- 0 ? -2 : 0,
- borderWidth: 0.5,
- borderColor: t.atoms.bg.backgroundColor,
- },
- ]}
- />
- ))}
+ p.subject)}
+ />
{starterPack.list?.listItemCount &&
starterPack.list.listItemCount > 4 && (
@@ -372,7 +366,12 @@ function StarterPackItem({
t.atoms.text_contrast_medium,
a.ml_xs,
]}>
- {`+${starterPack.list.listItemCount - 4} more`}
+
+
+
)}
>
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index e823f8bb7..651f8f77f 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -483,30 +483,44 @@ function Footer({
{
items.length < 2 ? (
-
- It's just{' '}
-
- {currentAccount?.did === items[0].did
- ? 'you'
- : getName(items[0])}{' '}
-
- right now! Add more people to your starter pack by searching
- above.
-
+ currentAccount?.did === items[0].did ? (
+
+ It's just you right now! Add more people to your starter
+ pack by searching above.
+
+ ) : (
+
+ It's just{' '}
+
+ {getName(items[0])}{' '}
+
+ right now! Add more people to your starter pack by searching
+ above.
+
+ )
) : items.length === 2 ? (
-
-
- {currentAccount?.did === items[0].did
- ? 'you'
- : getName(items[0])}
- {' '}
- and
-
-
- {getName(items[1] /* [0] is self, skip it */)}{' '}
-
- are included in your starter pack
-
+ currentAccount?.did === items[0].did ? (
+
+ You and
+
+
+ {getName(items[1] /* [0] is self, skip it */)}{' '}
+
+ are included in your starter pack
+
+ ) : (
+
+
+ {getName(items[0])}
+ {' '}
+ and
+
+
+ {getName(items[1] /* [0] is self, skip it */)}{' '}
+
+ are included in your starter pack
+
+ )
) : items.length > 2 ? (
--
2.51.2
From 962654041c9de253dfd66beb98be655acc655a08 Mon Sep 17 00:00:00 2001
From: Chenyu Huang
Date: Thu, 21 Aug 2025 11:49:08 -0700
Subject: [PATCH 7/8] fix linting, minor ui tweaks
---
.../Wizard/WizardEditListDialog.tsx | 1 -
src/components/dialogs/StarterPackDialog.tsx | 43 ++++++++-----------
2 files changed, 18 insertions(+), 26 deletions(-)
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index 7c3d1a40a..7dfde900f 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -11,7 +11,6 @@ import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isWeb} from '#/platform/detection'
-import {useSession} from '#/state/session'
import {type ListMethods} from '#/view/com/util/List'
import {
type WizardAction,
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index 443b84205..12dfc3d24 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -30,7 +30,7 @@ import * as bsky from '#/types/bsky'
import {AvatarStack} from '../AvatarStack'
import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
import {StarterPack} from '../icons/StarterPack'
-import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
+import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '../icons/Times'
type StarterPackWithMembership =
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
@@ -130,8 +130,6 @@ function StarterPackList({
enabled?: boolean
}) {
const {_} = useLingui()
- const t = useTheme()
-
const {
data,
refetch,
@@ -184,10 +182,9 @@ function StarterPackList({
Add to starter packs
-
+
{membershipItems.length > 0 && (
<>
@@ -215,27 +212,23 @@ function StarterPackList({
>
)
- if (isLoading) {
- return (
- <>
-
-
-
-
- >
- )
- }
-
return (
<>
-
- item.starterPack.uri || index.toString()
+ data={isLoading ? [{}] : membershipItems}
+ renderItem={
+ isLoading
+ ? () => (
+
+
+
+ )
+ : renderItem
+ }
+ keyExtractor={
+ isLoading
+ ? () => 'starter_pack_dialog_loader'
+ : (item: StarterPackWithMembership) => item.starterPack.uri
}
refreshing={false}
onRefresh={_onRefresh}
--
2.51.2
From f84a75669157ed5d2dab2a426d9ff5fefb86a9bb Mon Sep 17 00:00:00 2001
From: Chenyu Huang
Date: Thu, 21 Aug 2025 17:52:02 -0700
Subject: [PATCH 8/8] optimize for the web
---
src/components/dialogs/StarterPackDialog.tsx | 78 +++++++++++---------
src/screens/StarterPack/Wizard/index.tsx | 1 -
2 files changed, 45 insertions(+), 34 deletions(-)
diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx
index 12dfc3d24..ec041d401 100644
--- a/src/components/dialogs/StarterPackDialog.tsx
+++ b/src/components/dialogs/StarterPackDialog.tsx
@@ -11,6 +11,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
+import {isWeb} from '#/platform/detection'
import {
invalidateActorStarterPacksWithMembershipQuery,
useActorStarterPacksWithMembershipsQuery,
@@ -30,7 +31,7 @@ import * as bsky from '#/types/bsky'
import {AvatarStack} from '../AvatarStack'
import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus'
import {StarterPack} from '../icons/StarterPack'
-import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '../icons/Times'
+import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
type StarterPackWithMembership =
AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership
@@ -56,9 +57,11 @@ export function StarterPackDialog({
fromDialog: true,
targetDid: targetDid,
onSuccess: () => {
- if (!control.isOpen) {
- control.open()
- }
+ setTimeout(() => {
+ if (!control.isOpen) {
+ control.open()
+ }
+ }, 0)
},
})
}, [navigation, control, targetDid])
@@ -88,8 +91,9 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
const {_} = useLingui()
const t = useTheme()
+ isWeb
return (
-
+
{
+ return (
+
+ )
+ }, [t])
+
const listHeader = (
<>
Add to starter packs
{membershipItems.length > 0 && (
@@ -213,32 +227,30 @@ function StarterPackList({
)
return (
- <>
- (
-
-
-
- )
- : renderItem
- }
- keyExtractor={
- isLoading
- ? () => 'starter_pack_dialog_loader'
- : (item: StarterPackWithMembership) => item.starterPack.uri
- }
- refreshing={false}
- onRefresh={_onRefresh}
- onEndReached={_onEndReached}
- onEndReachedThreshold={0.1}
- ListHeaderComponent={listHeader}
- ListEmptyComponent={}
- contentContainerStyle={[a.px_2xl, a.pt_lg]}
- />
- >
+ (
+
+
+
+ )
+ : renderItem
+ }
+ keyExtractor={
+ isLoading
+ ? () => 'starter_pack_dialog_loader'
+ : (item: StarterPackWithMembership) => item.starterPack.uri
+ }
+ refreshing={false}
+ onRefresh={_onRefresh}
+ onEndReached={_onEndReached}
+ onEndReachedThreshold={0.1}
+ ListHeaderComponent={listHeader}
+ ListEmptyComponent={}
+ style={isWeb ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]}
+ />
)
}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 651f8f77f..839faf9aa 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -283,7 +283,6 @@ function WizardInner({
currentListItems: currentListItems,
})
} else {
- console.log('Creating new starter pack: ', state.profiles)
createStarterPack({
name: state.name?.trim() || getDefaultName(),
description: state.description?.trim(),