about summary refs log tree commit diff
path: root/src/components/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/dialogs')
-rw-r--r--src/components/dialogs/BirthDateSettings.tsx1
-rw-r--r--src/components/dialogs/EmbedConsent.tsx1
-rw-r--r--src/components/dialogs/GifSelect.ios.tsx255
-rw-r--r--src/components/dialogs/GifSelect.shared.tsx53
-rw-r--r--src/components/dialogs/GifSelect.tsx75
-rw-r--r--src/components/dialogs/MutedWords.tsx528
-rw-r--r--src/components/dialogs/PostInteractionSettingsDialog.tsx7
-rw-r--r--src/components/dialogs/SwitchAccount.tsx1
-rw-r--r--src/components/dialogs/nuxs/NeueTypography.tsx1
9 files changed, 357 insertions, 565 deletions
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index 08608f9d8..81d0c6740 100644
--- a/src/components/dialogs/BirthDateSettings.tsx
+++ b/src/components/dialogs/BirthDateSettings.tsx
@@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`My Birthday`)}>
         <View style={[a.gap_sm, a.pb_lg]}>
           <Text style={[a.text_2xl, a.font_bold]}>
diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx
index 765b8adc7..824155d8b 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -50,7 +50,6 @@ export function EmbedConsentDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner
         label={_(msg`External Media`)}
         style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx
