about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/auth/create/Step2.tsx8
-rw-r--r--src/view/com/auth/login/Login.tsx8
-rw-r--r--src/view/com/composer/Composer.tsx13
-rw-r--r--src/view/com/composer/labels/LabelsBtn.tsx6
-rw-r--r--src/view/com/composer/photos/Gallery.tsx28
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx8
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx8
-rw-r--r--src/view/com/lists/ListItems.tsx8
-rw-r--r--src/view/com/modals/AddAppPasswords.tsx6
-rw-r--r--src/view/com/modals/AltImage.tsx10
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx4
-rw-r--r--src/view/com/modals/ChangeEmail.tsx8
-rw-r--r--src/view/com/modals/ChangeHandle.tsx9
-rw-r--r--src/view/com/modals/Confirm.tsx8
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx9
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx9
-rw-r--r--src/view/com/modals/DeleteAccount.tsx6
-rw-r--r--src/view/com/modals/EditImage.tsx8
-rw-r--r--src/view/com/modals/EditProfile.tsx10
-rw-r--r--src/view/com/modals/InviteCodes.tsx6
-rw-r--r--src/view/com/modals/LinkWarning.tsx8
-rw-r--r--src/view/com/modals/ListAddUser.tsx4
-rw-r--r--src/view/com/modals/Modal.tsx24
-rw-r--r--src/view/com/modals/Modal.web.tsx17
-rw-r--r--src/view/com/modals/ModerationDetails.tsx9
-rw-r--r--src/view/com/modals/Repost.tsx6
-rw-r--r--src/view/com/modals/SelfLabel.tsx6
-rw-r--r--src/view/com/modals/ServerInput.tsx6
-rw-r--r--src/view/com/modals/UserAddRemoveLists.tsx10
-rw-r--r--src/view/com/modals/VerifyEmail.tsx10
-rw-r--r--src/view/com/modals/Waitlist.tsx6
-rw-r--r--src/view/com/modals/crop-image/CropImage.web.tsx8
-rw-r--r--src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx8
-rw-r--r--src/view/com/modals/lang-settings/PostLanguagesSettings.tsx8
-rw-r--r--src/view/com/modals/report/Modal.tsx6
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx8
-rw-r--r--src/view/com/profile/ProfileHeader.tsx22
-rw-r--r--src/view/com/testing/TestCtrls.e2e.tsx4
-rw-r--r--src/view/com/util/Link.tsx28
-rw-r--r--src/view/com/util/UserPreviewLink.tsx6
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx8
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx8
-rw-r--r--src/view/com/util/moderation/PostAlerts.tsx6
-rw-r--r--src/view/com/util/moderation/PostHider.tsx6
-rw-r--r--src/view/com/util/moderation/ProfileHeaderAlerts.tsx6
-rw-r--r--src/view/com/util/moderation/ScreenHider.tsx6
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx9
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx8
48 files changed, 252 insertions, 189 deletions
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
index 60e197564..b2054150b 100644
--- a/src/view/com/auth/create/Step2.tsx
+++ b/src/view/com/auth/create/Step2.tsx
@@ -10,8 +10,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {TextInput} from '../util/TextInput'
 import {Policies} from './Policies'
 import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import {useStores} from 'state/index'
 import {isWeb} from 'platform/detection'
