about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/AppPasswords.tsx36
-rw-r--r--src/view/screens/ProfileFeed.tsx361
-rw-r--r--src/view/screens/ProfileList.tsx198
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx2
-rw-r--r--src/view/screens/Storybook/Menus.tsx2
5 files changed, 289 insertions, 310 deletions
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index dc439c367..800216169 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -29,6 +29,8 @@ import {
 } from '#/state/queries/app-passwords'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {cleanError} from '#/lib/strings/errors'
+import * as Prompt from '#/components/Prompt'
+import {useDialogControl} from '#/components/Dialog'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
 export function AppPasswords({}: Props) {
@@ -212,23 +214,18 @@ function AppPassword({
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {openModal} = useModalControls()
+  const control = useDialogControl()
   const {contentLanguages} = useLanguagePrefs()
   const deleteMutation = useAppPasswordDeleteMutation()
 
   const onDelete = React.useCallback(async () => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Delete app password`),
-      message: _(
-        msg`Are you sure you want to delete the app password "${name}"?`,
-      ),
-      async onPressConfirm() {
-        await deleteMutation.mutateAsync({name})
-        Toast.show(_(msg`App password deleted`))
-      },
-    })
-  }, [deleteMutation, openModal, name, _])
+    await deleteMutation.mutateAsync({name})
+    Toast.show(_(msg`App password deleted`))
+  }, [deleteMutation, name, _])
+
+  const onPress = React.useCallback(() => {
+    control.open()
+  }, [control])
 
   const primaryLocale =
     contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
@@ -237,7 +234,7 @@ function AppPassword({
     <TouchableOpacity
       testID={testID}
       style={[styles.item, pal.border]}
-      onPress={onDelete}
+      onPress={onPress}
       accessibilityRole="button"
       accessibilityLabel={_(msg`Delete app password`)}
       accessibilityHint="">
@@ -260,6 +257,17 @@ function AppPassword({
         </Text>
       </View>
       <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
+
+      <Prompt.Basic
+        control={control}
+        title={_(msg`Delete app password?`)}
+        description={_(
+          msg`Are you sure you want to delete the app password "${name}"?`,
+        )}
+        onConfirm={onDelete}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+      />
     </TouchableOpacity>
   )
 }
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index d92c1cd83..579e77f57 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,11 +1,9 @@
 import React, {useMemo, useCallback} from 'react'
-import {Dimensions, StyleSheet, View} from 'react-native'
+import {StyleSheet, View, Pressable} from 'react-native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useIsFocused, useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 import {usePalette} from 'lib/hooks/usePalette'
-import {HeartIcon, HeartIconSolid} from 'lib/icons'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {s} from 'lib/styles'
@@ -13,7 +11,7 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
 import {Feed} from 'view/com/posts/Feed'
-import {TextLink} from 'view/com/util/Link'
+import {InlineLink} from '#/components/Link'
 import {ListRef} from 'view/com/util/List'
 import {Button} from 'view/com/util/forms/Button'
 import {Text} from 'view/com/util/text/Text'
@@ -29,15 +27,10 @@ import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {Haptics} from 'lib/haptics'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {useScrollHandlers} from '#/lib/ScrollContext'
-import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {makeCustomFeedLink} from 'lib/routes/links'
 import {pluralize} from 'lib/strings/helpers'
-import {CenteredView, ScrollView} from 'view/com/util/Views'
+import {CenteredView} from 'view/com/util/Views'
 import {NavigationProp} from 'lib/routes/types'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink} from 'lib/routes/links'
 import {ComposeIcon2} from 'lib/icons'
 import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
@@ -59,9 +52,21 @@ import {useComposerControls} from '#/state/shell/composer'
 import {truncateAndInvalidate} from '#/state/queries/util'
 import {isNative} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
+import * as Menu from '#/components/Menu'
+import {HITSLOP_20} from '#/lib/constants'
+import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {
+  Heart2_Stroke2_Corner0_Rounded as HeartOutline,
+  Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
+} from '#/components/icons/Heart2'
+import {Button as NewButton, ButtonText} from '#/components/Button'
 
-const SECTION_TITLES = ['Posts', 'About']
+const SECTION_TITLES = ['Posts']
 
 interface SectionRef {
   scrollToTop: () => void
@@ -148,7 +153,7 @@ export function ProfileFeedScreenInner({
   feedInfo: FeedSourceFeedInfo
 }) {
   const {_} = useLingui()
-  const pal = usePalette('default')
+  const t = useTheme()
   const {hasSession, currentAccount} = useSession()
   const {openModal} = useModalControls()
   const {openComposer} = useComposerControls()
@@ -200,9 +205,11 @@ export function ProfileFeedScreenInner({
       if (isSaved) {
         await removeFeed({uri: feedInfo.uri})
         resetRemoveFeed()
+        Toast.show(_(msg`Removed from your feeds`))
       } else {
         await saveFeed({uri: feedInfo.uri})
         resetSaveFeed()
+        Toast.show(_(msg`Saved to your feeds`))
       }
     } catch (err) {
       Toast.show(
@@ -263,130 +270,132 @@ export function ProfileFeedScreenInner({
     [feedSectionRef],
   )
 
-  // render
-  // =
-
-  const dropdownItems: DropdownItem[] = React.useMemo(() => {
-    return [
-      hasSession && {
-        testID: 'feedHeaderDropdownToggleSavedBtn',
-        label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
-        onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
-        icon: isSaved
-          ? {
-              ios: {
-                name: 'trash',
-              },
-              android: 'ic_delete',
-              web: ['far', 'trash-can'],
-            }
-          : {
-              ios: {
-                name: 'plus',
-              },
-              android: '',
-              web: 'plus',
-            },
-      },
-      hasSession && {
-        testID: 'feedHeaderDropdownReportBtn',
-        label: _(msg`Report feed`),
-        onPress: onPressReport,
-        icon: {
-          ios: {
-            name: 'exclamationmark.triangle',
-          },
-          android: 'ic_menu_report_image',
-          web: 'circle-exclamation',
-        },
-      },
-      {
-        testID: 'feedHeaderDropdownShareBtn',
-        label: _(msg`Share feed`),
-        onPress: onPressShare,
-        icon: {
-          ios: {
-            name: 'square.and.arrow.up',
-          },
-          android: 'ic_menu_share',
-          web: 'share',
-        },
-      },
-    ].filter(Boolean) as DropdownItem[]
-  }, [
-    hasSession,
-    onToggleSaved,
-    onPressReport,
-    onPressShare,
-    isSaved,
-    isSavePending,
-    isRemovePending,
-    _,
-  ])
-
   const renderHeader = useCallback(() => {
     return (
-      <ProfileSubpageHeader
-        isLoading={false}
-        href={feedInfo.route.href}
-        title={feedInfo?.displayName}
-        avatar={feedInfo?.avatar}
-        isOwner={feedInfo.creatorDid === currentAccount?.did}
-        creator={
-          feedInfo
-            ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
-            : undefined
-        }
-        avatarType="algo">
-        {feedInfo && hasSession && (
-          <>
-            <Button
-              disabled={isSavePending || isRemovePending}
-              type="default"
-              label={isSaved ? _(msg`Unsave`) : _(msg`Save`)}
-              onPress={onToggleSaved}
-              style={styles.btn}
-            />
-            <Button
-              testID={isPinned ? 'unpinBtn' : 'pinBtn'}
-              disabled={isPinPending || isUnpinPending}
-              type={isPinned ? 'default' : 'inverted'}
-              label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
-              onPress={onTogglePinned}
-              style={styles.btn}
-            />
-          </>
-        )}
-        <NativeDropdown
-          testID="headerDropdownBtn"
-          items={dropdownItems}
-          accessibilityLabel={_(msg`More options`)}
-          accessibilityHint="">
-          <View style={[pal.viewLight, styles.btn]}>
-            <FontAwesomeIcon
-              icon="ellipsis"
-              size={20}
-              color={pal.colors.text}
-            />
+      <>
+        <ProfileSubpageHeader
+          isLoading={false}
+          href={feedInfo.route.href}
+          title={feedInfo?.displayName}
+          avatar={feedInfo?.avatar}
+          isOwner={feedInfo.creatorDid === currentAccount?.did}
+          creator={
+            feedInfo
+              ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
+              : undefined
+          }
+          avatarType="algo">
+          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
+            {feedInfo && hasSession && (
+              <NewButton
+                testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+                disabled={isPinPending || isUnpinPending}
+                size="small"
+                variant="solid"
+                color={isPinned ? 'secondary' : 'primary'}
+                label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
+                onPress={onTogglePinned}>
+                <ButtonText>
+                  {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
+                </ButtonText>
+              </NewButton>
+            )}
+            <Menu.Root>
+              <Menu.Trigger label={_(msg`Open feed options menu`)}>
+                {({props, state}) => {
+                  return (
+                    <Pressable
+                      {...props}
+                      hitSlop={HITSLOP_20}
+                      style={[
+                        a.justify_center,
+                        a.align_center,
+                        a.rounded_full,
+                        {height: 36, width: 36},
+                        t.atoms.bg_contrast_50,
+                        (state.hovered || state.pressed) && [
+                          t.atoms.bg_contrast_100,
+                        ],
+                      ]}
+                      testID="headerDropdownBtn">
+                      <Ellipsis
+                        size="lg"
+                        fill={t.atoms.text_contrast_medium.color}
+                      />
+                    </Pressable>
+                  )
+                }}
+              </Menu.Trigger>
+
+              <Menu.Outer>
+                <Menu.Group>
+                  {hasSession && (
+                    <>
+                      <Menu.Item
+                        disabled={isSavePending || isRemovePending}
+                        testID="feedHeaderDropdownToggleSavedBtn"
+                        label={
+                          isSaved
+                            ? _(msg`Remove from my feeds`)
+                            : _(msg`Save to my feeds`)
+                        }
+                        onPress={onToggleSaved}>
+                        <Menu.ItemText>
+                          {isSaved
+                            ? _(msg`Remove from my feeds`)
+                            : _(msg`Save to my feeds`)}
+                        </Menu.ItemText>
+                        <Menu.ItemIcon
+                          icon={isSaved ? Trash : Plus}
+                          position="right"
+                        />
+                      </Menu.Item>
+
+                      <Menu.Item
+                        testID="feedHeaderDropdownReportBtn"
+                        label={_(msg`Report feed`)}
+                        onPress={onPressReport}>
+                        <Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText>
+                        <Menu.ItemIcon icon={CircleInfo} position="right" />
+                      </Menu.Item>
+                    </>
+                  )}
+
+                  <Menu.Item
+                    testID="feedHeaderDropdownShareBtn"
+                    label={_(msg`Share feed`)}
+                    onPress={onPressShare}>
+                    <Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText>
+                    <Menu.ItemIcon icon={Share} position="right" />
+                  </Menu.Item>
+                </Menu.Group>
+              </Menu.Outer>
+            </Menu.Root>
           </View>
-        </NativeDropdown>
-      </ProfileSubpageHeader>
+        </ProfileSubpageHeader>
+        <AboutSection
+          feedOwnerDid={feedInfo.creatorDid}
+          feedRkey={feedInfo.route.params.rkey}
+          feedInfo={feedInfo}
+        />
+      </>
     )
   }, [
     _,
     hasSession,
-    pal,
     feedInfo,
     isPinned,
     onTogglePinned,
     onToggleSaved,
-    dropdownItems,
     currentAccount?.did,
     isPinPending,
     isRemovePending,
     isSavePending,
     isSaved,
     isUnpinPending,
+    onPressReport,
+    onPressShare,
+    t,
   ])
 
   return (
@@ -405,18 +414,6 @@ export function ProfileFeedScreenInner({
             isFocused={isScreenFocused && isFocused}
           />
         )}
-        {({headerHeight, scrollElRef}) => (
-          <AboutSection
-            feedOwnerDid={feedInfo.creatorDid}
-            feedRkey={feedInfo.route.params.rkey}
-            feedInfo={feedInfo}
-            headerHeight={headerHeight}
-            scrollElRef={
-              scrollElRef as React.MutableRefObject<ScrollView | null>
-            }
-            isOwner={feedInfo.creatorDid === currentAccount?.did}
-          />
-        )}
       </PagerWithHeader>
       {hasSession && (
         <FAB
@@ -505,21 +502,14 @@ function AboutSection({
   feedOwnerDid,
   feedRkey,
   feedInfo,
-  headerHeight,
-  scrollElRef,
-  isOwner,
 }: {
   feedOwnerDid: string
   feedRkey: string
   feedInfo: FeedSourceFeedInfo
-  headerHeight: number
-  scrollElRef: React.MutableRefObject<ScrollView | null>
-  isOwner: boolean
 }) {
+  const t = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const scrollHandlers = useScrollHandlers()
-  const onScroll = useAnimatedScrollHandler(scrollHandlers)
   const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
   const {hasSession} = useSession()
   const {track} = useAnalytics()
@@ -555,24 +545,8 @@ function AboutSection({
   }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
 
   return (
-    <ScrollView
-      ref={scrollElRef}
-      onScroll={onScroll}
-      scrollEventThrottle={1}
-      contentContainerStyle={{
-        paddingTop: headerHeight,
-        minHeight: Dimensions.get('window').height * 1.5,
-      }}>
-      <View
-        style={[
-          {
-            borderTopWidth: 1,
-            paddingVertical: 20,
-            paddingHorizontal: 20,
-            gap: 12,
-          },
-          pal.border,
-        ]}>
+    <View style={[styles.aboutSectionContainer]}>
+      <View style={[a.pt_sm]}>
         {feedInfo.description ? (
           <RichText
             testID="listDescription"
@@ -584,50 +558,34 @@ function AboutSection({
             <Trans>No description</Trans>
           </Text>
         )}
-        <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
-          <Button
-            type="default"
-            testID="toggleLikeBtn"
-            accessibilityLabel={_(msg`Like this feed`)}
-            accessibilityHint=""
-            disabled={!hasSession || isLikePending || isUnlikePending}
-            onPress={onToggleLiked}
-            style={{paddingHorizontal: 10}}>
-            {isLiked ? (
-              <HeartIconSolid size={19} style={s.likeColor} />
-            ) : (
-              <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
-            )}
-          </Button>
-          {typeof likeCount === 'number' && (
-            <TextLink
-              href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-              text={_(
-                msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
-              )}
-              style={[pal.textLight, s.semiBold]}
-            />
-          )}
-        </View>
-        <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-          {isOwner ? (
-            <Trans>Created by you</Trans>
+      </View>
+
+      <View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}>
+        <NewButton
+          size="small"
+          variant="solid"
+          color="secondary"
+          shape="round"
+          label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
+          testID="toggleLikeBtn"
+          disabled={!hasSession || isLikePending || isUnlikePending}
+          onPress={onToggleLiked}>
+          {isLiked ? (
+            <HeartFilled size="md" fill={s.likeColor.color} />
           ) : (
-            <Trans>
-              Created by{' '}
-              <TextLink
-                text={sanitizeHandle(feedInfo.creatorHandle, '@')}
-                href={makeProfileLink({
-                  did: feedInfo.creatorDid,
-                  handle: feedInfo.creatorHandle,
-                })}
-                style={pal.textLight}
-              />
-            </Trans>
+            <HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} />
           )}
-        </Text>
+        </NewButton>
+        {typeof likeCount === 'number' && (
+          <InlineLink
+            label={_(msg`View users who like this feed`)}
+            to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
+            style={[t.atoms.text_contrast_medium, a.font_bold]}>
+            {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
+          </InlineLink>
+        )}
       </View>
-    </ScrollView>
+    </View>
   )
 }
 
@@ -647,4 +605,9 @@ const styles = StyleSheet.create({
     paddingVertical: 14,
     borderRadius: 6,
   },
+  aboutSectionContainer: {
+    paddingVertical: 4,
+    paddingHorizontal: 16,
+    gap: 12,
+  },
 })
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 3c675ee0a..798611157 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -61,6 +61,8 @@ import {logger} from '#/logger'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {listenSoftReset} from '#/state/events'
 import {atoms as a, useTheme} from '#/alf'
+import * as Prompt from '#/components/Prompt'
+import {useDialogControl} from '#/components/Dialog'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -234,7 +236,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const {currentAccount} = useSession()
-  const {openModal, closeModal} = useModalControls()
+  const {openModal} = useModalControls()
   const listMuteMutation = useListMuteMutation()
   const listBlockMutation = useListBlockMutation()
   const listDeleteMutation = useListDeleteMutation()
@@ -251,6 +253,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
   const {track} = useAnalytics()
 
+  const deleteListPromptControl = useDialogControl()
+  const subscribeMutePromptControl = useDialogControl()
+  const subscribeBlockPromptControl = useDialogControl()
+
   const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
   const isSaved = preferences?.feeds?.saved?.includes(list.uri)
 
@@ -269,32 +275,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
   }, [list.uri, isPinned, pinFeed, unpinFeed, _])
 
-  const onSubscribeMute = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Mute these accounts?`),
-      message: _(
-        msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
-      ),
-      confirmBtnText: _(msg`Mute this List`),
-      async onPressConfirm() {
-        try {
-          await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
-          Toast.show(_(msg`List muted`))
-          track('Lists:Mute')
-        } catch {
-          Toast.show(
-            _(
-              msg`There was an issue. Please check your internet connection and try again.`,
-            ),
-          )
-        }
-      },
-      onPressCancel() {
-        closeModal()
-      },
-    })
-  }, [openModal, closeModal, list, listMuteMutation, track, _])
+  const onSubscribeMute = useCallback(async () => {
+    try {
+      await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
+      Toast.show(_(msg`List muted`))
+      track('Lists:Mute')
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }, [list, listMuteMutation, track, _])
 
   const onUnsubscribeMute = useCallback(async () => {
     try {
@@ -310,32 +303,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
   }, [list, listMuteMutation, track, _])
 
-  const onSubscribeBlock = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Block these accounts?`),
-      message: _(
-        msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
-      ),
-      confirmBtnText: _(msg`Block this List`),
-      async onPressConfirm() {
-        try {
-          await listBlockMutation.mutateAsync({uri: list.uri, block: true})
-          Toast.show(_(msg`List blocked`))
-          track('Lists:Block')
-        } catch {
-          Toast.show(
-            _(
-              msg`There was an issue. Please check your internet connection and try again.`,
-            ),
-          )
-        }
-      },
-      onPressCancel() {
-        closeModal()
-      },
-    })
-  }, [openModal, closeModal, list, listBlockMutation, track, _])
+  const onSubscribeBlock = useCallback(async () => {
+    try {
+      await listBlockMutation.mutateAsync({uri: list.uri, block: true})
+      Toast.show(_(msg`List blocked`))
+      track('Lists:Block')
+    } catch {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+    }
+  }, [list, listBlockMutation, track, _])
 
   const onUnsubscribeBlock = useCallback(async () => {
     try {
@@ -358,34 +338,26 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     })
   }, [openModal, list])
 
-  const onPressDelete = useCallback(() => {
-    openModal({
-      name: 'confirm',
-      title: _(msg`Delete List`),
-      message: _(msg`Are you sure?`),
-      async onPressConfirm() {
-        await listDeleteMutation.mutateAsync({uri: list.uri})
-
-        if (isSaved || isPinned) {
-          const {saved, pinned} = preferences!.feeds
-
-          setSavedFeeds({
-            saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved,
-            pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned,
-          })
-        }
+  const onPressDelete = useCallback(async () => {
+    await listDeleteMutation.mutateAsync({uri: list.uri})
 
-        Toast.show(_(msg`List deleted`))
-        track('Lists:Delete')
-        if (navigation.canGoBack()) {
-          navigation.goBack()
-        } else {
-          navigation.navigate('Home')
-        }
-      },
-    })
+    if (isSaved || isPinned) {
+      const {saved, pinned} = preferences!.feeds
+
+      setSavedFeeds({
+        saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved,
+        pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned,
+      })
+    }
+
+    Toast.show(_(msg`List deleted`))
+    track('Lists:Delete')
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
   }, [
-    openModal,
     list,
     listDeleteMutation,
     navigation,
@@ -443,7 +415,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       items.push({
         testID: 'listHeaderDropdownDeleteBtn',
         label: _(msg`Delete List`),
-        onPress: onPressDelete,
+        onPress: deleteListPromptControl.open,
         icon: {
           ios: {
             name: 'trash',
@@ -489,7 +461,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         items.push({
           testID: 'listHeaderDropdownMuteBtn',
           label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
-          onPress: isMuting ? onUnsubscribeMute : onSubscribeMute,
+          onPress: isMuting
+            ? onUnsubscribeMute
+            : subscribeMutePromptControl.open,
           icon: {
             ios: {
               name: isMuting ? 'eye' : 'eye.slash',
@@ -504,7 +478,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         items.push({
           testID: 'listHeaderDropdownBlockBtn',
           label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
-          onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock,
+          onPress: isBlocking
+            ? onUnsubscribeBlock
+            : subscribeBlockPromptControl.open,
           icon: {
             ios: {
               name: 'person.fill.xmark',
@@ -517,24 +493,24 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
     }
     return items
   }, [
-    isOwner,
-    onPressShare,
-    onPressEdit,
-    onPressDelete,
-    onPressReport,
     _,
+    onPressShare,
+    isOwner,
     isModList,
     isPinned,
-    unpinFeed,
+    isCurateList,
+    onPressEdit,
+    deleteListPromptControl.open,
+    onPressReport,
     isPending,
+    unpinFeed,
     list.uri,
-    isCurateList,
-    isMuting,
     isBlocking,
+    isMuting,
     onUnsubscribeMute,
-    onSubscribeMute,
+    subscribeMutePromptControl.open,
     onUnsubscribeBlock,
-    onSubscribeBlock,
+    subscribeBlockPromptControl.open,
   ])
 
   const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
@@ -542,7 +518,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       {
         testID: 'subscribeDropdownMuteBtn',
         label: _(msg`Mute accounts`),
-        onPress: onSubscribeMute,
+        onPress: subscribeMutePromptControl.open,
         icon: {
           ios: {
             name: 'speaker.slash',
@@ -554,7 +530,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       {
         testID: 'subscribeDropdownBlockBtn',
         label: _(msg`Block accounts`),
-        onPress: onSubscribeBlock,
+        onPress: subscribeBlockPromptControl.open,
         icon: {
           ios: {
             name: 'person.fill.xmark',
@@ -564,7 +540,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         },
       },
     ]
-  }, [onSubscribeMute, onSubscribeBlock, _])
+  }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
 
   return (
     <ProfileSubpageHeader
@@ -620,6 +596,38 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
           <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
         </View>
       </NativeDropdown>
+
+      <Prompt.Basic
+        control={deleteListPromptControl}
+        title={_(msg`Delete this list?`)}
+        description={_(
+          msg`If you delete this list, you won't be able to recover it.`,
+        )}
+        onConfirm={onPressDelete}
+        confirmButtonCta={_(msg`Delete`)}
+        confirmButtonColor="negative"
+      />
+
+      <Prompt.Basic
+        control={subscribeMutePromptControl}
+        title={_(msg`Mute these accounts?`)}
+        description={_(
+          msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
+        )}
+        onConfirm={onSubscribeMute}
+        confirmButtonCta={_(msg`Mute list`)}
+      />
+
+      <Prompt.Basic
+        control={subscribeBlockPromptControl}
+        title={_(msg`Block these accounts?`)}
+        description={_(
+          msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+        )}
+        onConfirm={onSubscribeBlock}
+        confirmButtonCta={_(msg`Block list`)}
+        confirmButtonColor="negative"
+      />
     </ProfileSubpageHeader>
   )
 }
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
index 09be124db..c2eaf19ac 100644
--- a/src/view/screens/Storybook/Dialogs.tsx
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -68,7 +68,7 @@ export function Dialogs() {
         </Prompt.Description>
         <Prompt.Actions>
           <Prompt.Cancel>Cancel</Prompt.Cancel>
-          <Prompt.Action>Confirm</Prompt.Action>
+          <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>
         </Prompt.Actions>
       </Prompt.Outer>
 
diff --git a/src/view/screens/Storybook/Menus.tsx b/src/view/screens/Storybook/Menus.tsx
index 082fb2b6e..2f2b14721 100644
--- a/src/view/screens/Storybook/Menus.tsx
+++ b/src/view/screens/Storybook/Menus.tsx
@@ -16,7 +16,7 @@ export function Menus() {
     <View style={[a.gap_md]}>
       <View style={[a.flex_row, a.align_start]}>
         <Menu.Root control={menuControl}>
-          <Menu.Trigger label="Open basic menu" style={[a.flex_1]}>
+          <Menu.Trigger label="Open basic menu">
             {({state, props}) => {
               return (
                 <Text