deleted file mode 100644
index 2f867e865..000000000
--- a/src/components/dialogs/GifSelect.ios.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import React, {
-  useCallback,
-  useImperativeHandle,
-  useMemo,
-  useRef,
-  useState,
-} from 'react'
-import {Modal, ScrollView, TextInput, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {cleanError} from '#/lib/strings/errors'
-import {
-  Gif,
-  useFeaturedGifsQuery,
-  useGifSearchQuery,
-} from '#/state/queries/tenor'
-import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
-import {FlatList_INTERNAL} from '#/view/com/util/Views'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import * as TextField from '#/components/forms/TextField'
-import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
-import {Button, ButtonText} from '../Button'
-import {Handle} from '../Dialog'
-import {useThrottledValue} from '../hooks/useThrottledValue'
-import {ListFooter, ListMaybePlaceholder} from '../Lists'
-import {GifPreview} from './GifSelect.shared'
-
-export function GifSelectDialog({
-  controlRef,
-  onClose,
-  onSelectGif: onSelectGifProp,
-}: {
-  controlRef: React.RefObject<{open: () => void}>
-  onClose: () => void
-  onSelectGif: (gif: Gif) => void
-}) {
-  const t = useTheme()
-  const [open, setOpen] = useState(false)
-
-  useImperativeHandle(controlRef, () => ({
-    open: () => setOpen(true),
-  }))
-
-  const close = useCallback(() => {
-    setOpen(false)
-    onClose()
-  }, [onClose])
-
-  const onSelectGif = useCallback(
-    (gif: Gif) => {
-      onSelectGifProp(gif)
-      close()
-    },
-    [onSelectGifProp, close],
-  )
-
-  const renderErrorBoundary = useCallback(
-    (error: any) => <ModalError details={String(error)} close={close} />,
-    [close],
-  )
-
-  return (
-    <Modal
-      visible={open}
-      animationType="slide"
-      presentationStyle="formSheet"
-      onRequestClose={close}
-      aria-modal
-      accessibilityViewIsModal>
-      <View style={[a.flex_1, t.atoms.bg]}>
-        <Handle />
-        <ErrorBoundary renderError={renderErrorBoundary}>
-          <GifList onSelectGif={onSelectGif} close={close} />
-        </ErrorBoundary>
-      </View>
-    </Modal>
-  )
-}
-
-function GifList({
-  onSelectGif,
-}: {
-  close: () => void
-  onSelectGif: (gif: Gif) => void
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const {gtMobile} = useBreakpoints()
-  const textInputRef = useRef<TextInput>(null)
-  const listRef = useRef<FlatList_INTERNAL>(null)
-  const [undeferredSearch, setSearch] = useState('')
-  const search = useThrottledValue(undeferredSearch, 500)
-
-  const isSearching = search.length > 0
-
-  const trendingQuery = useFeaturedGifsQuery()
-  const searchQuery = useGifSearchQuery(search)
-
-  const {
-    data,
-    fetchNextPage,
-    isFetchingNextPage,
-    hasNextPage,
-    error,
-    isLoading,
-    isError,
-    refetch,
-  } = isSearching ? searchQuery : trendingQuery
-
-  const flattenedData = useMemo(() => {
-    return data?.pages.flatMap(page => page.results) || []
-  }, [data])
-
-  const renderItem = useCallback(
-    ({item}: {item: Gif}) => {
-      return <GifPreview gif={item} onSelectGif={onSelectGif} />
-    },
-    [onSelectGif],
-  )
-
-  const onEndReached = React.useCallback(() => {
-    if (isFetchingNextPage || !hasNextPage || error) return
-    fetchNextPage()
-  }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
-
-  const hasData = flattenedData.length > 0
-
-  const onGoBack = useCallback(() => {
-    if (isSearching) {
-      // clear the input and reset the state
-      textInputRef.current?.clear()
-      setSearch('')
-    } else {
-      close()
-    }
-  }, [isSearching])
-
-  const listHeader = useMemo(() => {
-    return (
-      <View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}>
-        {/* cover top corners */}
-        <View
-          style={[
-            a.absolute,
-            a.inset_0,
-            {
-              borderBottomLeftRadius: 8,
-              borderBottomRightRadius: 8,
-            },
-            t.atoms.bg,
-          ]}
-        />
-
-        <TextField.Root>
-          <TextField.Icon icon={Search} />
-          <TextField.Input
-            label={_(msg`Search GIFs`)}
-            placeholder={_(msg`Search Tenor`)}
-            onChangeText={text => {
-              setSearch(text)
-              listRef.current?.scrollToOffset({offset: 0, animated: false})
-            }}
-            returnKeyType="search"
-            clearButtonMode="while-editing"
-            inputRef={textInputRef}
-            maxLength={50}
-          />
-        </TextField.Root>
-      </View>
-    )
-  }, [t.atoms.bg, _])
-
-  return (
-    <FlatList_INTERNAL
-      ref={listRef}
-      key={gtMobile ? '3 cols' : '2 cols'}
-      data={flattenedData}
-      renderItem={renderItem}
-      numColumns={gtMobile ? 3 : 2}
-      columnWrapperStyle={a.gap_sm}
-      contentContainerStyle={a.px_lg}
-      ListHeaderComponent={
-        <>
-          {listHeader}
-          {!hasData && (
-            <ListMaybePlaceholder
-              isLoading={isLoading}
-              isError={isError}
-              onRetry={refetch}
-              onGoBack={onGoBack}
-              emptyType="results"
-              sideBorders={false}
-              topBorder={false}
-              errorTitle={_(msg`Failed to load GIFs`)}
-              errorMessage={_(msg`There was an issue connecting to Tenor.`)}
-              emptyMessage={
-                isSearching
-                  ? _(msg`No search results found for "${search}".`)
-                  : _(
-                      msg`No featured GIFs found. There may be an issue with Tenor.`,
-                    )
-              }
-            />
-          )}
-        </>
-      }
-      stickyHeaderIndices={[0]}
-      onEndReached={onEndReached}
-      onEndReachedThreshold={4}
-      keyExtractor={(item: Gif) => item.id}
-      keyboardDismissMode="on-drag"
-      ListFooterComponent={
-        hasData ? (
-          <ListFooter
-            isFetchingNextPage={isFetchingNextPage}
-            error={cleanError(error)}
-            onRetry={fetchNextPage}
-            style={{borderTopWidth: 0}}
-          />
-        ) : null
-      }
-    />
-  )
-}
-
-function ModalError({details, close}: {details?: string; close: () => void}) {
-  const {_} = useLingui()
-
-  return (
-    <ScrollView
-      style={[a.flex_1, a.gap_md]}
-      centerContent
-      contentContainerStyle={a.px_lg}>
-      <ErrorScreen
-        title={_(msg`Oh no!`)}
-        message={_(
-          msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
-        )}
-        details={details}
-      />
-      <Button
-        label={_(msg`Close dialog`)}
-        onPress={close}
-        color="primary"
-        size="large"
-        variant="solid">
-        <ButtonText>
-          <Trans>Close</Trans>
-        </ButtonText>
-      </Button>
-    </ScrollView>
-  )
-}
diff --git a/src/components/dialogs/GifSelect.shared.tsx b/src/components/dialogs/GifSelect.shared.tsx
deleted file mode 100644
index 90b2abaa8..000000000
--- a/src/components/dialogs/GifSelect.shared.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, {useCallback} from 'react'
-import {Image} from 'expo-image'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {logEvent} from '#/lib/statsig/statsig'
-import {Gif} from '#/state/queries/tenor'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button} from '../Button'
-
-export function GifPreview({
-  gif,
-  onSelectGif,
-}: {
-  gif: Gif
-  onSelectGif: (gif: Gif) => void
-}) {
-  const {gtTablet} = useBreakpoints()
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const onPress = useCallback(() => {
-    logEvent('composer:gif:select', {})
-    onSelectGif(gif)
-  }, [onSelectGif, gif])
-
-  return (
-    <Button
-      label={_(msg`Select GIF "${gif.title}"`)}
-      style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
-      onPress={onPress}>
-      {({pressed}) => (
-        <Image
-          style={[
-            a.flex_1,
-            a.mb_sm,
-            a.rounded_sm,
-            {aspectRatio: 1, opacity: pressed ? 0.8 : 1},
-            t.atoms.bg_contrast_25,
-          ]}
-          source={{
-            uri: gif.media_formats.tinygif.url,
-          }}
-          contentFit="cover"
-          accessibilityLabel={gif.title}
-          accessibilityHint=""
-          cachePolicy="none"
-          accessibilityIgnoresInvertColors
-        />
-      )}
-    </Button>
-  )
-}
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index 1afc588da..6023b5808 100644
--- a/src/components/dialogs/GifSelect.tsx
+++ b/src/components/dialogs/GifSelect.tsx
@@ -6,10 +6,12 @@ import React, {
   useState,
 } from 'react'
 import {TextInput, View} from 'react-native'
-import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
+import {useWindowDimensions} from 'react-native'
+import {Image} from 'expo-image'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {logEvent} from '#/lib/statsig/statsig'
 import {cleanError} from '#/lib/strings/errors'
 import {isWeb} from '#/platform/detection'
 import {
@@ -19,7 +21,8 @@ import {
 } from '#/state/queries/tenor'
 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {ListMethods} from '#/view/com/util/List'
+import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
 import * as TextField from '#/components/forms/TextField'
 import {useThrottledValue} from '#/components/hooks/useThrottledValue'
@@ -27,16 +30,18 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr
 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
 import {Button, ButtonIcon, ButtonText} from '../Button'
 import {ListFooter, ListMaybePlaceholder} from '../Lists'
-import {GifPreview} from './GifSelect.shared'
+import {PortalComponent} from '../Portal'
 
 export function GifSelectDialog({
   controlRef,
   onClose,
   onSelectGif: onSelectGifProp,
+  Portal,
 }: {
   controlRef: React.RefObject<{open: () => void}>
   onClose: () => void
   onSelectGif: (gif: Gif) => void
+  Portal?: PortalComponent
 }) {
   const control = Dialog.useDialogControl()
 
@@ -59,8 +64,13 @@ export function GifSelectDialog({
   return (
     <Dialog.Outer
       control={control}
-      nativeOptions={{sheet: {snapPoints: ['100%']}}}
-      onClose={onClose}>
+      onClose={onClose}
+      Portal={Portal}
+      nativeOptions={{
+        bottomInset: 0,
+        // use system corner radius on iOS
+        ...ios({cornerRadius: undefined}),
+      }}>
       <Dialog.Handle />
       <ErrorBoundary renderError={renderErrorBoundary}>
         <GifList control={control} onSelectGif={onSelectGif} />
@@ -80,9 +90,10 @@ function GifList({
   const t = useTheme()
   const {gtMobile} = useBreakpoints()
   const textInputRef = useRef<TextInput>(null)
-  const listRef = useRef<BottomSheetFlatListMethods>(null)
+  const listRef = useRef<ListMethods>(null)
   const [undeferredSearch, setSearch] = useState('')
   const search = useThrottledValue(undeferredSearch, 500)
+  const {height} = useWindowDimensions()
 
   const isSearching = search.length > 0
 
@@ -95,7 +106,7 @@ function GifList({
     isFetchingNextPage,
     hasNextPage,
     error,
-    isLoading,
+    isPending,
     isError,
     refetch,
   } = isSearching ? searchQuery : trendingQuery
@@ -132,6 +143,7 @@ function GifList({
     return (
       <View
         style={[
+          native(a.pt_4xl),
           a.relative,
           a.mb_lg,
           a.flex_row,
@@ -196,13 +208,14 @@ function GifList({
         data={flattenedData}
         renderItem={renderItem}
         numColumns={gtMobile ? 3 : 2}
-        columnWrapperStyle={a.gap_sm}
+        columnWrapperStyle={[a.gap_sm]}
+        contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
         ListHeaderComponent={
           <>
             {listHeader}
             {!hasData && (
               <ListMaybePlaceholder
-                isLoading={isLoading}
+                isLoading={isPending}
                 isError={isError}
                 onRetry={refetch}
                 onGoBack={onGoBack}
@@ -273,3 +286,47 @@ function DialogError({details}: {details?: string}) {
     </Dialog.ScrollableInner>
   )
 }
+
+export function GifPreview({
+  gif,
+  onSelectGif,
+}: {
+  gif: Gif
+  onSelectGif: (gif: Gif) => void
+}) {
+  const {gtTablet} = useBreakpoints()
+  const {_} = useLingui()
+  const t = useTheme()
+
+  const onPress = useCallback(() => {
+    logEvent('composer:gif:select', {})
+    onSelectGif(gif)
+  }, [onSelectGif, gif])
+
+  return (
+    <Button
+      label={_(msg`Select GIF "${gif.title}"`)}
+      style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
+      onPress={onPress}>
+      {({pressed}) => (
+        <Image
+          style={[
+            a.flex_1,
+            a.mb_sm,
+            a.rounded_sm,
+            {aspectRatio: 1, opacity: pressed ? 0.8 : 1},
+            t.atoms.bg_contrast_25,
+          ]}
+          source={{
+            uri: gif.media_formats.tinygif.url,
+          }}
+          contentFit="cover"
+          accessibilityLabel={gif.title}
+          accessibilityHint=""
+          cachePolicy="none"
+          accessibilityIgnoresInvertColors
+        />
+      )}
+    </Button>
+  )
+}
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx
index 81a614103..c3aae8f0d 100644
--- a/src/components/dialogs/MutedWords.tsx
+++ b/src/components/dialogs/MutedWords.tsx
@@ -30,11 +30,14 @@ import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/P
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Loader} from '#/components/Loader'
+import {createPortalGroup} from '#/components/Portal'
 import * as Prompt from '#/components/Prompt'
 import {Text} from '#/components/Typography'
 
 const ONE_DAY = 24 * 60 * 60 * 1000
 
+const Portal = createPortalGroup()
+
 export function MutedWordsDialog() {
   const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
   return (
@@ -105,307 +108,349 @@ function MutedWordsInner() {
   }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
 
   return (
-    <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
-      <View>
-        <Text
-          style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
-          <Trans>Add muted words and tags</Trans>
-        </Text>
-        <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
-          <Trans>
-            Posts can be muted based on their text, their tags, or both. We
-            recommend avoiding common words that appear in many posts, since it
-            can result in no posts being shown.
-          </Trans>
-        </Text>
-
-        <View style={[a.pb_sm]}>
-          <Dialog.Input
-            autoCorrect={false}
-            autoCapitalize="none"
-            autoComplete="off"
-            label={_(msg`Enter a word or tag`)}
-            placeholder={_(msg`Enter a word or tag`)}
-            value={field}
-            onChangeText={value => {
-              if (error) {
-                setError('')
-              }
-              setField(value)
-            }}
-            onSubmitEditing={submit}
-          />
-        </View>
+    <Portal.Provider>
+      <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
+        <View>
+          <Text
+            style={[
+              a.text_md,
+              a.font_bold,
+              a.pb_sm,
+              t.atoms.text_contrast_high,
+            ]}>
+            <Trans>Add muted words and tags</Trans>
+          </Text>
+          <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
+            <Trans>
+              Posts can be muted based on their text, their tags, or both. We
+              recommend avoiding common words that appear in many posts, since
+              it can result in no posts being shown.
+            </Trans>
+          </Text>
 
-        <View style={[a.pb_xl, a.gap_sm]}>
-          <Toggle.Group
-            label={_(msg`Select how long to mute this word for.`)}
-            type="radio"
-            values={durations}
-            onChange={setDurations}>
-            <Text
-              style={[
-                a.pb_xs,
-                a.text_sm,
-                a.font_bold,
-                t.atoms.text_contrast_medium,
-              ]}>
-              <Trans>Duration:</Trans>
-            </Text>
+          <View style={[a.pb_sm]}>
+            <Dialog.Input
+              autoCorrect={false}
+              autoCapitalize="none"
+              autoComplete="off"
+              label={_(msg`Enter a word or tag`)}
+              placeholder={_(msg`Enter a word or tag`)}
+              value={field}
+              onChangeText={value => {
+                if (error) {
+                  setError('')
+                }
+                setField(value)
+              }}
+              onSubmitEditing={submit}
+            />
+          </View>
+
+          <View style={[a.pb_xl, a.gap_sm]}>
+            <Toggle.Group
+              label={_(msg`Select how long to mute this word for.`)}
+              type="radio"
+              values={durations}
+              onChange={setDurations}>
+              <Text
+                style={[
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>Duration:</Trans>
+              </Text>
 
-            <View
-              style={[
-                gtMobile && [a.flex_row, a.align_center, a.justify_start],
-                a.gap_sm,
-              ]}>
               <View
                 style={[
-                  a.flex_1,
-                  a.flex_row,
-                  a.justify_start,
-                  a.align_center,
+                  gtMobile && [a.flex_row, a.align_center, a.justify_start],
                   a.gap_sm,
                 ]}>
-                <Toggle.Item
-                  label={_(msg`Mute this word until you unmute it`)}
-                  name="forever"
-                  style={[a.flex_1]}>
-                  <TargetToggle>
-                    <View
-                      style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                      <Toggle.Radio />
-                      <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>Forever</Trans>
-                      </Toggle.LabelText>
-                    </View>
-                  </TargetToggle>
-                </Toggle.Item>
+                <View
+                  style={[
+                    a.flex_1,
+                    a.flex_row,
+                    a.justify_start,
+                    a.align_center,
+                    a.gap_sm,
+                  ]}>
+                  <Toggle.Item
+                    label={_(msg`Mute this word until you unmute it`)}
+                    name="forever"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>Forever</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 24 hours`)}
+                    name="24_hours"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>24 hours</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+                </View>
 
-                <Toggle.Item
-                  label={_(msg`Mute this word for 24 hours`)}
-                  name="24_hours"
-                  style={[a.flex_1]}>
-                  <TargetToggle>
-                    <View
-                      style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                      <Toggle.Radio />
-                      <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>24 hours</Trans>
-                      </Toggle.LabelText>
-                    </View>
-                  </TargetToggle>
-                </Toggle.Item>
+                <View
+                  style={[
+                    a.flex_1,
+                    a.flex_row,
+                    a.justify_start,
+                    a.align_center,
+                    a.gap_sm,
+                  ]}>
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 7 days`)}
+                    name="7_days"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>7 days</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+
+                  <Toggle.Item
+                    label={_(msg`Mute this word for 30 days`)}
+                    name="30_days"
+                    style={[a.flex_1]}>
+                    <TargetToggle>
+                      <View
+                        style={[
+                          a.flex_1,
+                          a.flex_row,
+                          a.align_center,
+                          a.gap_sm,
+                        ]}>
+                        <Toggle.Radio />
+                        <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
+                          <Trans>30 days</Trans>
+                        </Toggle.LabelText>
+                      </View>
+                    </TargetToggle>
+                  </Toggle.Item>
+                </View>
               </View>
+            </Toggle.Group>
 
-              <View
+            <Toggle.Group
+              label={_(
+                msg`Select what content this mute word should apply to.`,
+              )}
+              type="radio"
+              values={targets}
+              onChange={setTargets}>
+              <Text
                 style={[
-                  a.flex_1,
-                  a.flex_row,
-                  a.justify_start,
-                  a.align_center,
-                  a.gap_sm,
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
                 ]}>
+                <Trans>Mute in:</Trans>
+              </Text>
+
+              <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
                 <Toggle.Item
-                  label={_(msg`Mute this word for 7 days`)}
-                  name="7_days"
+                  label={_(msg`Mute this word in post text and tags`)}
+                  name="content"
                   style={[a.flex_1]}>
                   <TargetToggle>
                     <View
                       style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
                       <Toggle.Radio />
                       <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>7 days</Trans>
+                        <Trans>Text & tags</Trans>
                       </Toggle.LabelText>
                     </View>
+                    <PageText size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
 
                 <Toggle.Item
-                  label={_(msg`Mute this word for 30 days`)}
-                  name="30_days"
+                  label={_(msg`Mute this word in tags only`)}
+                  name="tag"
                   style={[a.flex_1]}>
                   <TargetToggle>
                     <View
                       style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
                       <Toggle.Radio />
                       <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                        <Trans>30 days</Trans>
+                        <Trans>Tags only</Trans>
                       </Toggle.LabelText>
                     </View>
+                    <Hashtag size="sm" />
                   </TargetToggle>
                 </Toggle.Item>
               </View>
-            </View>
-          </Toggle.Group>
+            </Toggle.Group>
 
-          <Toggle.Group
-            label={_(msg`Select what content this mute word should apply to.`)}
-            type="radio"
-            values={targets}
-            onChange={setTargets}>
-            <Text
-              style={[
-                a.pb_xs,
-                a.text_sm,
-                a.font_bold,
-                t.atoms.text_contrast_medium,
-              ]}>
-              <Trans>Mute in:</Trans>
-            </Text>
-
-            <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
+            <View>
+              <Text
+                style={[
+                  a.pb_xs,
+                  a.text_sm,
+                  a.font_bold,
+                  t.atoms.text_contrast_medium,
+                ]}>
+                <Trans>Options:</Trans>
+              </Text>
               <Toggle.Item
-                label={_(msg`Mute this word in post text and tags`)}
-                name="content"
-                style={[a.flex_1]}>
+                label={_(msg`Do not apply this mute word to users you follow`)}
+                name="exclude_following"
+                style={[a.flex_row, a.justify_between]}
+                value={excludeFollowing}
+                onChange={setExcludeFollowing}>
                 <TargetToggle>
                   <View
                     style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                    <Toggle.Radio />
+                    <Toggle.Checkbox />
                     <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                      <Trans>Text & tags</Trans>
+                      <Trans>Exclude users you follow</Trans>
                     </Toggle.LabelText>
                   </View>
-                  <PageText size="sm" />
                 </TargetToggle>
               </Toggle.Item>
+            </View>
 
-              <Toggle.Item
-                label={_(msg`Mute this word in tags only`)}
-                name="tag"
-                style={[a.flex_1]}>
-                <TargetToggle>
-                  <View
-                    style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                      <Trans>Tags only</Trans>
-                    </Toggle.LabelText>
-                  </View>
-                  <Hashtag size="sm" />
-                </TargetToggle>
-              </Toggle.Item>
+            <View style={[a.pt_xs]}>
+              <Button
+                disabled={isPending || !field}
+                label={_(msg`Add mute word for configured settings`)}
+                size="large"
+                color="primary"
+                variant="solid"
+                style={[]}
+                onPress={submit}>
+                <ButtonText>
+                  <Trans>Add</Trans>
+                </ButtonText>
+                <ButtonIcon icon={isPending ? Loader : Plus} position="right" />
+              </Button>
             </View>
-          </Toggle.Group>
 
-          <View>
+            {error && (
+              <View
+                style={[
+                  a.mb_lg,
+                  a.flex_row,
+                  a.rounded_sm,
+                  a.p_md,
+                  a.mb_xs,
+                  t.atoms.bg_contrast_25,
+                  {
+                    backgroundColor: t.palette.negative_400,
+                  },
+                ]}>
+                <Text
+                  style={[
+                    a.italic,
+                    {color: t.palette.white},
+                    native({marginTop: 2}),
+                  ]}>
+                  {error}
+                </Text>
+              </View>
+            )}
+          </View>
+
+          <Divider />
+
+          <View style={[a.pt_2xl]}>
             <Text
               style={[
-                a.pb_xs,
-                a.text_sm,
+                a.text_md,
                 a.font_bold,
-                t.atoms.text_contrast_medium,
+                a.pb_md,
+                t.atoms.text_contrast_high,
               ]}>
-              <Trans>Options:</Trans>
+              <Trans>Your muted words</Trans>
             </Text>
-            <Toggle.Item
-              label={_(msg`Do not apply this mute word to users you follow`)}
-              name="exclude_following"
-              style={[a.flex_row, a.justify_between]}
-              value={excludeFollowing}
-              onChange={setExcludeFollowing}>
-              <TargetToggle>
-                <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
-                  <Toggle.Checkbox />
-                  <Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
-                    <Trans>Exclude users you follow</Trans>
-                  </Toggle.LabelText>
-                </View>
-              </TargetToggle>
-            </Toggle.Item>
-          </View>
-
-          <View style={[a.pt_xs]}>
-            <Button
-              disabled={isPending || !field}
-              label={_(msg`Add mute word for configured settings`)}
-              size="large"
-              color="primary"
-              variant="solid"
-              style={[]}
-              onPress={submit}>
-              <ButtonText>
-                <Trans>Add</Trans>
-              </ButtonText>
-              <ButtonIcon icon={isPending ? Loader : Plus} position="right" />
-            </Button>
-          </View>
 
-          {error && (
-            <View
-              style={[
-                a.mb_lg,
-                a.flex_row,
-                a.rounded_sm,
-                a.p_md,
-                a.mb_xs,
-                t.atoms.bg_contrast_25,
-                {
-                  backgroundColor: t.palette.negative_400,
-                },
-              ]}>
-              <Text
+            {isPreferencesLoading ? (
+              <Loader />
+            ) : preferencesError || !preferences ? (
+              <View
                 style={[
-                  a.italic,
-                  {color: t.palette.white},
-                  native({marginTop: 2}),
+                  a.py_md,
+                  a.px_lg,
+                  a.rounded_md,
+                  t.atoms.bg_contrast_25,
                 ]}>
-                {error}
-              </Text>
-            </View>
-          )}
-        </View>
-
-        <Divider />
-
-        <View style={[a.pt_2xl]}>
-          <Text
-            style={[
-              a.text_md,
-              a.font_bold,
-              a.pb_md,
-              t.atoms.text_contrast_high,
-            ]}>
-            <Trans>Your muted words</Trans>
-          </Text>
+                <Text style={[a.italic, t.atoms.text_contrast_high]}>
+                  <Trans>
+                    We're sorry, but we weren't able to load your muted words at
+                    this time. Please try again.
+                  </Trans>
+                </Text>
+              </View>
+            ) : preferences.moderationPrefs.mutedWords.length ? (
+              [...preferences.moderationPrefs.mutedWords]
+                .reverse()
+                .map((word, i) => (
+                  <MutedWordRow
+                    key={word.value + i}
+                    word={word}
+                    style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
+                  />
+                ))
+            ) : (
+              <View
+                style={[
+                  a.py_md,
+                  a.px_lg,
+                  a.rounded_md,
+                  t.atoms.bg_contrast_25,
+                ]}>
+                <Text style={[a.italic, t.atoms.text_contrast_high]}>
+                  <Trans>You haven't muted any words or tags yet</Trans>
+                </Text>
+              </View>
+            )}
+          </View>
 
-          {isPreferencesLoading ? (
-            <Loader />
-          ) : preferencesError || !preferences ? (
-            <View
-              style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
-              <Text style={[a.italic, t.atoms.text_contrast_high]}>
-                <Trans>
-                  We're sorry, but we weren't able to load your muted words at
-                  this time. Please try again.
-                </Trans>
-              </Text>
-            </View>
-          ) : preferences.moderationPrefs.mutedWords.length ? (
-            [...preferences.moderationPrefs.mutedWords]
-              .reverse()
-              .map((word, i) => (
-                <MutedWordRow
-                  key={word.value + i}
-                  word={word}
-                  style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
-                />
-              ))
-          ) : (
-            <View
-              style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
-              <Text style={[a.italic, t.atoms.text_contrast_high]}>
-                <Trans>You haven't muted any words or tags yet</Trans>
-              </Text>
-            </View>
-          )}
+          {isNative && <View style={{height: 20}} />}
         </View>
 
-        {isNative && <View style={{height: 20}} />}
-      </View>
+        <Dialog.Close />
+      </Dialog.ScrollableInner>
 
-      <Dialog.Close />
-    </Dialog.ScrollableInner>
+      <Portal.Outlet />
+    </Portal.Provider>
   )
 }
 
@@ -437,6 +482,7 @@ function MutedWordRow({
         onConfirm={remove}
         confirmButtonCta={_(msg`Remove`)}
         confirmButtonColor="negative"
+        Portal={Portal.Portal}
       />
 
       <View
diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx
index 47eefae6f..bddc49968 100644
--- a/src/components/dialogs/PostInteractionSettingsDialog.tsx
+++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx
@@ -37,6 +37,7 @@ import * as Toggle from '#/components/forms/Toggle'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {Loader} from '#/components/Loader'
+import {PortalComponent} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
 export type PostInteractionSettingsFormProps = {
@@ -54,13 +55,15 @@ export type PostInteractionSettingsFormProps = {
 
 export function PostInteractionSettingsControlledDialog({
   control,
+  Portal,
   ...rest
 }: PostInteractionSettingsFormProps & {
   control: Dialog.DialogControlProps
+  Portal?: PortalComponent
 }) {
   const {_} = useLingui()
   return (
-    <Dialog.Outer control={control}>
+    <Dialog.Outer control={control} Portal={Portal}>
       <Dialog.Handle />
       <Dialog.ScrollableInner
         label={_(msg`Edit post interaction settings`)}
@@ -231,7 +234,6 @@ export function PostInteractionSettingsForm({
 }: PostInteractionSettingsFormProps) {
   const t = useTheme()
   const {_} = useLingui()
-  const control = Dialog.useDialogContext()
   const {data: lists} = useMyListsQuery('curate')
   const [quotesEnabled, setQuotesEnabled] = React.useState(
     !(
@@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({
       <Button
         label={_(msg`Save`)}
         onPress={onSave}
-        onAccessibilityEscape={control.close}
         color="primary"
         size="large"
         variant="solid"
diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx
index 0bd4bcb8c..ea870e2da 100644
--- a/src/components/dialogs/SwitchAccount.tsx
+++ b/src/components/dialogs/SwitchAccount.tsx
@@ -43,7 +43,6 @@ export function SwitchAccountDialog({
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`Switch Account`)}>
         <View style={[a.gap_lg]}>
           <Text style={[a.text_2xl, a.font_bold]}>
diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx
index f160c8774..f29dc356d 100644
--- a/src/components/dialogs/nuxs/NeueTypography.tsx
+++ b/src/components/dialogs/nuxs/NeueTypography.tsx
@@ -44,7 +44,6 @@ export function NeueTypography() {
   return (
     <Dialog.Outer control={control} onClose={onClose}>
       <Dialog.Handle />
-
       <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
         <View style={[a.gap_xl]}>
           <View style={[a.gap_md]}>