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/view/com/profile/ProfileMenu.tsx | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

(limited to 'src/view/com/profile/ProfileMenu.tsx')

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 = ({
                     )}
                   </>
                 )}
+                <Menu.Item
+                  testID="profileHeaderDropdownStarterPackAddRemoveBtn"
+                  label={_(msg`Add to starter packs`)}
+                  onPress={addToStarterPacksDialogControl.open}>
+                  <Menu.ItemText>
+                    <Trans>Add to starter packs</Trans>
+                  </Menu.ItemText>
+                  <Menu.ItemIcon icon={StarterPack} />
+                </Menu.Item>
                 <Menu.Item
                   testID="profileHeaderDropdownListAddRemoveBtn"
                   label={_(msg`Add to lists`)}
@@ -440,6 +452,14 @@ let ProfileMenu = ({
         </Menu.Outer>
       </Menu.Root>
 
+      {currentAccount && (
+        <StarterPackDialog
+          control={addToStarterPacksDialogControl}
+          accountDid={currentAccount.did}
+          targetDid={profile.did}
+        />
+      )}
+
       <ReportDialog
         control={reportDialogControl}
         subject={{
-- 
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/view/com/profile/ProfileMenu.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

