From 7182cd3d5e157d7ad80f2e5c4a458730c46939a0 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Fri, 8 Aug 2025 15:33:45 -0700
Subject: starter pack dialog flow from profileMenu

---
 src/components/dialogs/StarterPackDialog.tsx | 388 +++++++++++++++++++++++++++
 1 file changed, 388 insertions(+)
 create mode 100644 src/components/dialogs/StarterPackDialog.tsx

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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<NavigationProp>()
+  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: [
+      <Trans key="nav">
+        Before creating a starter pack, you must first verify your email.
+      </Trans>,
+    ],
+  })
+
+  const onClose = React.useCallback(() => {
+    // setCurrentView('initial')
+    control.close()
+  }, [control])
+
+  const t = useTheme()
+
+  return (
+    <Dialog.Outer control={control}>
+      <Dialog.Handle />
+      <Dialog.Inner label={_(msg`Add to starter packs`)} style={[a.w_full]}>
+        <View>
+          <View
+            style={[
+              {justifyContent: 'space-between', flexDirection: 'row'},
+              a.my_lg,
+            ]}>
+            <Text style={[a.text_lg, a.font_bold]}>
+              <Trans>Add to starter packs</Trans>
+            </Text>
+            <TimesLarge_Stroke2_Corner0_Rounded
+              onPress={onClose}
+              fill={t.atoms.text_contrast_medium.color}
+            />
+          </View>
+
+          <StarterPackList
+            onStartWizard={wrappedNavToWizard}
+            targetDid={targetDid}
+            enabled={enabled}
+          />
+        </View>
+      </Dialog.Inner>
+    </Dialog.Outer>
+  )
+}
+
+function Empty({onStartWizard}: {onStartWizard: () => void}) {
+  const {_} = useLingui()
+  const t = useTheme()
+
+  return (
+    <View
+      style={[a.align_center, a.gap_2xl, {paddingTop: 64, paddingBottom: 64}]}>
+      <View style={[a.gap_xs, a.align_center]}>
+        <StarterPack
+          width={48}
+          fill={t.atoms.border_contrast_medium.borderColor}
+        />
+        <Text style={[a.text_center]}>
+          <Trans>You have no starter packs.</Trans>
+        </Text>
+      </View>
+
+      <Button
+        label={_(msg`Create starter pack`)}
+        color="secondary_inverted"
+        size="small"
+        onPress={onStartWizard}>
+        <ButtonText>
+          <Trans>Create</Trans>
+        </ButtonText>
+        <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
+      </Button>
+    </View>
+  )
+}
+
+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}) => (
+      <StarterPackItem starterPackWithMembership={item} targetDid={targetDid} />
+    ),
+    [targetDid],
+  )
+
+  const ListHeaderComponent = React.useCallback(
+    () => (
+      <>
+        <View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
+          <Text style={[a.text_md, a.font_bold]}>
+            <Trans>New starter pack</Trans>
+          </Text>
+          <Button
+            label={_(msg`Create starter pack`)}
+            color="secondary_inverted"
+            size="small"
+            onPress={onStartWizard}>
+            <ButtonText>
+              <Trans>Create</Trans>
+            </ButtonText>
+            <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
+          </Button>
+        </View>
+        <Divider />
+      </>
+    ),
+    [_, onStartWizard],
+  )
+
+  return (
+    <List
+      data={membershipItems}
+      renderItem={renderItem}
+      keyExtractor={(item: StarterPackWithMembership, index: number) =>
+        item.starterPack.uri || index.toString()
+      }
+      refreshing={false}
+      onRefresh={_onRefresh}
+      onEndReached={_onEndReached}
+      onEndReachedThreshold={0.1}
+      ListHeaderComponent={
+        membershipItems.length > 0 ? ListHeaderComponent : null
+      }
+      ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
+    />
+  )
+}
+
+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<AppBskyGraphStarterpack.Record>(
+      record,
+      AppBskyGraphStarterpack.isRecord,
+    )
+  ) {
+    return null
+  }
+
+  return (
+    <View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
+      <View>
+        <Text emoji style={[a.text_md, a.font_bold]} numberOfLines={1}>
+          {record.name}
+        </Text>
+
+        <View style={[a.flex_row, a.align_center, a.mt_xs]}>
+          {starterPack.listItemsSample &&
+            starterPack.listItemsSample.length > 0 && (
+              <>
+                {starterPack.listItemsSample?.slice(0, 4).map((p, index) => (
+                  <UserAvatar
+                    key={p.subject.did}
+                    avatar={p.subject.avatar}
+                    size={32}
+                    type={'user'}
+                    style={[
+                      {
+                        zIndex: 1 - index,
+                        marginLeft: index > 0 ? -2 : 0,
+                        borderWidth: 0.5,
+                        borderColor: t.atoms.bg.backgroundColor,
+                      },
+                    ]}
+                  />
+                ))}
+
+                {starterPack.list?.listItemCount &&
+                  starterPack.list.listItemCount > 4 && (
+                    <Text
+                      style={[
+                        a.text_sm,
+                        t.atoms.text_contrast_medium,
+                        a.ml_xs,
+                      ]}>
+                      {`+${starterPack.list.listItemCount - 4} more`}
+                    </Text>
+                  )}
+              </>
+            )}
+        </View>
+      </View>
+
+      <Button
+        label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
+        color={isInPack ? 'secondary' : 'primary'}
+        size="tiny"
+        disabled={isUpdating}
+        onPress={handleToggleMembership}>
+        <ButtonText>
+          {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
+        </ButtonText>
+      </Button>
+    </View>
+  )
+}
-- 
cgit 1.4.1


