about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Dialog/index.web.tsx5
-rw-r--r--src/components/WhoCanReply.tsx150
-rw-r--r--src/components/dialogs/EmbedConsent.tsx1
-rw-r--r--src/components/dialogs/ThreadgateEditor.tsx218
-rw-r--r--src/state/modals/index.tsx9
-rw-r--r--src/view/com/composer/threadgate/ThreadgateBtn.tsx50
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/modals/Threadgate.tsx208
9 files changed, 334 insertions, 314 deletions
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 35d807b4b..aff1842f7 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -88,7 +88,10 @@ export function Outer({
     if (!isOpen) return
 
     function handler(e: KeyboardEvent) {
-      if (e.key === 'Escape') close()
+      if (e.key === 'Escape') {
+        e.stopPropagation()
+        close()
+      }
     }
 
     document.addEventListener('keydown', handler)
diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx
index cd171a0a4..a73aae850 100644
--- a/src/components/WhoCanReply.tsx
+++ b/src/components/WhoCanReply.tsx
@@ -17,7 +17,6 @@ import {HITSLOP_10} from '#/lib/constants'
 import {makeListLink, makeProfileLink} from '#/lib/routes/links'
 import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
 import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread'
 import {
   ThreadgateSetting,
@@ -34,6 +33,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
 import {Text} from '#/components/Typography'
 import {TextLink} from '../view/com/util/Link'
+import {ThreadgateEditorDialog} from './dialogs/ThreadgateEditor'
 import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
 
 interface WhoCanReplyProps {
@@ -46,7 +46,15 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
   const {_} = useLingui()
   const t = useTheme()
   const infoDialogControl = useDialogControl()
-  const {settings, isRootPost, onPressEdit} = useWhoCanReply(post)
+  const editDialogControl = useDialogControl()
+  const agent = useAgent()
+  const queryClient = useQueryClient()
+
+  const settings = React.useMemo(
+    () => threadgateViewToSettings(post.threadgate),
+    [post],
+  )
+  const isRootPost = !('reply' in post.record)
 
   if (!isRootPost) {
     return null
@@ -63,6 +71,55 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
     ? _(msg`Replies disabled`)
     : _(msg`Some people can reply`)
 
+  const onPressEdit = () => {
+    if (isNative && Keyboard.isVisible()) {
+      Keyboard.dismiss()
+    }
+    if (isThreadAuthor) {
+      editDialogControl.open()
+    } else {
+      infoDialogControl.open()
+    }
+  }
+
+  const onEditConfirm = async (newSettings: ThreadgateSetting[]) => {
+    if (JSON.stringify(settings) === JSON.stringify(newSettings)) {
+      return
+    }
+    try {
+      if (newSettings.length) {
+        await createThreadgate(agent, post.uri, newSettings)
+      } else {
+        await agent.api.com.atproto.repo.deleteRecord({
+          repo: agent.session!.did,
+          collection: 'app.bsky.feed.threadgate',
+          rkey: new AtUri(post.uri).rkey,
+        })
+      }
+      await whenAppViewReady(agent, post.uri, res => {
+        const thread = res.data.thread
+        if (AppBskyFeedDefs.isThreadViewPost(thread)) {
+          const fetchedSettings = threadgateViewToSettings(
+            thread.post.threadgate,
+          )
+          return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
+        }
+        return false
+      })
+      Toast.show(_(msg`Thread settings updated`))
+      queryClient.invalidateQueries({
+        queryKey: [POST_THREAD_RQKEY_ROOT],
+      })
+    } catch (err) {
+      Toast.show(
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
+      )
+      logger.error('Failed to edit threadgate', {message: err})
+    }
+  }
+
   return (
     <>
       <Button
@@ -93,7 +150,18 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
           </View>
         )}
       </Button>
-      <WhoCanReplyDialog control={infoDialogControl} post={post} />
+      <WhoCanReplyDialog
+        control={infoDialogControl}
+        post={post}
+        settings={settings}
+      />
+      {isThreadAuthor && (
+        <ThreadgateEditorDialog
+          control={editDialogControl}
+          threadgate={settings}
+          onConfirm={onEditConfirm}
+        />
+      )}
     </>
   )
 }
@@ -113,24 +181,31 @@ function Icon({
   return <IconComponent fill={color} width={width} />
 }
 
-export function WhoCanReplyDialog({
+function WhoCanReplyDialog({
   control,
   post,
+  settings,
 }: {
   control: Dialog.DialogControlProps
   post: AppBskyFeedDefs.PostView
+  settings: ThreadgateSetting[]
 }) {
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
-      <WhoCanReplyDialogInner post={post} />
+      <WhoCanReplyDialogInner post={post} settings={settings} />
     </Dialog.Outer>
   )
 }
 
-function WhoCanReplyDialogInner({post}: {post: AppBskyFeedDefs.PostView}) {
+function WhoCanReplyDialogInner({
+  post,
+  settings,
+}: {
+  post: AppBskyFeedDefs.PostView
+  settings: ThreadgateSetting[]
+}) {
   const {_} = useLingui()
-  const {settings} = useWhoCanReply(post)
   return (
     <Dialog.ScrollableInner
       label={_(msg`Who can reply dialog`)}
@@ -245,67 +320,6 @@ function Separator({i, length}: {i: number; length: number}) {
   return <>, </>
 }
 
-function useWhoCanReply(post: AppBskyFeedDefs.PostView) {
-  const agent = useAgent()
-  const queryClient = useQueryClient()
-  const {openModal} = useModalControls()
-
-  const settings = React.useMemo(
-    () => threadgateViewToSettings(post.threadgate),
-    [post],
-  )
-  const isRootPost = !('reply' in post.record)
-
-  const onPressEdit = () => {
-    if (isNative && Keyboard.isVisible()) {
-      Keyboard.dismiss()
-    }
-    openModal({
-      name: 'threadgate',
-      settings,
-      async onConfirm(newSettings: ThreadgateSetting[]) {
-        if (JSON.stringify(settings) === JSON.stringify(newSettings)) {
-          return
-        }
-        try {
-          if (newSettings.length) {
-            await createThreadgate(agent, post.uri, newSettings)
-          } else {
-            await agent.api.com.atproto.repo.deleteRecord({
-              repo: agent.session!.did,
-              collection: 'app.bsky.feed.threadgate',
-              rkey: new AtUri(post.uri).rkey,
-            })
-          }
-          await whenAppViewReady(agent, post.uri, res => {
-            const thread = res.data.thread
-            if (AppBskyFeedDefs.isThreadViewPost(thread)) {
-              const fetchedSettings = threadgateViewToSettings(
-                thread.post.threadgate,
-              )
-              return (
-                JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
-              )
-            }
-            return false
-          })
-          Toast.show('Thread settings updated')
-          queryClient.invalidateQueries({
-            queryKey: [POST_THREAD_RQKEY_ROOT],
-          })
-        } catch (err) {
-          Toast.show(
-            'There was an issue. Please check your internet connection and try again.',
-          )
-          logger.error('Failed to edit threadgate', {message: err})
-        }
-      },
-    })
-  }
-
-  return {settings, isRootPost, onPressEdit}
-}
-
 async function whenAppViewReady(
   agent: BskyAgent,
   uri: string,
diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx
index c3fefd9f0..f7e614597 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -113,6 +113,7 @@ export function EmbedConsentDialog({
             </ButtonText>
           </Button>
         </View>
+        <Dialog.Close />
       </Dialog.ScrollableInner>
     </Dialog.Outer>
   )
diff --git a/src/components/dialogs/ThreadgateEditor.tsx b/src/components/dialogs/ThreadgateEditor.tsx
new file mode 100644
index 000000000..75383764f
--- /dev/null
+++ b/src/components/dialogs/ThreadgateEditor.tsx
@@ -0,0 +1,218 @@
+import React from 'react'
+import {StyleProp, View, ViewStyle} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import isEqual from 'lodash.isequal'
+
+import {useMyListsQuery} from '#/state/queries/my-lists'
+import {ThreadgateSetting} from '#/state/queries/threadgate'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
+import {Text} from '#/components/Typography'
+
+interface ThreadgateEditorDialogProps {
+  control: Dialog.DialogControlProps
+  threadgate: ThreadgateSetting[]
+  onChange?: (v: ThreadgateSetting[]) => void
+  onConfirm?: (v: ThreadgateSetting[]) => void
+}
+
+export function ThreadgateEditorDialog({
+  control,
+  threadgate,
+  onChange,
+  onConfirm,
+}: ThreadgateEditorDialogProps) {
+  return (
+    <Dialog.Outer control={control}>
+      <Dialog.Handle />
+      <DialogContent
+        seedThreadgate={threadgate}
+        onChange={onChange}
+        onConfirm={onConfirm}
+      />
+    </Dialog.Outer>
+  )
+}
+
+function DialogContent({
+  seedThreadgate,
+  onChange,
+  onConfirm,
+}: {
+  seedThreadgate: ThreadgateSetting[]
+  onChange?: (v: ThreadgateSetting[]) => void
+  onConfirm?: (v: ThreadgateSetting[]) => void
+}) {
+  const {_} = useLingui()
+  const control = Dialog.useDialogContext()
+  const {data: lists} = useMyListsQuery('curate')
+  const [draft, setDraft] = React.useState(seedThreadgate)
+
+  const [prevSeedThreadgate, setPrevSeedThreadgate] =
+    React.useState(seedThreadgate)
+  if (seedThreadgate !== prevSeedThreadgate) {
+    // New data flowed from above (e.g. due to update coming through).
+    setPrevSeedThreadgate(seedThreadgate)
+    setDraft(seedThreadgate) // Reset draft.
+  }
+
+  function updateThreadgate(nextThreadgate: ThreadgateSetting[]) {
+    setDraft(nextThreadgate)
+    onChange?.(nextThreadgate)
+  }
+
+  const onPressEverybody = () => {
+    updateThreadgate([])
+  }
+
+  const onPressNobody = () => {
+    updateThreadgate([{type: 'nobody'}])
+  }
+
+  const onPressAudience = (setting: ThreadgateSetting) => {
+    // remove nobody
+    let newSelected = draft.filter(v => v.type !== 'nobody')
+    // toggle
+    const i = newSelected.findIndex(v => isEqual(v, setting))
+    if (i === -1) {
+      newSelected.push(setting)
+    } else {
+      newSelected.splice(i, 1)
+    }
+    updateThreadgate(newSelected)
+  }
+
+  const doneLabel = onConfirm ? _(msg`Save`) : _(msg`Done`)
+  return (
+    <Dialog.ScrollableInner
+      label={_(msg`Choose who can reply`)}
+      style={[{maxWidth: 500}, a.w_full]}>
+      <View style={[a.flex_1, a.gap_md]}>
+        <Text style={[a.text_2xl, a.font_bold]}>
+          <Trans>Chose who can reply</Trans>
+        </Text>
+        <Text style={a.mt_xs}>
+          <Trans>Either choose "Everybody" or "Nobody"</Trans>
+        </Text>
+        <View style={[a.flex_row, a.gap_sm]}>
+          <Selectable
+            label={_(msg`Everybody`)}
+            isSelected={draft.length === 0}
+            onPress={onPressEverybody}
+            style={{flex: 1}}
+          />
+          <Selectable
+            label={_(msg`Nobody`)}
+            isSelected={!!draft.find(v => v.type === 'nobody')}
+            onPress={onPressNobody}
+            style={{flex: 1}}
+          />
+        </View>
+        <Text style={a.mt_md}>
+          <Trans>Or combine these options:</Trans>
+        </Text>
+        <View style={[a.gap_sm]}>
+          <Selectable
+            label={_(msg`Mentioned users`)}
+            isSelected={!!draft.find(v => v.type === 'mention')}
+            onPress={() => onPressAudience({type: 'mention'})}
+          />
+          <Selectable
+            label={_(msg`Followed users`)}
+            isSelected={!!draft.find(v => v.type === 'following')}
+            onPress={() => onPressAudience({type: 'following'})}
+          />
+          {lists && lists.length > 0
+            ? lists.map(list => (
+                <Selectable
+                  key={list.uri}
+                  label={_(msg`Users in "${list.name}"`)}
+                  isSelected={
+                    !!draft.find(v => v.type === 'list' && v.list === list.uri)
+                  }
+                  onPress={() =>
+                    onPressAudience({type: 'list', list: list.uri})
+                  }
+                />
+              ))
+            : // No loading states to avoid jumps for the common case (no lists)
+              null}
+        </View>
+      </View>
+      <Button
+        label={doneLabel}
+        onPress={() => {
+          control.close()
+          onConfirm?.(draft)
+        }}
+        onAccessibilityEscape={control.close}
+        color="primary"
+        size="medium"
+        variant="solid"
+        style={a.mt_xl}>
+        <ButtonText>{doneLabel}</ButtonText>
+      </Button>
+      <Dialog.Close />
+    </Dialog.ScrollableInner>
+  )
+}
+
+function Selectable({
+  label,
+  isSelected,
+  onPress,
+  style,
+}: {
+  label: string
+  isSelected: boolean
+  onPress: () => void
+  style?: StyleProp<ViewStyle>
+}) {
+  const t = useTheme()
+  return (
+    <Button
+      onPress={onPress}
+      label={label}
+      accessibilityHint="Select this option"
+      accessibilityRole="checkbox"
+      aria-checked={isSelected}
+      accessibilityState={{
+        checked: isSelected,
+      }}
+      style={a.flex_1}>
+      {({hovered, focused}) => (
+        <View
+          style={[
+            a.flex_1,
+            a.flex_row,
+            a.align_center,
+            a.justify_between,
+            a.rounded_sm,
+            a.p_md,
+            {height: 40}, // for consistency with checkmark icon visible or not
+            t.atoms.bg_contrast_50,
+            (hovered || focused) && t.atoms.bg_contrast_100,
+            isSelected && {
+              backgroundColor:
+                t.name === 'light'
+                  ? t.palette.primary_50
+                  : t.palette.primary_975,
+            },
+            style,
+          ]}>
+          <Text style={[a.text_sm, isSelected && a.font_semibold]}>
+            {label}
+          </Text>
+          {isSelected ? (
+            <Check size="sm" fill={t.palette.primary_500} />
+          ) : (
+            <View />
+          )}
+        </View>
+      )}
+    </Button>
+  )
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 685b10bd8..529dc5590 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -5,7 +5,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {GalleryModel} from '#/state/models/media/gallery'
 import {ImageModel} from '#/state/models/media/image'
-import {ThreadgateSetting} from '../queries/threadgate'
 
 export interface EditProfileModal {
   name: 'edit-profile'
@@ -67,13 +66,6 @@ export interface SelfLabelModal {
   onChange: (labels: string[]) => void
 }
 
-export interface ThreadgateModal {
-  name: 'threadgate'
-  settings: ThreadgateSetting[]
-  onChange?: (settings: ThreadgateSetting[]) => void
-  onConfirm?: (settings: ThreadgateSetting[]) => void
-}
-
 export interface ChangeHandleModal {
   name: 'change-handle'
   onChanged: () => void
@@ -149,7 +141,6 @@ export type Modal =
   | CropImageModal
   | EditImageModal
   | SelfLabelModal
-  | ThreadgateModal
 
   // Bluesky access
   | WaitlistModal
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index 2aefdfbbf..6cf2eea2c 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -5,11 +5,12 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {isNative} from '#/platform/detection'
-import {useModalControls} from '#/state/modals'
 import {ThreadgateSetting} from '#/state/queries/threadgate'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {ThreadgateEditorDialog} from '#/components/dialogs/ThreadgateEditor'
 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
 import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
@@ -26,18 +27,15 @@ export function ThreadgateBtn({
   const {track} = useAnalytics()
   const {_} = useLingui()
   const t = useTheme()
-  const {openModal} = useModalControls()
+  const control = Dialog.useDialogControl()
 
   const onPress = () => {
     track('Composer:ThreadgateOpened')
     if (isNative && Keyboard.isVisible()) {
       Keyboard.dismiss()
     }
-    openModal({
-      name: 'threadgate',
-      settings: threadgate,
-      onChange,
-    })
+
+    control.open()
   }
 
   const isEverybody = threadgate.length === 0
@@ -49,19 +47,29 @@ export function ThreadgateBtn({
     : _(msg`Some people can reply`)
 
   return (
-    <Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
-      <Button
-        variant="solid"
-        color="secondary"
-        size="xsmall"
-        testID="openReplyGateButton"
-        onPress={onPress}
-        label={label}>
-        <ButtonIcon
-          icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
-        />
-        <ButtonText>{label}</ButtonText>
-      </Button>
-    </Animated.View>
+    <>
+      <Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
+        <Button
+          variant="solid"
+          color="secondary"
+          size="xsmall"
+          testID="openReplyGateButton"
+          onPress={onPress}
+          label={label}
+          accessibilityHint={_(
+            msg`Opens a dialog to choose who can reply to this thread`,
+          )}>
+          <ButtonIcon
+            icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
+          />
+          <ButtonText>{label}</ButtonText>
+        </Button>
+      </Animated.View>
+      <ThreadgateEditorDialog
+        control={control}
+        threadgate={threadgate}
+        onChange={onChange}
+      />
+    </>
   )
 }
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index ecfe5806e..3455e1cdf 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin
 import * as LinkWarningModal from './LinkWarning'
 import * as ListAddUserModal from './ListAddRemoveUsers'
 import * as SelfLabelModal from './SelfLabel'
-import * as ThreadgateModal from './Threadgate'
 import * as UserAddRemoveListsModal from './UserAddRemoveLists'
 import * as VerifyEmailModal from './VerifyEmail'
 
@@ -76,9 +75,6 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'self-label') {
     snapPoints = SelfLabelModal.snapPoints
     element = <SelfLabelModal.Component {...activeModal} />
-  } else if (activeModal?.name === 'threadgate') {
-    snapPoints = ThreadgateModal.snapPoints
-    element = <ThreadgateModal.Component {...activeModal} />
   } else if (activeModal?.name === 'alt-text-image') {
     snapPoints = AltImageModal.snapPoints
     element = <AltImageModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 14ee99e57..c4bab6fb1 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin
 import * as LinkWarningModal from './LinkWarning'
 import * as ListAddUserModal from './ListAddRemoveUsers'
 import * as SelfLabelModal from './SelfLabel'
-import * as ThreadgateModal from './Threadgate'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
 import * as VerifyEmailModal from './VerifyEmail'
 
@@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <DeleteAccountModal.Component />
   } else if (modal.name === 'self-label') {
     element = <SelfLabelModal.Component {...modal} />
-  } else if (modal.name === 'threadgate') {
-    element = <ThreadgateModal.Component {...modal} />
   } else if (modal.name === 'change-handle') {
     element = <ChangeHandleModal.Component {...modal} />
   } else if (modal.name === 'invite-codes') {
diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx
deleted file mode 100644
index 4a9a9e2ab..000000000
--- a/src/view/com/modals/Threadgate.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import React, {useState} from 'react'
-import {
-  Pressable,
-  StyleProp,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import isEqual from 'lodash.isequal'
-
-import {useModalControls} from '#/state/modals'
-import {useMyListsQuery} from '#/state/queries/my-lists'
-import {ThreadgateSetting} from '#/state/queries/threadgate'
-import {usePalette} from 'lib/hooks/usePalette'
-import {colors, s} from 'lib/styles'
-import {isWeb} from 'platform/detection'
-import {ScrollView} from 'view/com/modals/util'
-import {Text} from '../util/text/Text'
-
-export const snapPoints = ['60%']
-
-export function Component({
-  settings,
-  onChange,
-  onConfirm,
-}: {
-  settings: ThreadgateSetting[]
-  onChange?: (settings: ThreadgateSetting[]) => void
-  onConfirm?: (settings: ThreadgateSetting[]) => void
-}) {
-  const pal = usePalette('default')
-  const {closeModal} = useModalControls()
-  const [selected, setSelected] = useState(settings)
-  const {_} = useLingui()
-  const {data: lists} = useMyListsQuery('curate')
-
-  const onPressEverybody = () => {
-    setSelected([])
-    onChange?.([])
-  }
-
-  const onPressNobody = () => {
-    setSelected([{type: 'nobody'}])
-    onChange?.([{type: 'nobody'}])
-  }
-
-  const onPressAudience = (setting: ThreadgateSetting) => {
-    // remove nobody
-    let newSelected = selected.filter(v => v.type !== 'nobody')
-    // toggle
-    const i = newSelected.findIndex(v => isEqual(v, setting))
-    if (i === -1) {
-      newSelected.push(setting)
-    } else {
-      newSelected.splice(i, 1)
-    }
-    setSelected(newSelected)
-    onChange?.(newSelected)
-  }
-
-  return (
-    <View testID="threadgateModal" style={[pal.view, styles.container]}>
-      <View style={styles.titleSection}>
-        <Text type="title-lg" style={[pal.text, styles.title]}>
-          <Trans>Who can reply</Trans>
-        </Text>
-      </View>
-
-      <ScrollView>
-        <Text style={[pal.text, styles.description]}>
-          <Trans>Choose "Everybody" or "Nobody"</Trans>
-        </Text>
-        <View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
-          <Selectable
-            label={_(msg`Everybody`)}
-            isSelected={selected.length === 0}
-            onPress={onPressEverybody}
-            style={{flex: 1}}
-          />
-          <Selectable
-            label={_(msg`Nobody`)}
-            isSelected={!!selected.find(v => v.type === 'nobody')}
-            onPress={onPressNobody}
-            style={{flex: 1}}
-          />
-        </View>
-        <Text style={[pal.text, styles.description]}>
-          <Trans>Or combine these options:</Trans>
-        </Text>
-        <View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
-          <Selectable
-            label={_(msg`Mentioned users`)}
-            isSelected={!!selected.find(v => v.type === 'mention')}
-            onPress={() => onPressAudience({type: 'mention'})}
-          />
-          <Selectable
-            label={_(msg`Followed users`)}
-            isSelected={!!selected.find(v => v.type === 'following')}
-            onPress={() => onPressAudience({type: 'following'})}
-          />
-          {lists?.length
-            ? lists.map(list => (
-                <Selectable
-                  key={list.uri}
-                  label={_(msg`Users in "${list.name}"`)}
-                  isSelected={
-                    !!selected.find(
-                      v => v.type === 'list' && v.list === list.uri,
-                    )
-                  }
-                  onPress={() =>
-                    onPressAudience({type: 'list', list: list.uri})
-                  }
-                />
-              ))
-            : null}
-        </View>
-      </ScrollView>
-
-      <View style={[styles.btnContainer, pal.borderDark]}>
-        <TouchableOpacity
-          testID="confirmBtn"
-          onPress={() => {
-            closeModal()
-            onConfirm?.(selected)
-          }}
-          style={styles.btn}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
-          accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>
-            <Trans context="action">Done</Trans>
-          </Text>
-        </TouchableOpacity>
-      </View>
-    </View>
-  )
-}
-
-function Selectable({
-  label,
-  isSelected,
-  onPress,
-  style,
-}: {
-  label: string
-  isSelected: boolean
-  onPress: () => void
-  style?: StyleProp<ViewStyle>
-}) {
-  const pal = usePalette(isSelected ? 'inverted' : 'default')
-  return (
-    <Pressable
-      onPress={onPress}
-      accessibilityLabel={label}
-      accessibilityHint=""
-      style={[styles.selectable, pal.border, pal.view, style]}>
-      <Text type="lg" style={[pal.text]}>
-        {label}
-      </Text>
-      {isSelected ? (
-        <FontAwesomeIcon icon="check" color={pal.colors.text} size={18} />
-      ) : null}
-    </Pressable>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    paddingBottom: isWeb ? 0 : 40,
-  },
-  titleSection: {
-    paddingTop: isWeb ? 0 : 4,
-  },
-  title: {
-    textAlign: 'center',
-    fontWeight: '600',
-  },
-  description: {
-    textAlign: 'center',
-    paddingVertical: 16,
-  },
-  selectable: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    paddingHorizontal: 18,
-    paddingVertical: 16,
-    borderWidth: 1,
-    borderRadius: 6,
-  },
-  btn: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 32,
-    padding: 14,
-    backgroundColor: colors.blue3,
-  },
-  btnContainer: {
-    paddingTop: 20,
-    paddingHorizontal: 20,
-  },
-})