+import {useModalControls} from '#/state/modals'
 
 /** STEP 2: Your account
  * @field Invite code or waitlist
@@ -28,11 +28,11 @@ export const Step2 = observer(function Step2Impl({
   model: CreateAccountModel
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {openModal} = useModalControls()
 
   const onPressWaitlist = React.useCallback(() => {
-    store.shell.openModal({name: 'waitlist'})
-  }, [store])
+    openModal({name: 'waitlist'})
+  }, [openModal])
 
   return (
     <View>
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index acc05b6ca..24a657c66 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -31,6 +31,7 @@ import {useTheme} from 'lib/ThemeContext'
 import {cleanError} from 'lib/strings/errors'
 import {isWeb} from 'platform/detection'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 enum Forms {
   Login,
@@ -303,9 +304,10 @@ const LoginForm = ({
   const [identifier, setIdentifier] = useState<string>(initialHandle)
   const [password, setPassword] = useState<string>('')
   const passwordInputRef = useRef<TextInput>(null)
+  const {openModal} = useModalControls()
 
   const onPressSelectService = () => {
-    store.shell.openModal({
+    openModal({
       name: 'server-input',
       initialService: serviceUrl,
       onSelect: setServiceUrl,
@@ -528,7 +530,6 @@ const LoginForm = ({
 }
 
 const ForgotPasswordForm = ({
-  store,
   error,
   serviceUrl,
   serviceDescription,
@@ -551,13 +552,14 @@ const ForgotPasswordForm = ({
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [email, setEmail] = useState<string>('')
   const {screen} = useAnalytics()
+  const {openModal} = useModalControls()
 
   useEffect(() => {
     screen('Signin:ForgotPassword')
   }, [screen])
 
   const onPressSelectService = () => {
-    store.shell.openModal({
+    openModal({
       name: 'server-input',
       initialService: serviceUrl,
       onSelect: setServiceUrl,
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 632e72fde..68f706828 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -49,6 +49,7 @@ import {LabelsBtn} from './labels/LabelsBtn'
 import {SelectLangBtn} from './select-language/SelectLangBtn'
 import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
 import {insertMentionAt} from 'lib/strings/mention-manip'
+import {useModals, useModalControls} from '#/state/modals'
 import {useRequireAltTextEnabled} from '#/state/preferences'
 import {
   useLanguagePrefs,
@@ -64,6 +65,8 @@ export const ComposePost = observer(function ComposePost({
   quote: initQuote,
   mention: initMention,
 }: Props) {
+  const {activeModals} = useModals()
+  const {openModal, closeModal} = useModalControls()
   const {track} = useAnalytics()
   const pal = usePalette('default')
   const {isDesktop, isMobile} = useWebMediaQueries()
@@ -118,18 +121,18 @@ export const ComposePost = observer(function ComposePost({
 
   const onPressCancel = useCallback(() => {
     if (graphemeLength > 0 || !gallery.isEmpty) {
-      if (store.shell.activeModals.some(modal => modal.name === 'confirm')) {
-        store.shell.closeModal()
+      if (activeModals.some(modal => modal.name === 'confirm')) {
+        closeModal()
       }
       if (Keyboard) {
         Keyboard.dismiss()
       }
-      store.shell.openModal({
+      openModal({
         name: 'confirm',
         title: 'Discard draft',
         onPressConfirm: onClose,
         onPressCancel: () => {
-          store.shell.closeModal()
+          closeModal()
         },
         message: "Are you sure you'd like to discard this draft?",
         confirmBtnText: 'Discard',
@@ -138,7 +141,7 @@ export const ComposePost = observer(function ComposePost({
     } else {
       onClose()
     }
-  }, [store, onClose, graphemeLength, gallery])
+  }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery])
   // android back button
   useEffect(() => {
     if (!isAndroid) {
diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx
index 96908d47f..4b6ad81c7 100644
--- a/src/view/com/composer/labels/LabelsBtn.tsx
+++ b/src/view/com/composer/labels/LabelsBtn.tsx
@@ -3,11 +3,11 @@ import {Keyboard, StyleSheet} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {Button} from 'view/com/util/forms/Button'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {ShieldExclamation} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {isNative} from 'platform/detection'
+import {useModalControls} from '#/state/modals'
 
 export const LabelsBtn = observer(function LabelsBtn({
   labels,
@@ -19,7 +19,7 @@ export const LabelsBtn = observer(function LabelsBtn({
   onChange: (v: string[]) => void
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {openModal} = useModalControls()
 
   return (
     <Button
@@ -34,7 +34,7 @@ export const LabelsBtn = observer(function LabelsBtn({
             Keyboard.dismiss()
           }
         }
-        store.shell.openModal({name: 'self-label', labels, hasMedia, onChange})
+        openModal({name: 'self-label', labels, hasMedia, onChange})
       }}>
       <ShieldExclamation style={pal.link} size={26} />
       {labels.length > 0 ? (
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index fcd99842a..069a05475 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -7,11 +7,11 @@ import {s, colors} from 'lib/styles'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {Image} from 'expo-image'
 import {Text} from 'view/com/util/text/Text'
-import {openAltTextModal} from 'lib/media/alt-text'
 import {Dimensions} from 'lib/media/types'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useModalControls} from '#/state/modals'
+import {isNative} from 'platform/detection'
 
 const IMAGE_GAP = 8
 
@@ -47,9 +47,9 @@ const GalleryInner = observer(function GalleryImpl({
   gallery,
   containerInfo,
 }: GalleryInnerProps) {
-  const store = useStores()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
 
   let side: number
 
@@ -117,7 +117,10 @@ const GalleryInner = observer(function GalleryImpl({
               accessibilityHint=""
               onPress={() => {
                 Keyboard.dismiss()
-                openAltTextModal(store, image)
+                openModal({
+                  name: 'alt-text-image',
+                  image,
+                })
               }}
               style={[styles.altTextControl, altTextControlStyle]}>
               <Text style={styles.altTextControlLabel} accessible={false}>
@@ -137,7 +140,17 @@ const GalleryInner = observer(function GalleryImpl({
                 accessibilityRole="button"
                 accessibilityLabel="Edit image"
                 accessibilityHint=""
-                onPress={() => gallery.edit(image)}
+                onPress={() => {
+                  if (isNative) {
+                    gallery.crop(image)
+                  } else {
+                    openModal({
+                      name: 'edit-image',
+                      image,
+                      gallery,
+                    })
+                  }
+                }}
                 style={styles.imageControl}>
                 <FontAwesomeIcon
                   icon="pen"
@@ -165,7 +178,10 @@ const GalleryInner = observer(function GalleryImpl({
               accessibilityHint=""
               onPress={() => {
                 Keyboard.dismiss()
-                openAltTextModal(store, image)
+                openModal({
+                  name: 'alt-text-image',
+                  image,
+                })
               }}
               style={styles.altTextHiddenRegion}
             />
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
index 646542387..6c45f3384 100644
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ b/src/view/com/composer/select-language/SelectLangBtn.tsx
@@ -12,9 +12,9 @@ import {
   DropdownItemButton,
 } from 'view/com/util/forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {isNative} from 'platform/detection'
 import {codeToLanguageName} from '../../../../locale/helpers'
+import {useModalControls} from '#/state/modals'
 import {
   useLanguagePrefs,
   useSetLanguagePrefs,
@@ -24,7 +24,7 @@ import {
 
 export const SelectLangBtn = observer(function SelectLangBtn() {
   const pal = usePalette('default')
-  const store = useStores()
+  const {openModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useSetLanguagePrefs()
 
@@ -34,8 +34,8 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
         Keyboard.dismiss()
       }
     }
-    store.shell.openModal({name: 'post-languages-settings'})
-  }, [store])
+    openModal({name: 'post-languages-settings'})
+  }, [openModal])
 
   const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
   const items: DropdownItem[] = useMemo(() => {
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 2c4335dc1..63af52619 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -10,12 +10,12 @@ import {observer} from 'mobx-react-lite'
 import {FeedSourceModel} from 'state/models/content/feed-source'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
-import {useStores} from 'state/index'
 import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import * as Toast from 'view/com/util/Toast'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 export const FeedSourceCard = observer(function FeedSourceCardImpl({
   item,
@@ -30,13 +30,13 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
   showDescription?: boolean
   showLikes?: boolean
 }) {
-  const store = useStores()
   const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
+  const {openModal} = useModalControls()
 
   const onToggleSaved = React.useCallback(async () => {
     if (item.isSaved) {
-      store.shell.openModal({
+      openModal({
         name: 'confirm',
         title: 'Remove from my feeds',
         message: `Remove ${item.displayName} from my feeds?`,
@@ -59,7 +59,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
         logger.error('Failed to save feed', {error: e})
       }
     }
-  }, [store, item])
+  }, [openModal, item])
 
   return (
     <Pressable
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
index 192cdd9d3..3658e5522 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListItems.tsx
@@ -17,11 +17,11 @@ import {Button} from '../util/forms/Button'
 import {ListModel} from 'state/models/content/list'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
 import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
@@ -54,10 +54,10 @@ export const ListItems = observer(function ListItemsImpl({
   desktopFixedHeightOffset?: number
 }) {
   const pal = usePalette('default')
-  const store = useStores()
   const {track} = useAnalytics()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
   const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
 
   const data = React.useMemo(() => {
     let items: any[] = []
@@ -115,7 +115,7 @@ export const ListItems = observer(function ListItemsImpl({
 
   const onPressEditMembership = React.useCallback(
     (profile: AppBskyActorDefs.ProfileViewBasic) => {
-      store.shell.openModal({
+      openModal({
         name: 'user-add-remove-lists',
         subject: profile.did,
         displayName: profile.displayName || profile.handle,
@@ -131,7 +131,7 @@ export const ListItems = observer(function ListItemsImpl({
         },
       })
     },
-    [store, list],
+    [openModal, list],
   )
 
   // rendering
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx
index 29763620f..621c61b90 100644
--- a/src/view/com/modals/AddAppPasswords.tsx
+++ b/src/view/com/modals/AddAppPasswords.tsx
@@ -13,6 +13,7 @@ import {
 import Clipboard from '@react-native-clipboard/clipboard'
 import * as Toast from '../util/Toast'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['70%']
 
@@ -54,6 +55,7 @@ const shadesOfBlue: string[] = [
 export function Component({}: {}) {
   const pal = usePalette('default')
   const store = useStores()
+  const {closeModal} = useModalControls()
   const [name, setName] = useState(
     shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
   )
@@ -69,8 +71,8 @@ export function Component({}: {}) {
   }, [appPassword])
 
   const onDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const createAppPassword = async () => {
     // if name is all whitespace, we don't allow it
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
index c084e84a3..9c377a121 100644
--- a/src/view/com/modals/AltImage.tsx
+++ b/src/view/com/modals/AltImage.tsx
@@ -17,9 +17,9 @@ import {MAX_ALT_TEXT} from 'lib/constants'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {isAndroid, isWeb} from 'platform/detection'
 import {ImageModel} from 'state/models/media/image'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['fullscreen']
 
@@ -29,10 +29,10 @@ interface Props {
 
 export function Component({image}: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const theme = useTheme()
   const [altText, setAltText] = useState(image.altText)
   const windim = useWindowDimensions()
+  const {closeModal} = useModalControls()
 
   const imageStyles = useMemo<ImageStyle>(() => {
     const maxWidth = isWeb ? 450 : windim.width
@@ -53,11 +53,11 @@ export function Component({image}: Props) {
 
   const onPressSave = useCallback(() => {
     image.setAltText(altText)
-    store.shell.closeModal()
-  }, [store, image, altText])
+    closeModal()
+  }, [closeModal, image, altText])
 
   const onPressCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
index 6927ba8d2..7b0778f83 100644
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ b/src/view/com/modals/BirthDateSettings.tsx
@@ -15,12 +15,14 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
 export const Component = observer(function Component({}: {}) {
   const pal = usePalette('default')
   const store = useStores()
+  const {closeModal} = useModalControls()
   const [date, setDate] = useState<Date>(
     store.preferences.birthDate || new Date(),
   )
@@ -33,7 +35,7 @@ export const Component = observer(function Component({}: {}) {
     setIsProcessing(true)
     try {
       await store.preferences.setBirthDate(date)
-      store.shell.closeModal()
+      closeModal()
     } catch (e) {
       setError(cleanError(String(e)))
     } finally {
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 012570556..ec37aeede 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -12,6 +12,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
 
 enum Stages {
   InputEmail,
@@ -32,6 +33,7 @@ export const Component = observer(function Component({}: {}) {
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const {isMobile} = useWebMediaQueries()
+  const {openModal, closeModal} = useModalControls()
 
   const onRequestChange = async () => {
     if (email === store.session.currentSession?.email) {
@@ -90,8 +92,8 @@ export const Component = observer(function Component({}: {}) {
   }
 
   const onVerify = async () => {
-    store.shell.closeModal()
-    store.shell.openModal({name: 'verify-email'})
+    closeModal()
+    openModal({name: 'verify-email'})
   }
 
   return (
@@ -207,7 +209,7 @@ export const Component = observer(function Component({}: {}) {
               <Button
                 testID="cancelBtn"
                 type="default"
-                onPress={() => store.shell.closeModal()}
+                onPress={() => closeModal()}
                 accessibilityLabel="Cancel"
                 accessibilityHint=""
                 label="Cancel"
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index c54c1c043..6184cb3b7 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -22,6 +22,7 @@ import {useTheme} from 'lib/ThemeContext'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {cleanError} from 'lib/strings/errors'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['100%']
 
@@ -30,6 +31,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
   const [error, setError] = useState<string>('')
   const pal = usePalette('default')
   const {track} = useAnalytics()
+  const {closeModal} = useModalControls()
 
   const [isProcessing, setProcessing] = useState<boolean>(false)
   const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>(
@@ -85,8 +87,8 @@ export function Component({onChanged}: {onChanged: () => void}) {
   // events
   // =
   const onPressCancel = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
   const onPressRetryConnect = React.useCallback(
     () => setRetryDescribeTrigger({}),
     [setRetryDescribeTrigger],
@@ -110,7 +112,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
       await store.agent.updateHandle({
         handle: newHandle,
       })
-      store.shell.closeModal()
+      closeModal()
       onChanged()
     } catch (err: any) {
       setError(cleanError(err))
@@ -127,6 +129,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
     isCustom,
     onChanged,
     track,
+    closeModal,
   ])
 
   // rendering
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index c1324b1cb..6b942057b 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -6,13 +6,13 @@ import {
   View,
 } from 'react-native'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
-import type {ConfirmModal} from 'state/models/ui/shell'
+import type {ConfirmModal} from '#/state/modals'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
@@ -26,7 +26,7 @@ export function Component({
   cancelBtnText,
 }: ConfirmModal) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const onPress = async () => {
@@ -34,7 +34,7 @@ export function Component({
     setIsProcessing(true)
     try {
       await onPressConfirm()
-      store.shell.closeModal()
+      closeModal()
       return
     } catch (e: any) {
       setError(cleanError(e))
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 9075d0272..0891a6473 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -16,6 +16,7 @@ import {isIOS} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import * as Toast from '../util/Toast'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['90%']
 
@@ -24,14 +25,15 @@ export const Component = observer(
     const store = useStores()
     const {isMobile} = useWebMediaQueries()
     const pal = usePalette('default')
+    const {closeModal} = useModalControls()
 
     React.useEffect(() => {
       store.preferences.sync()
     }, [store])
 
     const onPressDone = React.useCallback(() => {
-      store.shell.closeModal()
-    }, [store])
+      closeModal()
+    }, [closeModal])
 
     return (
       <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
@@ -89,8 +91,9 @@ const AdultContentEnabledPref = observer(
   function AdultContentEnabledPrefImpl() {
     const store = useStores()
     const pal = usePalette('default')
+    const {openModal} = useModalControls()
 
-    const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'})
+    const onSetAge = () => openModal({name: 'birth-date-settings'})
 
     const onToggleAdultContent = async () => {
       if (isIOS) {
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 1ea12695f..cdad37770 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -24,6 +24,7 @@ import {useTheme} from 'lib/ThemeContext'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError, isNetworkError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
 
 const MAX_NAME = 64 // todo
 const MAX_DESCRIPTION = 300 // todo
@@ -40,6 +41,7 @@ export function Component({
   list?: ListModel
 }) {
   const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [error, setError] = useState<string>('')
   const pal = usePalette('default')
@@ -67,8 +69,8 @@ export function Component({
   const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
 
   const onPressCancel = useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const onSelectNewAvatar = useCallback(
     async (img: RNImage | null) => {
@@ -123,7 +125,7 @@ export function Component({
         Toast.show(`${purposeLabel} list created`)
         onSave?.(res.uri)
       }
-      store.shell.closeModal()
+      closeModal()
     } catch (e: any) {
       if (isNetworkError(e)) {
         setError(
@@ -141,6 +143,7 @@ export function Component({
     error,
     onSave,
     store,
+    closeModal,
     activePurpose,
     isCurateList,
     purposeLabel,
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 50a4cd603..9a8a8b4b0 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
 import {resetToTab} from '../../../Navigation'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['60%']
 
@@ -24,6 +25,7 @@ export function Component({}: {}) {
   const pal = usePalette('default')
   const theme = useTheme()
   const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
   const [confirmCode, setConfirmCode] = React.useState<string>('')
@@ -55,14 +57,14 @@ export function Component({}: {}) {
       Toast.show('Your account has been deleted')
       resetToTab('HomeTab')
       store.session.clear()
-      store.shell.closeModal()
+      closeModal()
     } catch (e: any) {
       setError(cleanError(e))
     }
     setIsProcessing(false)
   }
   const onCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
   return (
     <View style={[styles.container, pal.view]}>
diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
index dcb6668c7..a2a458f4c 100644
--- a/src/view/com/modals/EditImage.tsx
+++ b/src/view/com/modals/EditImage.tsx
@@ -6,7 +6,6 @@ import {gradients, s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import ImageEditor, {Position} from 'react-avatar-editor'
 import {TextInput} from './util'
@@ -19,6 +18,7 @@ import {Slider} from '@miblanchard/react-native-slider'
 import {MaterialIcons} from '@expo/vector-icons'
 import {observer} from 'mobx-react-lite'
 import {getKeys} from 'lib/type-assertions'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
@@ -52,9 +52,9 @@ export const Component = observer(function EditImageImpl({
 }: Props) {
   const pal = usePalette('default')
   const theme = useTheme()
-  const store = useStores()
   const windowDimensions = useWindowDimensions()
   const {isMobile} = useWebMediaQueries()
+  const {closeModal} = useModalControls()
 
   const {
     aspectRatio,
@@ -128,8 +128,8 @@ export const Component = observer(function EditImageImpl({
   }, [image])
 
   const onCloseModal = useCallback(() => {
-    store.shell.closeModal()
-  }, [store.shell])
+    closeModal()
+  }, [closeModal])
 
   const onPressCancel = useCallback(async () => {
     await gallery.previous(image)
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index dfd5305f5..f08bb2326 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -13,7 +13,6 @@ import LinearGradient from 'react-native-linear-gradient'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {Text} from '../util/text/Text'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
 import {ProfileModel} from 'state/models/content/profile'
 import {s, colors, gradients} from 'lib/styles'
 import {enforceLen} from 'lib/strings/helpers'
@@ -27,6 +26,7 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {cleanError, isNetworkError} from 'lib/strings/errors'
 import Animated, {FadeOut} from 'react-native-reanimated'
 import {isWeb} from 'platform/detection'
+import {useModalControls} from '#/state/modals'
 
 const AnimatedTouchableOpacity =
   Animated.createAnimatedComponent(TouchableOpacity)
@@ -40,11 +40,11 @@ export function Component({
   profileView: ProfileModel
   onUpdate?: () => void
 }) {
-  const store = useStores()
   const [error, setError] = useState<string>('')
   const pal = usePalette('default')
   const theme = useTheme()
   const {track} = useAnalytics()
+  const {closeModal} = useModalControls()
 
   const [isProcessing, setProcessing] = useState<boolean>(false)
   const [displayName, setDisplayName] = useState<string>(
@@ -66,7 +66,7 @@ export function Component({
     RNImage | undefined | null
   >()
   const onPressCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
   const onSelectNewAvatar = useCallback(
     async (img: RNImage | null) => {
@@ -123,7 +123,7 @@ export function Component({
       )
       Toast.show('Profile updated')
       onUpdate?.()
-      store.shell.closeModal()
+      closeModal()
     } catch (e: any) {
       if (isNetworkError(e)) {
         setError(
@@ -141,7 +141,7 @@ export function Component({
     error,
     profileView,
     onUpdate,
-    store,
+    closeModal,
     displayName,
     description,
     newUserAvatar,
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index a8aa164c3..227b25275 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -15,6 +15,7 @@ import {ScrollView} from './util'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useModalControls} from '#/state/modals'
 import {useInvitesState, useInvitesAPI} from '#/state/invites'
 import {UserInfoText} from '../util/UserInfoText'
 import {makeProfileLink} from '#/lib/routes/links'
@@ -25,11 +26,12 @@ export const snapPoints = ['70%']
 export function Component({}: {}) {
   const pal = usePalette('default')
   const store = useStores()
+  const {closeModal} = useModalControls()
   const {isTabletOrDesktop} = useWebMediaQueries()
 
   const onClose = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   if (store.me.invites.length === 0) {
     return (
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
index 67a156af4..751c69b3f 100644
--- a/src/view/com/modals/LinkWarning.tsx
+++ b/src/view/com/modals/LinkWarning.tsx
@@ -5,12 +5,12 @@ import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
@@ -22,12 +22,12 @@ export const Component = observer(function Component({
   href: string
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const potentiallyMisleading = isPossiblyAUrl(text)
 
   const onPressVisit = () => {
-    store.shell.closeModal()
+    closeModal()
     Linking.openURL(href)
   }
 
@@ -83,7 +83,7 @@ export const Component = observer(function Component({
           <Button
             testID="cancelBtn"
             type="default"
-            onPress={() => store.shell.closeModal()}
+            onPress={() => closeModal()}
             accessibilityLabel="Cancel"
             accessibilityHint=""
             label="Cancel"
diff --git a/src/view/com/modals/ListAddUser.tsx b/src/view/com/modals/ListAddUser.tsx
index a04e2d186..8864ebc78 100644
--- a/src/view/com/modals/ListAddUser.tsx
+++ b/src/view/com/modals/ListAddUser.tsx
@@ -26,6 +26,7 @@ import {cleanError} from 'lib/strings/errors'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {HITSLOP_20} from '#/lib/constants'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['90%']
 
@@ -38,6 +39,7 @@ export const Component = observer(function Component({
 }) {
   const pal = usePalette('default')
   const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [query, setQuery] = useState('')
   const autocompleteView = useMemo<UserAutocompleteModel>(
@@ -146,7 +148,7 @@ export const Component = observer(function Component({
           <Button
             testID="doneBtn"
             type="default"
-            onPress={() => store.shell.closeModal()}
+            onPress={() => closeModal()}
             accessibilityLabel="Done"
             accessibilityHint=""
             label="Done"
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 5aaa09e87..c1999c5d6 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -3,13 +3,13 @@ import {StyleSheet} from 'react-native'
 import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
 import {observer} from 'mobx-react-lite'
 import BottomSheet from '@gorhom/bottom-sheet'
-import {useStores} from 'state/index'
 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 import {usePalette} from 'lib/hooks/usePalette'
 import {timeout} from 'lib/async/timeout'
 import {navigate} from '../../../Navigation'
 import once from 'lodash.once'
 
+import {useModals, useModalControls} from '#/state/modals'
 import * as ConfirmModal from './Confirm'
 import * as EditProfileModal from './EditProfile'
 import * as ProfilePreviewModal from './ProfilePreview'
@@ -41,17 +41,17 @@ const DEFAULT_SNAPPOINTS = ['90%']
 const HANDLE_HEIGHT = 24
 
 export const ModalsContainer = observer(function ModalsContainer() {
-  const store = useStores()
+  const {isModalActive, activeModals} = useModals()
+  const {closeModal} = useModalControls()
   const bottomSheetRef = useRef<BottomSheet>(null)
   const pal = usePalette('default')
   const safeAreaInsets = useSafeAreaInsets()
 
-  const activeModal =
-    store.shell.activeModals[store.shell.activeModals.length - 1]
+  const activeModal = activeModals[activeModals.length - 1]
 
   const navigateOnce = once(navigate)
 
-  const onBottomSheetAnimate = (fromIndex: number, toIndex: number) => {
+  const onBottomSheetAnimate = (_fromIndex: number, toIndex: number) => {
     if (activeModal?.name === 'profile-preview' && toIndex === 1) {
       // begin loading the profile screen behind the scenes
       navigateOnce('Profile', {name: activeModal.did})
@@ -59,7 +59,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
   }
   const onBottomSheetChange = async (snapPoint: number) => {
     if (snapPoint === -1) {
-      store.shell.closeModal()
+      closeModal()
     } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) {
       await navigateOnce('Profile', {name: activeModal.did})
       // There is no particular callback for when the view has actually been presented.
@@ -67,21 +67,21 @@ export const ModalsContainer = observer(function ModalsContainer() {
       // It's acceptable because the data is already being fetched + it usually takes longer anyway.
       // TODO: Figure out why avatar/cover don't always show instantly from cache.
       await timeout(200)
-      store.shell.closeModal()
+      closeModal()
     }
   }
   const onClose = () => {
     bottomSheetRef.current?.close()
-    store.shell.closeModal()
+    closeModal()
   }
 
   useEffect(() => {
-    if (store.shell.isModalActive) {
+    if (isModalActive) {
       bottomSheetRef.current?.expand()
     } else {
       bottomSheetRef.current?.close()
     }
-  }, [store.shell.isModalActive, bottomSheetRef, activeModal?.name])
+  }, [isModalActive, bottomSheetRef, activeModal?.name])
 
   let needsSafeTopInset = false
   let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
@@ -184,12 +184,12 @@ export const ModalsContainer = observer(function ModalsContainer() {
       snapPoints={snapPoints}
       topInset={topInset}
       handleHeight={HANDLE_HEIGHT}
-      index={store.shell.isModalActive ? 0 : -1}
+      index={isModalActive ? 0 : -1}
       enablePanDownToClose
       android_keyboardInputMode="adjustResize"
       keyboardBlurBehavior="restore"
       backdropComponent={
-        store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined
+        isModalActive ? createCustomBackdrop(onClose) : undefined
       }
       handleIndicatorStyle={{backgroundColor: pal.text.color}}
       handleStyle={[styles.handle, pal.view]}
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index ede845378..65c4ee444 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -1,11 +1,11 @@
 import React from 'react'
 import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import type {Modal as ModalIface} from 'state/models/ui/shell'
+import type {Modal as ModalIface} from '#/state/modals'
 
+import {useModals, useModalControls} from '#/state/modals'
 import * as ConfirmModal from './Confirm'
 import * as EditProfileModal from './EditProfile'
 import * as ProfilePreviewModal from './ProfilePreview'
@@ -34,15 +34,15 @@ import * as ChangeEmailModal from './ChangeEmail'
 import * as LinkWarningModal from './LinkWarning'
 
 export const ModalsContainer = observer(function ModalsContainer() {
-  const store = useStores()
+  const {isModalActive, activeModals} = useModals()
 
-  if (!store.shell.isModalActive) {
+  if (!isModalActive) {
     return null
   }
 
   return (
     <>
-      {store.shell.activeModals.map((modal, i) => (
+      {activeModals.map((modal, i) => (
         <Modal key={`modal-${i}`} modal={modal} />
       ))}
     </>
@@ -50,11 +50,12 @@ export const ModalsContainer = observer(function ModalsContainer() {
 })
 
 function Modal({modal}: {modal: ModalIface}) {
-  const store = useStores()
+  const {isModalActive} = useModals()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
 
-  if (!store.shell.isModalActive) {
+  if (!isModalActive) {
     return null
   }
 
@@ -62,7 +63,7 @@ function Modal({modal}: {modal: ModalIface}) {
     if (modal.name === 'crop-image' || modal.name === 'edit-image') {
       return // dont close on mask presses during crop
     }
-    store.shell.closeModal()
+    closeModal()
   }
   const onInnerPress = () => {
     // TODO: can we use prevent default?
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
index c01312d69..35ddfe2a1 100644
--- a/src/view/com/modals/ModerationDetails.tsx
+++ b/src/view/com/modals/ModerationDetails.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {ModerationUI} from '@atproto/api'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s} from 'lib/styles'
 import {Text} from '../util/text/Text'
@@ -10,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {listUriToHref} from 'lib/strings/url-helpers'
 import {Button} from '../util/forms/Button'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = [300]
 
@@ -20,7 +20,7 @@ export function Component({
   context: 'account' | 'content'
   moderation: ModerationUI
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
 
@@ -99,10 +99,7 @@ export function Component({
         {description}
       </Text>
       <View style={s.flex1} />
-      <Button
-        type="primary"
-        style={styles.btn}
-        onPress={() => store.shell.closeModal()}>
+      <Button type="primary" style={styles.btn} onPress={() => closeModal()}>
         <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
           Okay
         </Text>
diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx
index b1862ecbd..13728b62b 100644
--- a/src/view/com/modals/Repost.tsx
+++ b/src/view/com/modals/Repost.tsx
@@ -1,12 +1,12 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
 import {s, colors, gradients} from 'lib/styles'
 import {Text} from '../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {RepostIcon} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = [250]
 
@@ -20,10 +20,10 @@ export function Component({
   isReposted: boolean
   // TODO: Add author into component
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {closeModal} = useModalControls()
   const onPress = async () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx
index 820f2895b..242b6a38a 100644
--- a/src/view/com/modals/SelfLabel.tsx
+++ b/src/view/com/modals/SelfLabel.tsx
@@ -2,7 +2,6 @@ import React, {useState} from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -10,6 +9,7 @@ import {isWeb} from 'platform/detection'
 import {Button} from '../util/forms/Button'
 import {SelectableBtn} from '../util/forms/SelectableBtn'
 import {ScrollView} from 'view/com/modals/util'
+import {useModalControls} from '#/state/modals'
 
 const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
 
@@ -25,7 +25,7 @@ export const Component = observer(function Component({
   onChange: (labels: string[]) => void
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const [selected, setSelected] = useState(labels)
 
@@ -143,7 +143,7 @@ export const Component = observer(function Component({
         <TouchableOpacity
           testID="confirmBtn"
           onPress={() => {
-            store.shell.closeModal()
+            closeModal()
           }}
           style={styles.btn}
           accessibilityRole="button"
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 13b21fe22..0f8db30b6 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -6,26 +6,26 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {ScrollView, TextInput} from './util'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
 import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
 export function Component({onSelect}: {onSelect: (url: string) => void}) {
   const theme = useTheme()
   const pal = usePalette('default')
-  const store = useStores()
   const [customUrl, setCustomUrl] = useState<string>('')
+  const {closeModal} = useModalControls()
 
   const doSelect = (url: string) => {
     if (!url.startsWith('http://') && !url.startsWith('https://')) {
       url = `https://${url}`
     }
-    store.shell.closeModal()
+    closeModal()
     onSelect(url)
   }
 
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index aeec2e87f..f86e88439 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -21,6 +21,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
 import isEqual from 'lodash.isequal'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['fullscreen']
 
@@ -36,6 +37,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
   onRemove?: (listUri: string) => void
 }) {
   const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const palPrimary = usePalette('primary')
   const palInverted = usePalette('inverted')
@@ -69,8 +71,8 @@ export const Component = observer(function UserAddRemoveListsImpl({
   }, [memberships, listsList, store, setSelected, setMembershipsLoaded])
 
   const onPressCancel = useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const onPressSave = useCallback(async () => {
     let changes
@@ -87,8 +89,8 @@ export const Component = observer(function UserAddRemoveListsImpl({
     for (const uri of changes.removed) {
       onRemove?.(uri)
     }
-    store.shell.closeModal()
-  }, [store, selected, memberships, onAdd, onRemove])
+    closeModal()
+  }, [closeModal, selected, memberships, onAdd, onRemove])
 
   const onToggleSelected = useCallback(
     (uri: string) => {
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index 9fe8811b0..3adaffb14 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -20,6 +20,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {cleanError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['90%']
 
@@ -43,6 +44,7 @@ export const Component = observer(function Component({
   const [isProcessing, setIsProcessing] = useState<boolean>(false)
   const [error, setError] = useState<string>('')
   const {isMobile} = useWebMediaQueries()
+  const {openModal, closeModal} = useModalControls()
 
   const onSendEmail = async () => {
     setError('')
@@ -67,7 +69,7 @@ export const Component = observer(function Component({
       })
       store.session.updateLocalAccountData({emailConfirmed: true})
       Toast.show('Email verified')
-      store.shell.closeModal()
+      closeModal()
     } catch (e) {
       setError(cleanError(String(e)))
     } finally {
@@ -76,8 +78,8 @@ export const Component = observer(function Component({
   }
 
   const onEmailIncorrect = () => {
-    store.shell.closeModal()
-    store.shell.openModal({name: 'change-email'})
+    closeModal()
+    openModal({name: 'change-email'})
   }
 
   return (
@@ -224,7 +226,7 @@ export const Component = observer(function Component({
               <Button
                 testID="cancelBtn"
                 type="default"
-                onPress={() => store.shell.closeModal()}
+                onPress={() => closeModal()}
                 accessibilityLabel={
                   stage === Stages.Reminder ? 'Not right now' : 'Cancel'
                 }
diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx
index 0fb371fe4..219bdc583 100644
--- a/src/view/com/modals/Waitlist.tsx
+++ b/src/view/com/modals/Waitlist.tsx
@@ -12,19 +12,19 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import LinearGradient from 'react-native-linear-gradient'
 import {Text} from '../util/text/Text'
-import {useStores} from 'state/index'
 import {s, gradients} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {cleanError} from 'lib/strings/errors'
+import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['80%']
 
 export function Component({}: {}) {
   const pal = usePalette('default')
   const theme = useTheme()
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const [email, setEmail] = React.useState<string>('')
   const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
   const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
@@ -54,7 +54,7 @@ export function Component({}: {}) {
     setIsProcessing(false)
   }
   const onCancel = () => {
-    store.shell.closeModal()
+    closeModal()
   }
 
   return (
diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx
index 8e35201d1..c88d002a9 100644
--- a/src/view/com/modals/crop-image/CropImage.web.tsx
+++ b/src/view/com/modals/crop-image/CropImage.web.tsx
@@ -7,10 +7,10 @@ import {Text} from 'view/com/util/text/Text'
 import {Dimensions} from 'lib/media/types'
 import {getDataUriSize} from 'lib/media/util'
 import {s, gradients} from 'lib/styles'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
 import {Image as RNImage} from 'react-native-image-crop-picker'
+import {useModalControls} from '#/state/modals'
 
 enum AspectRatio {
   Square = 'square',
@@ -33,7 +33,7 @@ export function Component({
   uri: string
   onSelect: (img?: RNImage) => void
 }) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
   const [scale, setScale] = React.useState<number>(1)
@@ -43,7 +43,7 @@ export function Component({
 
   const onPressCancel = () => {
     onSelect(undefined)
-    store.shell.closeModal()
+    closeModal()
   }
   const onPressDone = () => {
     const canvas = editorRef.current?.getImageScaledToCanvas()
@@ -59,7 +59,7 @@ export function Component({
     } else {
       onSelect(undefined)
     }
-    store.shell.closeModal()
+    closeModal()
   }
 
   let cropperStyle
diff --git a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
index 659245616..d37d51e47 100644
--- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {ScrollView} from '../util'
-import {useStores} from 'state/index'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -9,6 +8,7 @@ import {deviceLocales} from 'platform/detection'
 import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
 import {LanguageToggle} from './LanguageToggle'
 import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
+import {useModalControls} from '#/state/modals'
 import {
   useLanguagePrefs,
   useSetLanguagePrefs,
@@ -18,14 +18,14 @@ import {
 export const snapPoints = ['100%']
 
 export function Component({}: {}) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useSetLanguagePrefs()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const onPressDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const languages = React.useMemo(() => {
     const langs = LANGUAGES.filter(
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
index 435fb9e1a..4a39da752 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -2,7 +2,6 @@ import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {ScrollView} from '../util'
-import {useStores} from 'state/index'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -10,6 +9,7 @@ import {deviceLocales} from 'platform/detection'
 import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
 import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+import {useModalControls} from '#/state/modals'
 import {
   useLanguagePrefs,
   useSetLanguagePrefs,
@@ -20,14 +20,14 @@ import {
 export const snapPoints = ['100%']
 
 export const Component = observer(function PostLanguagesSettingsImpl() {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useSetLanguagePrefs()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const onPressDone = React.useCallback(() => {
-    store.shell.closeModal()
-  }, [store])
+    closeModal()
+  }, [closeModal])
 
   const languages = React.useMemo(() => {
     const langs = LANGUAGES.filter(
diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx
index 98aa2d471..8dc3f53f7 100644
--- a/src/view/com/modals/report/Modal.tsx
+++ b/src/view/com/modals/report/Modal.tsx
@@ -14,6 +14,7 @@ import {SendReportButton} from './SendReportButton'
 import {InputIssueDetails} from './InputIssueDetails'
 import {ReportReasonOptions} from './ReasonOptions'
 import {CollectionId} from './types'
+import {useModalControls} from '#/state/modals'
 
 const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
 
@@ -37,6 +38,7 @@ type ReportComponentProps =
 
 export function Component(content: ReportComponentProps) {
   const store = useStores()
+  const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const [isProcessing, setIsProcessing] = useState(false)
@@ -60,7 +62,7 @@ export function Component(content: ReportComponentProps) {
     try {
       if (issue === '__copyright__') {
         Linking.openURL(DMCA_LINK)
-        store.shell.closeModal()
+        closeModal()
         return
       }
       const $type = !isAccountReport
@@ -76,7 +78,7 @@ export function Component(content: ReportComponentProps) {
       })
       Toast.show("Thank you for your report! We'll look into it promptly.")
 
-      store.shell.closeModal()
+      closeModal()
       return
     } catch (e: any) {
       setError(cleanError(e))
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index 9e75d9507..84e438fcd 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -11,6 +11,7 @@ import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useStores} from 'state/index'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 const MESSAGES = {
   [KnownError.Unknown]: '',
@@ -57,13 +58,14 @@ function FeedgenErrorMessage({
   const msg = MESSAGES[knownError]
   const uri = (feed.params as GetCustomFeed.QueryParams).feed
   const [ownerDid] = safeParseFeedgenUri(uri)
+  const {openModal, closeModal} = useModalControls()
 
   const onViewProfile = React.useCallback(() => {
     navigation.navigate('Profile', {name: ownerDid})
   }, [navigation, ownerDid])
 
   const onRemoveFeed = React.useCallback(async () => {
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
       title: 'Remove feed',
       message: 'Remove this feed from your saved feeds?',
@@ -78,10 +80,10 @@ function FeedgenErrorMessage({
         }
       },
       onPressCancel() {
-        store.shell.closeModal()
+        closeModal()
       },
     })
-  }, [store, uri])
+  }, [store, openModal, closeModal, uri])
 
   return (
     <View
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 1a1d38e4b..1ee209785 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -40,6 +40,7 @@ import {makeProfileLink} from 'lib/routes/links'
 import {Link} from '../util/Link'
 import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
 
 interface Props {
   view: ProfileModel
@@ -113,6 +114,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
   const store = useStores()
+  const {openModal} = useModalControls()
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
   const invalidHandle = isInvalidHandle(view.handle)
@@ -157,12 +159,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 
   const onPressEditProfile = React.useCallback(() => {
     track('ProfileHeader:EditProfileButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'edit-profile',
       profileView: view,
       onUpdate: onRefreshAll,
     })
-  }, [track, store, view, onRefreshAll])
+  }, [track, openModal, view, onRefreshAll])
 
   const trackPress = React.useCallback(
     (f: 'Followers' | 'Follows') => {
@@ -181,12 +183,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 
   const onPressAddRemoveLists = React.useCallback(() => {
     track('ProfileHeader:AddToListsButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'user-add-remove-lists',
       subject: view.did,
       displayName: view.displayName || view.handle,
     })
-  }, [track, view, store])
+  }, [track, view, openModal])
 
   const onPressMuteAccount = React.useCallback(async () => {
     track('ProfileHeader:MuteAccountButtonClicked')
@@ -212,7 +214,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 
   const onPressBlockAccount = React.useCallback(async () => {
     track('ProfileHeader:BlockAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
       title: 'Block Account',
       message:
@@ -228,11 +230,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
         }
       },
     })
-  }, [track, view, store, onRefreshAll])
+  }, [track, view, openModal, onRefreshAll])
 
   const onPressUnblockAccount = React.useCallback(async () => {
     track('ProfileHeader:UnblockAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
       title: 'Unblock Account',
       message:
@@ -248,15 +250,15 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
         }
       },
     })
-  }, [track, view, store, onRefreshAll])
+  }, [track, view, openModal, onRefreshAll])
 
   const onPressReportAccount = React.useCallback(() => {
     track('ProfileHeader:ReportAccountButtonClicked')
-    store.shell.openModal({
+    openModal({
       name: 'report',
       did: view.did,
     })
-  }, [track, store, view])
+  }, [track, openModal, view])
 
   const isMe = React.useMemo(
     () => store.me.did === view.did,
diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx
index db9b6b4bf..2f36609e9 100644
--- a/src/view/com/testing/TestCtrls.e2e.tsx
+++ b/src/view/com/testing/TestCtrls.e2e.tsx
@@ -2,6 +2,7 @@ import React from 'react'
 import {Pressable, View} from 'react-native'
 import {useStores} from 'state/index'
 import {navigate} from '../../../Navigation'
+import {useModalControls} from '#/state/modals'
 
 /**
  * This utility component is only included in the test simulator
@@ -13,6 +14,7 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
 
 export function TestCtrls() {
   const store = useStores()
+  const {openModal} = useModalControls()
   const onPressSignInAlice = async () => {
     await store.session.login({
       service: 'http://localhost:3000',
@@ -85,7 +87,7 @@ export function TestCtrls() {
       />
       <Pressable
         testID="e2eOpenInviteCodesModal"
-        onPress={() => store.shell.openModal({name: 'invite-codes'})}
+        onPress={() => openModal({name: 'invite-codes'})}
         accessibilityRole="button"
         style={BTN}
       />
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 1777f6659..074ab2329 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -21,7 +21,6 @@ import {Text} from './text/Text'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {NavigationProp} from 'lib/routes/types'
 import {router} from '../../../routes'
-import {useStores, RootStoreModel} from 'state/index'
 import {
   convertBskyAppUrlIfNeeded,
   isExternalUrl,
@@ -31,6 +30,7 @@ import {isAndroid, isWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
 import {PressableWithHover} from './PressableWithHover'
 import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
+import {useModalControls} from '#/state/modals'
 
 type Event =
   | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -60,17 +60,17 @@ export const Link = memo(function Link({
   anchorNoUnderline,
   ...props
 }: Props) {
-  const store = useStores()
+  const {closeModal} = useModalControls()
   const navigation = useNavigation<NavigationProp>()
   const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
 
   const onPress = React.useCallback(
     (e?: Event) => {
       if (typeof href === 'string') {
-        return onPressInner(store, navigation, sanitizeUrl(href), e)
+        return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
       }
     },
-    [store, navigation, href],
+    [closeModal, navigation, href],
   )
 
   if (noFeedback) {
@@ -160,8 +160,8 @@ export const TextLink = memo(function TextLink({
   warnOnMismatchingLabel?: boolean
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
-  const store = useStores()
   const navigation = useNavigation<NavigationProp>()
+  const {openModal, closeModal} = useModalControls()
 
   if (warnOnMismatchingLabel && typeof text !== 'string') {
     console.error('Unable to detect mismatching label')
@@ -174,7 +174,7 @@ export const TextLink = memo(function TextLink({
         linkRequiresWarning(href, typeof text === 'string' ? text : '')
       if (requiresWarning) {
         e?.preventDefault?.()
-        store.shell.openModal({
+        openModal({
           name: 'link-warning',
           text: typeof text === 'string' ? text : '',
           href,
@@ -185,9 +185,17 @@ export const TextLink = memo(function TextLink({
         // @ts-ignore function signature differs by platform -prf
         return onPress()
       }
-      return onPressInner(store, navigation, sanitizeUrl(href), e)
+      return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
     },
-    [onPress, store, navigation, href, text, warnOnMismatchingLabel],
+    [
+      onPress,
+      closeModal,
+      openModal,
+      navigation,
+      href,
+      text,
+      warnOnMismatchingLabel,
+    ],
   )
   const hrefAttrs = useMemo(() => {
     const isExternal = isExternalUrl(href)
@@ -285,7 +293,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
 // needed customizations
 // -prf
 function onPressInner(
-  store: RootStoreModel,
+  closeModal = () => {},
   navigation: NavigationProp,
   href: string,
   e?: Event,
@@ -318,7 +326,7 @@ function onPressInner(
     if (newTab || href.startsWith('http') || href.startsWith('mailto')) {
       Linking.openURL(href)
     } else {
-      store.shell.closeModal() // close any active modals
+      closeModal() // close any active modals
 
       // @ts-ignore we're not able to type check on this one -prf
       navigation.dispatch(StackActions.push(...router.matchPath(href)))
diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx
index f43f9e80b..9c5efe55e 100644
--- a/src/view/com/util/UserPreviewLink.tsx
+++ b/src/view/com/util/UserPreviewLink.tsx
@@ -1,9 +1,9 @@
 import React from 'react'
 import {Pressable, StyleProp, ViewStyle} from 'react-native'
-import {useStores} from 'state/index'
 import {Link} from './Link'
 import {isWeb} from 'platform/detection'
 import {makeProfileLink} from 'lib/routes/links'
+import {useModalControls} from '#/state/modals'
 
 interface UserPreviewLinkProps {
   did: string
@@ -13,7 +13,7 @@ interface UserPreviewLinkProps {
 export function UserPreviewLink(
   props: React.PropsWithChildren<UserPreviewLinkProps>,
 ) {
-  const store = useStores()
+  const {openModal} = useModalControls()
 
   if (isWeb) {
     return (
@@ -29,7 +29,7 @@ export function UserPreviewLink(
   return (
     <Pressable
       onPress={() =>
-        store.shell.openModal({
+        openModal({
           name: 'profile-preview',
           did: props.did,
         })
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 1fffa3123..45abed647 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -2,7 +2,6 @@ import React from 'react'
 import {StyleProp, View, ViewStyle} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {toShareUrl} from 'lib/strings/url-helpers'
-import {useStores} from 'state/index'
 import {useTheme} from 'lib/ThemeContext'
 import {shareUrl} from 'lib/sharing'
 import {
@@ -10,6 +9,7 @@ import {
   DropdownItem as NativeDropdownItem,
 } from './NativeDropdown'
 import {EventStopper} from '../EventStopper'
+import {useModalControls} from '#/state/modals'
 
 export function PostDropdownBtn({
   testID,
@@ -37,9 +37,9 @@ export function PostDropdownBtn({
   onDeletePost: () => void
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const theme = useTheme()
   const defaultCtrlColor = theme.palette.default.postCtrl
+  const {openModal} = useModalControls()
 
   const dropdownItems: NativeDropdownItem[] = [
     {
@@ -108,7 +108,7 @@ export function PostDropdownBtn({
     !isAuthor && {
       label: 'Report post',
       onPress() {
-        store.shell.openModal({
+        openModal({
           name: 'report',
           uri: itemUri,
           cid: itemCid,
@@ -129,7 +129,7 @@ export function PostDropdownBtn({
     isAuthor && {
       label: 'Delete post',
       onPress() {
-        store.shell.openModal({
+        openModal({
           name: 'confirm',
           title: 'Delete this post?',
           message: 'Are you sure? This can not be undone.',
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index 4f917844a..b6fe0dd8c 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -6,7 +6,7 @@ import {ModerationUI} from '@atproto/api'
 import {Text} from '../text/Text'
 import {ShieldExclamation} from 'lib/icons'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {useModalControls} from '#/state/modals'
 
 export function ContentHider({
   testID,
@@ -22,10 +22,10 @@ export function ContentHider({
   style?: StyleProp<ViewStyle>
   childContainerStyle?: StyleProp<ViewStyle>
 }>) {
-  const store = useStores()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
+  const {openModal} = useModalControls()
 
   if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
     return (
@@ -43,7 +43,7 @@ export function ContentHider({
           if (!moderation.noOverride) {
             setOverride(v => !v)
           } else {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
@@ -62,7 +62,7 @@ export function ContentHider({
         ]}>
         <Pressable
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx
index 0dba367fc..2c9a71859 100644
--- a/src/view/com/util/moderation/PostAlerts.tsx
+++ b/src/view/com/util/moderation/PostAlerts.tsx
@@ -5,7 +5,7 @@ import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ShieldExclamation} from 'lib/icons'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {useModalControls} from '#/state/modals'
 
 export function PostAlerts({
   moderation,
@@ -15,8 +15,8 @@ export function PostAlerts({
   includeMute?: boolean
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {openModal} = useModalControls()
 
   const shouldAlert = !!moderation.cause && moderation.alert
   if (!shouldAlert) {
@@ -27,7 +27,7 @@ export function PostAlerts({
   return (
     <Pressable
       onPress={() => {
-        store.shell.openModal({
+        openModal({
           name: 'moderation-details',
           context: 'content',
           moderation,
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index d224286b0..a9ccf2ebd 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -8,7 +8,7 @@ import {Text} from '../text/Text'
 import {addStyle} from 'lib/styles'
 import {describeModerationCause} from 'lib/moderation'
 import {ShieldExclamation} from 'lib/icons'
-import {useStores} from 'state/index'
+import {useModalControls} from '#/state/modals'
 
 interface Props extends ComponentProps<typeof Link> {
   // testID?: string
@@ -25,10 +25,10 @@ export function PostHider({
   children,
   ...props
 }: Props) {
-  const store = useStores()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
+  const {openModal} = useModalControls()
 
   if (!moderation.blur) {
     return (
@@ -63,7 +63,7 @@ export function PostHider({
         ]}>
         <Pressable
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'content',
               moderation,
diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
index 6b7f4e7ec..d2406e7ae 100644
--- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
+++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
@@ -8,7 +8,7 @@ import {
   describeModerationCause,
   getProfileModerationCauses,
 } from 'lib/moderation'
-import {useStores} from 'state/index'
+import {useModalControls} from '#/state/modals'
 
 export function ProfileHeaderAlerts({
   moderation,
@@ -17,8 +17,8 @@ export function ProfileHeaderAlerts({
   moderation: ProfileModeration
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const {openModal} = useModalControls()
 
   const causes = getProfileModerationCauses(moderation)
   if (!causes.length) {
@@ -34,7 +34,7 @@ export function ProfileHeaderAlerts({
             testID="profileHeaderAlert"
             key={desc.name}
             onPress={() => {
-              store.shell.openModal({
+              openModal({
                 name: 'moderation-details',
                 context: 'content',
                 moderation: {cause},
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
index 0224b9fee..c3d23b84d 100644
--- a/src/view/com/util/moderation/ScreenHider.tsx
+++ b/src/view/com/util/moderation/ScreenHider.tsx
@@ -18,7 +18,7 @@ import {NavigationProp} from 'lib/routes/types'
 import {Text} from '../text/Text'
 import {Button} from '../forms/Button'
 import {describeModerationCause} from 'lib/moderation'
-import {useStores} from 'state/index'
+import {useModalControls} from '#/state/modals'
 
 export function ScreenHider({
   testID,
@@ -34,12 +34,12 @@ export function ScreenHider({
   style?: StyleProp<ViewStyle>
   containerStyle?: StyleProp<ViewStyle>
 }>) {
-  const store = useStores()
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
   const [override, setOverride] = React.useState(false)
   const navigation = useNavigation<NavigationProp>()
   const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
 
   if (!moderation.blur || override) {
     return (
@@ -72,7 +72,7 @@ export function ScreenHider({
         .{' '}
         <TouchableWithoutFeedback
           onPress={() => {
-            store.shell.openModal({
+            openModal({
               name: 'moderation-details',
               context: 'account',
               moderation,
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 5769a478b..7bcea0e79 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -16,6 +16,7 @@ import {useStores} from 'state/index'
 import {RepostButton} from './RepostButton'
 import {Haptics} from 'lib/haptics'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
+import {useModalControls} from '#/state/modals'
 
 interface PostCtrlsOpts {
   itemUri: string
@@ -51,6 +52,7 @@ interface PostCtrlsOpts {
 export function PostCtrls(opts: PostCtrlsOpts) {
   const store = useStores()
   const theme = useTheme()
+  const {closeModal} = useModalControls()
   const defaultCtrlColor = React.useMemo(
     () => ({
       color: theme.palette.default.postCtrl,
@@ -58,17 +60,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
     [theme],
   ) as StyleProp<ViewStyle>
   const onRepost = useCallback(() => {
-    store.shell.closeModal()
+    closeModal()
     if (!opts.isReposted) {
       Haptics.default()
       opts.onPressToggleRepost().catch(_e => undefined)
     } else {
       opts.onPressToggleRepost().catch(_e => undefined)
     }
-  }, [opts, store.shell])
+  }, [opts, closeModal])
 
   const onQuote = useCallback(() => {
-    store.shell.closeModal()
+    closeModal()
     store.shell.openComposer({
       quote: {
         uri: opts.itemUri,
@@ -86,6 +88,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
     opts.itemUri,
     opts.text,
     store.shell,
+    closeModal,
   ])
 
   const onPressToggleLikeWrapper = async () => {
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 9c4ed8e5d..0a7637252 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -5,8 +5,8 @@ import {s, colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
 import {pluralize} from 'lib/strings/helpers'
-import {useStores} from 'state/index'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
+import {useModalControls} from '#/state/modals'
 
 interface Props {
   isReposted: boolean
@@ -23,8 +23,8 @@ export const RepostButton = ({
   onRepost,
   onQuote,
 }: Props) => {
-  const store = useStores()
   const theme = useTheme()
+  const {openModal} = useModalControls()
 
   const defaultControlColor = React.useMemo(
     () => ({
@@ -34,13 +34,13 @@ export const RepostButton = ({
   )
 
   const onPressToggleRepostWrapper = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'repost',
       onRepost: onRepost,
       onQuote: onQuote,
       isReposted,
     })
-  }, [onRepost, onQuote, isReposted, store.shell])
+  }, [onRepost, onQuote, isReposted, openModal])
 
   return (
     <TouchableOpacity