From f42b5831bb831e3b9f925730477a27e01d2b33f4 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Fri, 8 Aug 2025 16:10:35 -0700
Subject: parameterize the initial profile for starter pack profile select
 wizard screen

---
 .../StarterPack/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(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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<ListMethods>(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<any>) =>
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}>
         <ButtonText>
           {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
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({
     <Layout.Screen
       testID="starterPackWizardScreen"
       style={web([{minHeight: 0}, a.flex_1])}>
-      <Provider starterPack={starterPack} listItems={listItems}>
+      <Provider
+        starterPack={starterPack}
+        listItems={listItems}
+        targetProfile={profile}>
         <WizardInner
           currentStarterPack={starterPack}
           currentListItems={listItems}
@@ -228,7 +235,7 @@ function WizardInner({
     } else {
       // Original behavior for other entry points
       navigation.replace('StarterPack', {
-        name: currentAccount!.handle,
+        name: profile!.handle,
         rkey,
         new: true,
       })
@@ -245,7 +252,7 @@ function WizardInner({
         navigation.goBack()
       } else {
         navigation.replace('StarterPack', {
-          name: currentAccount!.handle,
+          name: profile!.handle,
           rkey: parsed!.rkey,
         })
       }
@@ -281,6 +288,7 @@ function WizardInner({
         currentListItems: currentListItems,
       })
     } else {
+      console.log('Creating new starter pack: ', state.profiles)
       createStarterPack({
         name: state.name?.trim() || getDefaultName(),
         description: state.description?.trim(),
@@ -306,10 +314,7 @@ function WizardInner({
     )
   }
 
-  const items =
-    state.currentStep === 'Profiles'
-      ? [profile, ...state.profiles]
-      : state.feeds
+  const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds
 
   const isEditEnabled =
     (state.currentStep === 'Profiles' && items.length > 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 ? (
                 <Trans>
-                  It's just you right now! Add more people to your starter pack
-                  by searching above.
+                  It's just{' '}
+                  <Text style={[a.font_bold, textStyles]} emoji>
+                    {currentAccount?.did === items[0].did
+                      ? 'you'
+                      : getName(items[0])}{' '}
+                  </Text>
+                  right now! Add more people to your starter pack by searching
+                  above.
                 </Trans>
               ) : items.length === 2 ? (
                 <Trans>
-                  <Text style={[a.font_bold, textStyles]}>You</Text> and
+                  <Text style={[a.font_bold, textStyles]}>
+                    {currentAccount?.did === items[0].did
+                      ? 'you'
+                      : getName(items[0])}
+                  </Text>{' '}
+                  and
                   <Text> </Text>
                   <Text style={[a.font_bold, textStyles]} emoji>
                     {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)})
+}
-- 
cgit 1.4.1


From e32f280f472a6793c10f23d6363e3577dfef39db Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Tue, 19 Aug 2025 15:59:12 -0700
Subject: 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(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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<NavigationProp>()
   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}
         />
       </Provider>
     </Layout.Screen>
@@ -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<NavigationProp>()
   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({
       </Container>
 
       {state.currentStep !== 'Details' && (
-        <Footer
-          onNext={onNext}
-          nextBtnText={currUiStrings.nextBtn}
-          profile={profile}
-        />
+        <Footer onNext={onNext} nextBtnText={currUiStrings.nextBtn} />
       )}
       <WizardEditListDialog
         control={editDialogControl}
-- 
cgit 1.4.1


From 68e0a78f70478f1398f1fe65657257a9f2cd1d08 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Tue, 19 Aug 2025 16:29:18 -0700
Subject: 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(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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 (
+      <View style={[a.align_center, a.p_xl]}>
+        <Loader size="xl" />
+      </View>
+    )
+  }
+
   return (
     <List
       data={membershipItems}
@@ -242,41 +252,53 @@ function StarterPackItem({
   const starterPack = starterPackWithMembership.starterPack
   const isInPack = !!starterPackWithMembership.listItem
 
-  const {mutate: addMembership, isPending: isAddingPending} =
-    useListMembershipAddMutation({
-      onSuccess: () => {
-        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}>
         <ButtonText>
           {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
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 = ({
         </Menu.Outer>
       </Menu.Root>
 
-      {currentAccount && (
-        <StarterPackDialog
-          control={addToStarterPacksDialogControl}
-          accountDid={currentAccount.did}
-          targetDid={profile.did}
-        />
-      )}
+      <StarterPackDialog
+        control={addToStarterPacksDialogControl}
+        targetDid={profile.did}
+      />
 
       <ReportDialog
         control={reportDialogControl}
-- 
cgit 1.4.1


From f7c05c2df44bec9feebbcb8f888cdcb49d6095d3 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Wed, 20 Aug 2025 12:31:12 -0700
Subject: use Dialog.InnerFlatlist

---
 src/components/dialogs/StarterPackDialog.tsx | 171 ++++++++++++++-------------
 1 file changed, 89 insertions(+), 82 deletions(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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 (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-      <Dialog.Inner label={_(msg`Add to starter packs`)} style={[a.w_full]}>
-        <View>
-          <View
-            style={[
-              {justifyContent: 'space-between', flexDirection: 'row'},
-              a.my_lg,
-            ]}>
-            <Text style={[a.text_lg, a.font_bold]}>
-              <Trans>Add to starter packs</Trans>
-            </Text>
-            <TimesLarge_Stroke2_Corner0_Rounded
-              onPress={onClose}
-              fill={t.atoms.text_contrast_medium.color}
-            />
-          </View>
-
-          <StarterPackList
-            onStartWizard={wrappedNavToWizard}
-            targetDid={targetDid}
-            enabled={enabled}
-          />
-        </View>
-      </Dialog.Inner>
+      <StarterPackList
+        control={control}
+        onStartWizard={wrappedNavToWizard}
+        targetDid={targetDid}
+        enabled={enabled}
+      />
     </Dialog.Outer>
   )
 }
@@ -113,8 +89,7 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
   const t = useTheme()
 
   return (
-    <View
-      style={[a.align_center, a.gap_2xl, {paddingTop: 64, paddingBottom: 64}]}>
+    <View style={[a.gap_2xl, {paddingTop: 64}]}>
       <View style={[a.gap_xs, a.align_center]}>
         <StarterPack
           width={48}
@@ -125,30 +100,35 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
         </Text>
       </View>
 
-      <Button
-        label={_(msg`Create starter pack`)}
-        color="secondary_inverted"
-        size="small"
-        onPress={onStartWizard}>
-        <ButtonText>
-          <Trans>Create</Trans>
-        </ButtonText>
-        <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
-      </Button>
+      <View style={[a.align_center]}>
+        <Button
+          label={_(msg`Create starter pack`)}
+          color="secondary_inverted"
+          size="small"
+          onPress={onStartWizard}>
+          <ButtonText>
+            <Trans>Create</Trans>
+          </ButtonText>
+          <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
+        </Button>
+      </View>
     </View>
   )
 }
 
 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(
-    () => (
-      <>
-        <View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
-          <Text style={[a.text_md, a.font_bold]}>
-            <Trans>New starter pack</Trans>
-          </Text>
-          <Button
-            label={_(msg`Create starter pack`)}
-            color="secondary_inverted"
-            size="small"
-            onPress={onStartWizard}>
-            <ButtonText>
-              <Trans>Create</Trans>
-            </ButtonText>
-            <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
-          </Button>
-        </View>
-        <Divider />
-      </>
-    ),
-    [_, onStartWizard],
+  const onClose = React.useCallback(() => {
+    control.close()
+  }, [control])
+
+  const listHeader = (
+    <>
+      <View
+        style={[
+          {justifyContent: 'space-between', flexDirection: 'row'},
+          a.my_lg,
+          a.align_center,
+        ]}>
+        <Text style={[a.text_lg, a.font_bold]}>
+          <Trans>Add to starter packs</Trans>
+        </Text>
+        <TimesLarge_Stroke2_Corner0_Rounded
+          onPress={onClose}
+          fill={t.atoms.text_contrast_medium.color}
+        />
+      </View>
+      {membershipItems.length > 0 && (
+        <>
+          <View
+            style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}>
+            <Text style={[a.text_md, a.font_bold]}>
+              <Trans>New starter pack</Trans>
+            </Text>
+            <Button
+              label={_(msg`Create starter pack`)}
+              color="secondary_inverted"
+              size="small"
+              onPress={onStartWizard}>
+              <ButtonText>
+                <Trans>Create</Trans>
+              </ButtonText>
+              <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
+            </Button>
+          </View>
+          <Divider />
+        </>
+      )}
+    </>
   )
 
   if (isLoading) {
     return (
-      <View style={[a.align_center, a.p_xl]}>
-        <Loader size="xl" />
-      </View>
+      <>
+        <Dialog.Close />
+        <Dialog.ScrollableInner
+          contentContainerStyle={[a.align_center, a.p_xl]}
+          label={_(msg`Add to starter packs`)}>
+          <Loader size="xl" />
+        </Dialog.ScrollableInner>
+      </>
     )
   }
 
   return (
-    <List
-      data={membershipItems}
-      renderItem={renderItem}
-      keyExtractor={(item: StarterPackWithMembership, index: number) =>
-        item.starterPack.uri || index.toString()
-      }
-      refreshing={false}
-      onRefresh={_onRefresh}
-      onEndReached={_onEndReached}
-      onEndReachedThreshold={0.1}
-      ListHeaderComponent={
-        membershipItems.length > 0 ? ListHeaderComponent : null
-      }
-      ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
-    />
+    <>
+      <Dialog.Close />
+      <Dialog.InnerFlatList
+        data={membershipItems}
+        renderItem={renderItem}
+        keyExtractor={(item: StarterPackWithMembership, index: number) =>
+          item.starterPack.uri || index.toString()
+        }
+        refreshing={false}
+        onRefresh={_onRefresh}
+        onEndReached={_onEndReached}
+        onEndReachedThreshold={0.1}
+        ListHeaderComponent={listHeader}
+        ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
+        contentContainerStyle={[a.px_2xl, a.pt_lg]}
+      />
+    </>
   )
 }
 
-- 
cgit 1.4.1


From 0474a3b6f7706d9881f8f7544be343459ecc4035 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Wed, 20 Aug 2025 15:00:06 -0700
Subject: address feedback

---
 src/components/dialogs/StarterPackDialog.tsx | 41 ++++++++++---------
 src/screens/StarterPack/Wizard/index.tsx     | 60 +++++++++++++++++-----------
 2 files changed, 57 insertions(+), 44 deletions(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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}>
           <ButtonText>
-            <Trans>Create</Trans>
+            <Trans comment="Text on button to create a new starter pack">
+              Create
+            </Trans>
           </ButtonText>
           <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
         </Button>
@@ -200,7 +202,9 @@ function StarterPackList({
               size="small"
               onPress={onStartWizard}>
               <ButtonText>
-                <Trans>Create</Trans>
+                <Trans comment="Text on button to create a new starter pack">
+                  Create
+                </Trans>
               </ButtonText>
               <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} />
             </Button>
@@ -347,22 +351,12 @@ function StarterPackItem({
           {starterPack.listItemsSample &&
             starterPack.listItemsSample.length > 0 && (
               <>
-                {starterPack.listItemsSample?.slice(0, 4).map((p, index) => (
-                  <UserAvatar
-                    key={p.subject.did}
-                    avatar={p.subject.avatar}
-                    size={32}
-                    type={'user'}
-                    style={[
-                      {
-                        zIndex: 1 - index,
-                        marginLeft: index > 0 ? -2 : 0,
-                        borderWidth: 0.5,
-                        borderColor: t.atoms.bg.backgroundColor,
-                      },
-                    ]}
-                  />
-                ))}
+                <AvatarStack
+                  size={32}
+                  profiles={starterPack.listItemsSample
+                    ?.slice(0, 4)
+                    .map(p => 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`}
+                      <Trans>
+                        <Plural
+                          value={starterPack.list.listItemCount - 4}
+                          other="+# more"
+                        />
+                      </Trans>
                     </Text>
                   )}
               </>
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({
           <Text style={[a.text_center, textStyles]}>
             {
               items.length < 2 ? (
-                <Trans>
-                  It's just{' '}
-                  <Text style={[a.font_bold, textStyles]} emoji>
-                    {currentAccount?.did === items[0].did
-                      ? 'you'
-                      : getName(items[0])}{' '}
-                  </Text>
-                  right now! Add more people to your starter pack by searching
-                  above.
-                </Trans>
+                currentAccount?.did === items[0].did ? (
+                  <Trans>
+                    It's just you right now! Add more people to your starter
+                    pack by searching above.
+                  </Trans>
+                ) : (
+                  <Trans>
+                    It's just{' '}
+                    <Text style={[a.font_bold, textStyles]} emoji>
+                      {getName(items[0])}{' '}
+                    </Text>
+                    right now! Add more people to your starter pack by searching
+                    above.
+                  </Trans>
+                )
               ) : items.length === 2 ? (
-                <Trans>
-                  <Text style={[a.font_bold, textStyles]}>
-                    {currentAccount?.did === items[0].did
-                      ? 'you'
-                      : getName(items[0])}
-                  </Text>{' '}
-                  and
-                  <Text> </Text>
-                  <Text style={[a.font_bold, textStyles]} emoji>
-                    {getName(items[1] /* [0] is self, skip it */)}{' '}
-                  </Text>
-                  are included in your starter pack
-                </Trans>
+                currentAccount?.did === items[0].did ? (
+                  <Trans>
+                    <Text style={[a.font_bold, textStyles]}>You</Text> and
+                    <Text> </Text>
+                    <Text style={[a.font_bold, textStyles]} emoji>
+                      {getName(items[1] /* [0] is self, skip it */)}{' '}
+                    </Text>
+                    are included in your starter pack
+                  </Trans>
+                ) : (
+                  <Trans>
+                    <Text style={[a.font_bold, textStyles]}>
+                      {getName(items[0])}
+                    </Text>{' '}
+                    and
+                    <Text> </Text>
+                    <Text style={[a.font_bold, textStyles]} emoji>
+                      {getName(items[1] /* [0] is self, skip it */)}{' '}
+                    </Text>
+                    are included in your starter pack
+                  </Trans>
+                )
               ) : items.length > 2 ? (
                 <Trans context="profiles">
                   <Text style={[a.font_bold, textStyles]} emoji>
-- 
cgit 1.4.1


From 962654041c9de253dfd66beb98be655acc655a08 Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Thu, 21 Aug 2025 11:49:08 -0700
Subject: fix linting, minor ui tweaks

---
 .../StarterPack/Wizard/WizardEditListDialog.tsx    |  1 -
 src/components/dialogs/StarterPackDialog.tsx       | 43 +++++++++-------------
 2 files changed, 18 insertions(+), 26 deletions(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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({
         <Text style={[a.text_lg, a.font_bold]}>
           <Trans>Add to starter packs</Trans>
         </Text>
-        <TimesLarge_Stroke2_Corner0_Rounded
-          onPress={onClose}
-          fill={t.atoms.text_contrast_medium.color}
-        />
+        <Button label={_(msg`Close`)} onPress={onClose}>
+          <ButtonIcon icon={XIcon} />
+        </Button>
       </View>
       {membershipItems.length > 0 && (
         <>
@@ -215,27 +212,23 @@ function StarterPackList({
     </>
   )
 
-  if (isLoading) {
-    return (
-      <>
-        <Dialog.Close />
-        <Dialog.ScrollableInner
-          contentContainerStyle={[a.align_center, a.p_xl]}
-          label={_(msg`Add to starter packs`)}>
-          <Loader size="xl" />
-        </Dialog.ScrollableInner>
-      </>
-    )
-  }
-
   return (
     <>
-      <Dialog.Close />
       <Dialog.InnerFlatList
-        data={membershipItems}
-        renderItem={renderItem}
-        keyExtractor={(item: StarterPackWithMembership, index: number) =>
-          item.starterPack.uri || index.toString()
+        data={isLoading ? [{}] : membershipItems}
+        renderItem={
+          isLoading
+            ? () => (
+                <View style={[a.align_center, a.py_2xl]}>
+                  <Loader size="xl" />
+                </View>
+              )
+            : renderItem
+        }
+        keyExtractor={
+          isLoading
+            ? () => 'starter_pack_dialog_loader'
+            : (item: StarterPackWithMembership) => item.starterPack.uri
         }
         refreshing={false}
         onRefresh={_onRefresh}
-- 
cgit 1.4.1


From f84a75669157ed5d2dab2a426d9ff5fefb86a9bb Mon Sep 17 00:00:00 2001
From: Chenyu Huang <itschenyu@gmail.com>
Date: Thu, 21 Aug 2025 17:52:02 -0700
Subject: optimize for the web

---
 src/components/dialogs/StarterPackDialog.tsx | 78 ++++++++++++++++------------
 src/screens/StarterPack/Wizard/index.tsx     |  1 -
 2 files changed, 45 insertions(+), 34 deletions(-)

(limited to 'src/components/dialogs/StarterPackDialog.tsx')

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 (
-    <View style={[a.gap_2xl, {paddingTop: 64}]}>
+    <View style={[a.gap_2xl, {paddingTop: isWeb ? 100 : 64}]}>
       <View style={[a.gap_xs, a.align_center]}>
         <StarterPack
           width={48}
@@ -130,6 +134,8 @@ function StarterPackList({
   enabled?: boolean
 }) {
   const {_} = useLingui()
+  const t = useTheme()
+
   const {
     data,
     refetch,
@@ -171,19 +177,27 @@ function StarterPackList({
     control.close()
   }, [control])
 
+  const XIcon = React.useMemo(() => {
+    return (
+      <TimesLarge_Stroke2_Corner0_Rounded
+        fill={t.atoms.text_contrast_medium.color}
+      />
+    )
+  }, [t])
+
   const listHeader = (
     <>
       <View
         style={[
           {justifyContent: 'space-between', flexDirection: 'row'},
-          a.my_lg,
+          isWeb ? a.mb_2xl : a.my_lg,
           a.align_center,
         ]}>
         <Text style={[a.text_lg, a.font_bold]}>
           <Trans>Add to starter packs</Trans>
         </Text>
         <Button label={_(msg`Close`)} onPress={onClose}>
-          <ButtonIcon icon={XIcon} />
+          <ButtonIcon icon={() => XIcon} />
         </Button>
       </View>
       {membershipItems.length > 0 && (
@@ -213,32 +227,30 @@ function StarterPackList({
   )
 
   return (
-    <>
-      <Dialog.InnerFlatList
-        data={isLoading ? [{}] : membershipItems}
-        renderItem={
-          isLoading
-            ? () => (
-                <View style={[a.align_center, a.py_2xl]}>
-                  <Loader size="xl" />
-                </View>
-              )
-            : renderItem
-        }
-        keyExtractor={
-          isLoading
-            ? () => 'starter_pack_dialog_loader'
-            : (item: StarterPackWithMembership) => item.starterPack.uri
-        }
-        refreshing={false}
-        onRefresh={_onRefresh}
-        onEndReached={_onEndReached}
-        onEndReachedThreshold={0.1}
-        ListHeaderComponent={listHeader}
-        ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
-        contentContainerStyle={[a.px_2xl, a.pt_lg]}
-      />
-    </>
+    <Dialog.InnerFlatList
+      data={isLoading ? [{}] : membershipItems}
+      renderItem={
+        isLoading
+          ? () => (
+              <View style={[a.align_center, a.py_2xl]}>
+                <Loader size="xl" />
+              </View>
+            )
+          : renderItem
+      }
+      keyExtractor={
+        isLoading
+          ? () => 'starter_pack_dialog_loader'
+          : (item: StarterPackWithMembership) => item.starterPack.uri
+      }
+      refreshing={false}
+      onRefresh={_onRefresh}
+      onEndReached={_onEndReached}
+      onEndReachedThreshold={0.1}
+      ListHeaderComponent={listHeader}
+      ListEmptyComponent={<Empty onStartWizard={onStartWizard} />}
+      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(),
-- 
cgit 1.4.1

