about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx37
-rw-r--r--src/App.web.tsx37
-rw-r--r--src/lib/analytics/analytics.tsx18
-rw-r--r--src/lib/api/index.ts28
-rw-r--r--src/lib/async/revertible.ts68
-rw-r--r--src/lib/link-meta/bsky.ts13
-rw-r--r--src/lib/link-meta/link-meta.ts12
-rw-r--r--src/lib/media/image-sizes.ts34
-rw-r--r--src/lib/strings/url-helpers.ts2
-rw-r--r--src/state/index.ts54
-rw-r--r--src/state/models/cache/handle-resolutions.ts5
-rw-r--r--src/state/models/cache/image-sizes.ts38
-rw-r--r--src/state/models/cache/link-metas.ts44
-rw-r--r--src/state/models/cache/posts.ts70
-rw-r--r--src/state/models/cache/profiles-view.ts50
-rw-r--r--src/state/models/me.ts115
-rw-r--r--src/state/models/root-store.ts207
-rw-r--r--src/state/models/session.ts43
-rw-r--r--src/state/models/ui/shell.ts32
-rw-r--r--src/state/shell/reminders.e2e.ts4
-rw-r--r--src/view/com/auth/LoggedOut.tsx5
-rw-r--r--src/view/com/auth/Onboarding.tsx5
-rw-r--r--src/view/com/auth/create/Policies.tsx4
-rw-r--r--src/view/com/auth/create/Step1.tsx2
-rw-r--r--src/view/com/auth/login/ForgotPasswordForm.tsx4
-rw-r--r--src/view/com/auth/login/Login.tsx54
-rw-r--r--src/view/com/auth/login/LoginForm.tsx4
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeeds.tsx7
-rw-r--r--src/view/com/auth/onboarding/RecommendedFeedsItem.tsx5
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollows.tsx7
-rw-r--r--src/view/com/auth/onboarding/WelcomeDesktop.tsx7
-rw-r--r--src/view/com/auth/onboarding/WelcomeMobile.tsx8
-rw-r--r--src/view/com/auth/withAuthRequired.tsx5
-rw-r--r--src/view/com/composer/labels/LabelsBtn.tsx5
-rw-r--r--src/view/com/composer/select-language/SelectLangBtn.tsx5
-rw-r--r--src/view/com/composer/text-input/mobile/Autocomplete.tsx5
-rw-r--r--src/view/com/composer/useExternalLinkFetch.ts12
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx1
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx1
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx5
-rw-r--r--src/view/com/modals/ChangeEmail.tsx5
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx145
-rw-r--r--src/view/com/modals/InviteCodes.tsx5
-rw-r--r--src/view/com/modals/LinkWarning.tsx11
-rw-r--r--src/view/com/modals/Modal.tsx5
-rw-r--r--src/view/com/modals/Modal.web.tsx5
-rw-r--r--src/view/com/modals/SelfLabel.tsx5
-rw-r--r--src/view/com/modals/ServerInput.tsx2
-rw-r--r--src/view/com/modals/VerifyEmail.tsx9
-rw-r--r--src/view/com/modals/lang-settings/LanguageToggle.tsx5
-rw-r--r--src/view/com/modals/lang-settings/PostLanguagesSettings.tsx5
-rw-r--r--src/view/com/modals/report/Modal.tsx6
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx9
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx5
-rw-r--r--src/view/com/posts/FeedSlice.tsx5
-rw-r--r--src/view/com/profile/ProfileCard.tsx5
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx7
-rw-r--r--src/view/com/util/PostMeta.tsx5
-rw-r--r--src/view/com/util/SimpleViewHeader.tsx5
-rw-r--r--src/view/com/util/ViewHeader.tsx9
-rw-r--r--src/view/com/util/fab/FABInner.tsx9
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx9
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.tsx5
-rw-r--r--src/view/com/util/post-embeds/ListEmbed.tsx5
-rw-r--r--src/view/screens/Home.tsx231
-rw-r--r--src/view/screens/LanguageSettings.tsx7
-rw-r--r--src/view/screens/Log.tsx5
-rw-r--r--src/view/screens/Moderation.tsx5
-rw-r--r--src/view/screens/PostThread.tsx143
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx7
-rw-r--r--src/view/screens/PreferencesThreads.tsx7
-rw-r--r--src/view/screens/ProfileFeed.tsx11
-rw-r--r--src/view/screens/SavedFeeds.tsx5
-rw-r--r--src/view/screens/Settings.tsx1163
-rw-r--r--src/view/shell/Drawer.tsx13
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx7
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx5
-rw-r--r--src/view/shell/desktop/Feeds.tsx5
-rw-r--r--src/view/shell/desktop/LeftNav.tsx19
-rw-r--r--src/view/shell/desktop/RightNav.tsx9
-rw-r--r--src/view/shell/index.web.tsx5
81 files changed, 1069 insertions, 1926 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 3f49eb11f..af4c3be89 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -5,7 +5,6 @@ import React, {useState, useEffect} from 'react'
 import {RootSiblingParent} from 'react-native-root-siblings'
 import * as SplashScreen from 'expo-splash-screen'
 import {GestureHandlerRootView} from 'react-native-gesture-handler'
-import {observer} from 'mobx-react-lite'
 import {QueryClientProvider} from '@tanstack/react-query'
 
 import 'view/icons'
@@ -16,7 +15,6 @@ import {listenSessionDropped} from './state/events'
 import {useColorMode} from 'state/shell'
 import {ThemeProvider} from 'lib/ThemeContext'
 import {s} from 'lib/styles'
-import {RootStoreModel, setupState, RootStoreProvider} from './state'
 import {Shell} from 'view/shell'
 import * as notifications from 'lib/notifications/notifications'
 import * as analytics from 'lib/analytics/analytics'
@@ -44,22 +42,13 @@ i18n.activate('en')
 
 SplashScreen.preventAutoHideAsync()
 
-const InnerApp = observer(function AppImpl() {
+function InnerApp() {
   const colorMode = useColorMode()
   const {isInitialLoad} = useSession()
   const {resumeSession} = useSessionApi()
-  const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
-    undefined,
-  )
 
   // init
   useEffect(() => {
-    setupState().then(store => {
-      setRootStore(store)
-    })
-  }, [])
-
-  useEffect(() => {
     initReminders()
     analytics.init()
     notifications.init(queryClient)
@@ -72,7 +61,7 @@ const InnerApp = observer(function AppImpl() {
   }, [resumeSession])
 
   // show nothing prior to init
-  if (!rootStore || isInitialLoad) {
+  if (isInitialLoad) {
     // TODO add a loading state
     return null
   }
@@ -85,22 +74,20 @@ const InnerApp = observer(function AppImpl() {
     <UnreadNotifsProvider>
       <ThemeProvider theme={colorMode}>
         <analytics.Provider>
-          <RootStoreProvider value={rootStore}>
-            <I18nProvider i18n={i18n}>
-              {/* All components should be within this provider */}
-              <RootSiblingParent>
-                <GestureHandlerRootView style={s.h100pct}>
-                  <TestCtrls />
-                  <Shell />
-                </GestureHandlerRootView>
-              </RootSiblingParent>
-            </I18nProvider>
-          </RootStoreProvider>
+          <I18nProvider i18n={i18n}>
+            {/* All components should be within this provider */}
+            <RootSiblingParent>
+              <GestureHandlerRootView style={s.h100pct}>
+                <TestCtrls />
+                <Shell />
+              </GestureHandlerRootView>
+            </RootSiblingParent>
+          </I18nProvider>
         </analytics.Provider>
       </ThemeProvider>
     </UnreadNotifsProvider>
   )
-})
+}
 
 function App() {
   const [isReady, setReady] = useState(false)
diff --git a/src/App.web.tsx b/src/App.web.tsx
index e1f4e8030..67fa3dcc3 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,7 +1,6 @@
 import 'lib/sentry' // must be near top
 
 import React, {useState, useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
 import {QueryClientProvider} from '@tanstack/react-query'
 import {SafeAreaProvider} from 'react-native-safe-area-context'
 import {RootSiblingParent} from 'react-native-root-siblings'
@@ -12,7 +11,6 @@ import {init as initPersistedState} from '#/state/persisted'
 import {init as initReminders} from '#/state/shell/reminders'
 import {useColorMode} from 'state/shell'
 import * as analytics from 'lib/analytics/analytics'
-import {RootStoreModel, setupState, RootStoreProvider} from './state'
 import {Shell} from 'view/shell/index'
 import {ToastContainer} from 'view/com/util/Toast.web'
 import {ThemeProvider} from 'lib/ThemeContext'
@@ -34,22 +32,13 @@ import {
 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
 import * as persisted from '#/state/persisted'
 
-const InnerApp = observer(function AppImpl() {
+function InnerApp() {
   const {isInitialLoad} = useSession()
   const {resumeSession} = useSessionApi()
   const colorMode = useColorMode()
-  const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
-    undefined,
-  )
 
   // init
   useEffect(() => {
-    setupState().then(store => {
-      setRootStore(store)
-    })
-  }, [])
-
-  useEffect(() => {
     initReminders()
     analytics.init()
     dynamicActivate(defaultLocale) // async import of locale data
@@ -59,7 +48,7 @@ const InnerApp = observer(function AppImpl() {
   }, [resumeSession])
 
   // show nothing prior to init
-  if (!rootStore || isInitialLoad) {
+  if (isInitialLoad) {
     // TODO add a loading state
     return null
   }
@@ -72,22 +61,20 @@ const InnerApp = observer(function AppImpl() {
     <UnreadNotifsProvider>
       <ThemeProvider theme={colorMode}>
         <analytics.Provider>
-          <RootStoreProvider value={rootStore}>
-            <I18nProvider i18n={i18n}>
-              {/* All components should be within this provider */}
-              <RootSiblingParent>
-                <SafeAreaProvider>
-                  <Shell />
-                </SafeAreaProvider>
-              </RootSiblingParent>
-            </I18nProvider>
-            <ToastContainer />
-          </RootStoreProvider>
+          <I18nProvider i18n={i18n}>
+            {/* All components should be within this provider */}
+            <RootSiblingParent>
+              <SafeAreaProvider>
+                <Shell />
+              </SafeAreaProvider>
+            </RootSiblingParent>
+          </I18nProvider>
+          <ToastContainer />
         </analytics.Provider>
       </ThemeProvider>
     </UnreadNotifsProvider>
   )
-})
+}
 
 function App() {
   const [isReady, setReady] = useState(false)
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx
index 4b955b365..3a8254eb1 100644
--- a/src/lib/analytics/analytics.tsx
+++ b/src/lib/analytics/analytics.tsx
@@ -7,13 +7,21 @@ import {
   useAnalytics as useAnalyticsOrig,
   ClientMethods,
 } from '@segment/analytics-react-native'
-import {AppInfo} from 'state/models/root-store'
+import {z} from 'zod'
 import {useSession} from '#/state/session'
 import {sha256} from 'js-sha256'
 import {ScreenEvent, TrackEvent} from './types'
 import {logger} from '#/logger'
 import {listenSessionLoaded} from '#/state/events'
 
+export const appInfo = z.object({
+  build: z.string().optional(),
+  name: z.string().optional(),
+  namespace: z.string().optional(),
+  version: z.string().optional(),
+})
+export type AppInfo = z.infer<typeof appInfo>
+
 const segmentClient = createClient({
   writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
   trackAppLifecycleEvents: false,
@@ -128,7 +136,11 @@ async function writeAppInfo(value: AppInfo) {
   await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
 }
 
-async function readAppInfo(): Promise<Partial<AppInfo> | undefined> {
+async function readAppInfo(): Promise<AppInfo | undefined> {
   const rawData = await AsyncStorage.getItem('BSKY_APP_INFO')
-  return rawData ? JSON.parse(rawData) : undefined
+  const obj = rawData ? JSON.parse(rawData) : undefined
+  if (!obj || typeof obj !== 'object') {
+    return undefined
+  }
+  return obj
 }
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 92620c459..a78abcacd 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -10,7 +10,6 @@ import {
   RichText,
 } from '@atproto/api'
 import {AtUri} from '@atproto/api'
-import {RootStoreModel} from 'state/models/root-store'
 import {isNetworkError} from 'lib/strings/errors'
 import {LinkMeta} from '../link-meta/link-meta'
 import {isWeb} from 'platform/detection'
@@ -26,33 +25,6 @@ export interface ExternalEmbedDraft {
   localThumb?: ImageModel
 }
 
-export async function resolveName(store: RootStoreModel, didOrHandle: string) {
-  if (!didOrHandle) {
-    throw new Error('Invalid handle: ""')
-  }
-  if (didOrHandle.startsWith('did:')) {
-    return didOrHandle
-  }
-
-  // we run the resolution always to ensure freshness
-  const promise = store.agent
-    .resolveHandle({
-      handle: didOrHandle,
-    })
-    .then(res => {
-      store.handleResolutions.cache.set(didOrHandle, res.data.did)
-      return res.data.did
-    })
-
-  // but we can return immediately if it's cached
-  const cached = store.handleResolutions.cache.get(didOrHandle)
-  if (cached) {
-    return cached
-  }
-
-  return promise
-}
-
 export async function uploadBlob(
   agent: BskyAgent,
   blob: string,
diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts
deleted file mode 100644
index 43383b61e..000000000
--- a/src/lib/async/revertible.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import {runInAction} from 'mobx'
-import {deepObserve} from 'mobx-utils'
-import set from 'lodash.set'
-
-const ongoingActions = new Set<any>()
-
-/**
- * This is a TypeScript function that optimistically updates data on the client-side before sending a
- * request to the server and rolling back changes if the request fails.
- * @param {T} model - The object or record that needs to be updated optimistically.
- * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It
- * can be used to perform any necessary actions or updates on the model or UI before the server update
- * is initiated.
- * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server
- * update operation. This function is called after the previous state of the model has been recorded
- * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate`
- * function is called with the result
- * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the
- * server update is successful. It takes in the response from the server update as its parameter. If
- * this parameter is not provided, nothing will happen after the server update.
- * @returns A Promise that resolves to `void`.
- */
-export const updateDataOptimistically = async <
-  T extends Record<string, any>,
-  U,
->(
-  model: T,
-  preUpdate: () => void,
-  serverUpdate: () => Promise<U>,
-  postUpdate?: (res: U) => void,
-): Promise<void> => {
-  if (ongoingActions.has(model)) {
-    return
-  }
-  ongoingActions.add(model)
-
-  const prevState: Map<string, any> = new Map<string, any>()
-  const dispose = deepObserve(model, (change, path) => {
-    if (change.observableKind === 'object') {
-      if (change.type === 'update') {
-        prevState.set(
-          [path, change.name].filter(Boolean).join('.'),
-          change.oldValue,
-        )
-      } else if (change.type === 'add') {
-        prevState.set([path, change.name].filter(Boolean).join('.'), undefined)
-      }
-    }
-  })
-  preUpdate()
-  dispose()
-
-  try {
-    const res = await serverUpdate()
-    runInAction(() => {
-      postUpdate?.(res)
-    })
-  } catch (error) {
-    runInAction(() => {
-      prevState.forEach((value, path) => {
-        set(model, path, value)
-      })
-    })
-    throw error
-  } finally {
-    ongoingActions.delete(model)
-  }
-}
diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts
index 78d755331..322b02332 100644
--- a/src/lib/link-meta/bsky.ts
+++ b/src/lib/link-meta/bsky.ts
@@ -1,9 +1,8 @@
-import {AppBskyFeedPost} from '@atproto/api'
+import {AppBskyFeedPost, BskyAgent} from '@atproto/api'
 import * as apilib from 'lib/api/index'
 import {LikelyType, LinkMeta} from './link-meta'
 // import {match as matchRoute} from 'view/routes'
 import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
-import {RootStoreModel} from 'state/index'
 import {ComposerOptsQuote} from 'state/shell/composer'
 import {useGetPost} from '#/state/queries/post'
 
@@ -23,7 +22,7 @@ import {useGetPost} from '#/state/queries/post'
 // remove once that's implemented
 // -prf
 export async function extractBskyMeta(
-  store: RootStoreModel,
+  agent: BskyAgent,
   url: string,
 ): Promise<LinkMeta> {
   url = convertBskyAppUrlIfNeeded(url)
@@ -120,13 +119,13 @@ export async function getPostAsQuote(
 }
 
 export async function getFeedAsEmbed(
-  store: RootStoreModel,
+  agent: BskyAgent,
   url: string,
 ): Promise<apilib.ExternalEmbedDraft> {
   url = convertBskyAppUrlIfNeeded(url)
   const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
   const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey)
-  const res = await store.agent.app.bsky.feed.getFeedGenerator({feed})
+  const res = await agent.app.bsky.feed.getFeedGenerator({feed})
   return {
     isLoading: false,
     uri: feed,
@@ -146,13 +145,13 @@ export async function getFeedAsEmbed(
 }
 
 export async function getListAsEmbed(
-  store: RootStoreModel,
+  agent: BskyAgent,
   url: string,
 ): Promise<apilib.ExternalEmbedDraft> {
   url = convertBskyAppUrlIfNeeded(url)
   const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
   const list = makeRecordUri(user, 'app.bsky.graph.list', rkey)
-  const res = await store.agent.app.bsky.graph.getList({list})
+  const res = await agent.app.bsky.graph.getList({list})
   return {
     isLoading: false,
     uri: list,
diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts
index c490fa292..c17dee51f 100644
--- a/src/lib/link-meta/link-meta.ts
+++ b/src/lib/link-meta/link-meta.ts
@@ -1,5 +1,5 @@
+import {BskyAgent} from '@atproto/api'
 import {isBskyAppUrl} from '../strings/url-helpers'
-import {RootStoreModel} from 'state/index'
 import {extractBskyMeta} from './bsky'
 import {LINK_META_PROXY} from 'lib/constants'
 
@@ -23,12 +23,12 @@ export interface LinkMeta {
 }
 
 export async function getLinkMeta(
-  store: RootStoreModel,
+  agent: BskyAgent,
   url: string,
   timeout = 5e3,
 ): Promise<LinkMeta> {
   if (isBskyAppUrl(url)) {
-    return extractBskyMeta(store, url)
+    return extractBskyMeta(agent, url)
   }
 
   let urlp
@@ -55,9 +55,9 @@ export async function getLinkMeta(
     const to = setTimeout(() => controller.abort(), timeout || 5e3)
 
     const response = await fetch(
-      `${LINK_META_PROXY(
-        store.session.currentSession?.service || '',
-      )}${encodeURIComponent(url)}`,
+      `${LINK_META_PROXY(agent.service.toString() || '')}${encodeURIComponent(
+        url,
+      )}`,
       {signal: controller.signal},
     )
 
diff --git a/src/lib/media/image-sizes.ts b/src/lib/media/image-sizes.ts
new file mode 100644
index 000000000..4ea95ea23
--- /dev/null
+++ b/src/lib/media/image-sizes.ts
@@ -0,0 +1,34 @@
+import {Image} from 'react-native'
+import type {Dimensions} from 'lib/media/types'
+
+const sizes: Map<string, Dimensions> = new Map()
+const activeRequests: Map<string, Promise<Dimensions>> = new Map()
+
+export function get(uri: string): Dimensions | undefined {
+  return sizes.get(uri)
+}
+
+export async function fetch(uri: string): Promise<Dimensions> {
+  const Dimensions = sizes.get(uri)
+  if (Dimensions) {
+    return Dimensions
+  }
+
+  const prom =
+    activeRequests.get(uri) ||
+    new Promise<Dimensions>(resolve => {
+      Image.getSize(
+        uri,
+        (width: number, height: number) => resolve({width, height}),
+        (err: any) => {
+          console.error('Failed to fetch image dimensions for', uri, err)
+          resolve({width: 0, height: 0})
+        },
+      )
+    })
+  activeRequests.set(uri, prom)
+  const res = await prom
+  activeRequests.delete(uri)
+  sizes.set(uri, res)
+  return res
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index 106d2ca31..d06bd8028 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -1,5 +1,5 @@
 import {AtUri} from '@atproto/api'
-import {PROD_SERVICE} from 'state/index'
+import {PROD_SERVICE} from 'lib/constants'
 import TLDs from 'tlds'
 import psl from 'psl'
 
diff --git a/src/state/index.ts b/src/state/index.ts
deleted file mode 100644
index 55dcae6d6..000000000
--- a/src/state/index.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import {autorun} from 'mobx'
-import {AppState, Platform} from 'react-native'
-import {BskyAgent} from '@atproto/api'
-import {RootStoreModel} from './models/root-store'
-import * as apiPolyfill from 'lib/api/api-polyfill'
-import * as storage from 'lib/storage'
-import {logger} from '#/logger'
-
-export const LOCAL_DEV_SERVICE =
-  Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
-export const STAGING_SERVICE = 'https://staging.bsky.dev'
-export const PROD_SERVICE = 'https://bsky.social'
-export const DEFAULT_SERVICE = PROD_SERVICE
-const ROOT_STATE_STORAGE_KEY = 'root'
-const STATE_FETCH_INTERVAL = 15e3
-
-export async function setupState(serviceUri = DEFAULT_SERVICE) {
-  let rootStore: RootStoreModel
-  let data: any
-
-  apiPolyfill.doPolyfill()
-
-  rootStore = new RootStoreModel(new BskyAgent({service: serviceUri}))
-  try {
-    data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
-    logger.debug('Initial hydrate', {hasSession: !!data.session})
-    rootStore.hydrate(data)
-  } catch (e: any) {
-    logger.error('Failed to load state from storage', {error: e})
-  }
-  rootStore.attemptSessionResumption()
-
-  // track changes & save to storage
-  autorun(() => {
-    const snapshot = rootStore.serialize()
-    storage.save(ROOT_STATE_STORAGE_KEY, snapshot)
-  })
-
-  // periodic state fetch
-  setInterval(() => {
-    // NOTE
-    // this must ONLY occur when the app is active, as the bg-fetch handler
-    // will wake up the thread and cause this interval to fire, which in
-    // turn schedules a bunch of work at a poor time
-    // -prf
-    if (AppState.currentState === 'active') {
-      rootStore.updateSessionState()
-    }
-  }, STATE_FETCH_INTERVAL)
-
-  return rootStore
-}
-
-export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
diff --git a/src/state/models/cache/handle-resolutions.ts b/src/state/models/cache/handle-resolutions.ts
deleted file mode 100644
index 2e2b69661..000000000
--- a/src/state/models/cache/handle-resolutions.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {LRUMap} from 'lru_map'
-
-export class HandleResolutionsCache {
-  cache: LRUMap<string, string> = new LRUMap(500)
-}
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
deleted file mode 100644
index c30a68f4d..000000000
--- a/src/state/models/cache/image-sizes.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {Image} from 'react-native'
-import type {Dimensions} from 'lib/media/types'
-
-export class ImageSizesCache {
-  sizes: Map<string, Dimensions> = new Map()
-  activeRequests: Map<string, Promise<Dimensions>> = new Map()
-
-  constructor() {}
-
-  get(uri: string): Dimensions | undefined {
-    return this.sizes.get(uri)
-  }
-
-  async fetch(uri: string): Promise<Dimensions> {
-    const Dimensions = this.sizes.get(uri)
-    if (Dimensions) {
-      return Dimensions
-    }
-
-    const prom =
-      this.activeRequests.get(uri) ||
-      new Promise<Dimensions>(resolve => {
-        Image.getSize(
-          uri,
-          (width: number, height: number) => resolve({width, height}),
-          (err: any) => {
-            console.error('Failed to fetch image dimensions for', uri, err)
-            resolve({width: 0, height: 0})
-          },
-        )
-      })
-    this.activeRequests.set(uri, prom)
-    const res = await prom
-    this.activeRequests.delete(uri)
-    this.sizes.set(uri, res)
-    return res
-  }
-}
diff --git a/src/state/models/cache/link-metas.ts b/src/state/models/cache/link-metas.ts
deleted file mode 100644
index 607968c80..000000000
--- a/src/state/models/cache/link-metas.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta'
-
-type CacheValue = Promise<LinkMeta> | LinkMeta
-export class LinkMetasCache {
-  cache: LRUMap<string, CacheValue> = new LRUMap(100)
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        cache: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // public api
-  // =
-
-  async getLinkMeta(url: string) {
-    const cached = this.cache.get(url)
-    if (cached) {
-      try {
-        return await cached
-      } catch (e) {
-        // ignore, we'll try again
-      }
-    }
-    try {
-      const promise = getLinkMeta(this.rootStore, url)
-      this.cache.set(url, promise)
-      const res = await promise
-      this.cache.set(url, res)
-      return res
-    } catch (e) {
-      this.cache.delete(url)
-      throw e
-    }
-  }
-}
diff --git a/src/state/models/cache/posts.ts b/src/state/models/cache/posts.ts
deleted file mode 100644
index d3632f436..000000000
--- a/src/state/models/cache/posts.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {
-  AppBskyFeedDefs,
-  AppBskyEmbedRecord,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyFeedPost,
-} from '@atproto/api'
-
-type PostView = AppBskyFeedDefs.PostView
-
-export class PostsCache {
-  cache: LRUMap<string, PostView> = new LRUMap(500)
-
-  constructor(public rootStore: RootStoreModel) {}
-
-  set(uri: string, postView: PostView) {
-    this.cache.set(uri, postView)
-    if (postView.author.handle) {
-      this.rootStore.handleResolutions.cache.set(
-        postView.author.handle,
-        postView.author.did,
-      )
-    }
-  }
-
-  fromFeedItem(feedItem: AppBskyFeedDefs.FeedViewPost) {
-    this.set(feedItem.post.uri, feedItem.post)
-    if (
-      feedItem.reply?.parent &&
-      AppBskyFeedDefs.isPostView(feedItem.reply?.parent)
-    ) {
-      this.set(feedItem.reply.parent.uri, feedItem.reply.parent)
-    }
-    const embed = feedItem.post.embed
-    if (
-      AppBskyEmbedRecord.isView(embed) &&
-      AppBskyEmbedRecord.isViewRecord(embed.record) &&
-      AppBskyFeedPost.isRecord(embed.record.value) &&
-      AppBskyFeedPost.validateRecord(embed.record.value).success
-    ) {
-      this.set(embed.record.uri, embedViewToPostView(embed.record))
-    }
-    if (
-      AppBskyEmbedRecordWithMedia.isView(embed) &&
-      AppBskyEmbedRecord.isViewRecord(embed.record?.record) &&
-      AppBskyFeedPost.isRecord(embed.record.record.value) &&
-      AppBskyFeedPost.validateRecord(embed.record.record.value).success
-    ) {
-      this.set(
-        embed.record.record.uri,
-        embedViewToPostView(embed.record.record),
-      )
-    }
-  }
-}
-
-function embedViewToPostView(
-  embedView: AppBskyEmbedRecord.ViewRecord,
-): PostView {
-  return {
-    $type: 'app.bsky.feed.post#view',
-    uri: embedView.uri,
-    cid: embedView.cid,
-    author: embedView.author,
-    record: embedView.value,
-    indexedAt: embedView.indexedAt,
-    labels: embedView.labels,
-  }
-}
diff --git a/src/state/models/cache/profiles-view.ts b/src/state/models/cache/profiles-view.ts
deleted file mode 100644
index e5a9be587..000000000
--- a/src/state/models/cache/profiles-view.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
-
-type CacheValue = Promise<GetProfile.Response> | GetProfile.Response
-export class ProfilesCache {
-  cache: LRUMap<string, CacheValue> = new LRUMap(100)
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        cache: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // public api
-  // =
-
-  async getProfile(did: string) {
-    const cached = this.cache.get(did)
-    if (cached) {
-      try {
-        return await cached
-      } catch (e) {
-        // ignore, we'll try again
-      }
-    }
-    try {
-      const promise = this.rootStore.agent.getProfile({
-        actor: did,
-      })
-      this.cache.set(did, promise)
-      const res = await promise
-      this.cache.set(did, res)
-      return res
-    } catch (e) {
-      this.cache.delete(did)
-      throw e
-    }
-  }
-
-  overwrite(did: string, res: GetProfile.Response) {
-    this.cache.set(did, res)
-  }
-}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
deleted file mode 100644
index 1e802fb78..000000000
--- a/src/state/models/me.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {RootStoreModel} from './root-store'
-import {isObj, hasProp} from 'lib/type-guards'
-import {logger} from '#/logger'
-
-const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min
-
-export class MeModel {
-  did: string = ''
-  handle: string = ''
-  displayName: string = ''
-  description: string = ''
-  avatar: string = ''
-  followsCount: number | undefined
-  followersCount: number | undefined
-  lastProfileStateUpdate = Date.now()
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {rootStore: false, serialize: false, hydrate: false},
-      {autoBind: true},
-    )
-  }
-
-  clear() {
-    this.rootStore.profiles.cache.clear()
-    this.rootStore.posts.cache.clear()
-    this.did = ''
-    this.handle = ''
-    this.displayName = ''
-    this.description = ''
-    this.avatar = ''
-  }
-
-  serialize(): unknown {
-    return {
-      did: this.did,
-      handle: this.handle,
-      displayName: this.displayName,
-      description: this.description,
-      avatar: this.avatar,
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      let did, handle, displayName, description, avatar
-      if (hasProp(v, 'did') && typeof v.did === 'string') {
-        did = v.did
-      }
-      if (hasProp(v, 'handle') && typeof v.handle === 'string') {
-        handle = v.handle
-      }
-      if (hasProp(v, 'displayName') && typeof v.displayName === 'string') {
-        displayName = v.displayName
-      }
-      if (hasProp(v, 'description') && typeof v.description === 'string') {
-        description = v.description
-      }
-      if (hasProp(v, 'avatar') && typeof v.avatar === 'string') {
-        avatar = v.avatar
-      }
-      if (did && handle) {
-        this.did = did
-        this.handle = handle
-        this.displayName = displayName || ''
-        this.description = description || ''
-        this.avatar = avatar || ''
-      }
-    }
-  }
-
-  async load() {
-    const sess = this.rootStore.session
-    logger.debug('MeModel:load', {hasSession: sess.hasSession})
-    if (sess.hasSession) {
-      this.did = sess.currentSession?.did || ''
-      await this.fetchProfile()
-      this.rootStore.emitSessionLoaded()
-    } else {
-      this.clear()
-    }
-  }
-
-  async updateIfNeeded() {
-    if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) {
-      logger.debug('Updating me profile information')
-      this.lastProfileStateUpdate = Date.now()
-      await this.fetchProfile()
-    }
-  }
-
-  async fetchProfile() {
-    const profile = await this.rootStore.agent.getProfile({
-      actor: this.did,
-    })
-    runInAction(() => {
-      if (profile?.data) {
-        this.displayName = profile.data.displayName || ''
-        this.description = profile.data.description || ''
-        this.avatar = profile.data.avatar || ''
-        this.handle = profile.data.handle || ''
-        this.followsCount = profile.data.followsCount
-        this.followersCount = profile.data.followersCount
-      } else {
-        this.displayName = ''
-        this.description = ''
-        this.avatar = ''
-        this.followsCount = profile.data.followsCount
-        this.followersCount = undefined
-      }
-    })
-  }
-}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
deleted file mode 100644
index 6d7c2c12e..000000000
--- a/src/state/models/root-store.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * The root store is the base of all modeled state.
- */
-
-import {makeAutoObservable} from 'mobx'
-import {BskyAgent} from '@atproto/api'
-import {createContext, useContext} from 'react'
-import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
-import {z} from 'zod'
-import {isObj, hasProp} from 'lib/type-guards'
-import {SessionModel} from './session'
-import {ShellUiModel} from './ui/shell'
-import {HandleResolutionsCache} from './cache/handle-resolutions'
-import {ProfilesCache} from './cache/profiles-view'
-import {PostsCache} from './cache/posts'
-import {LinkMetasCache} from './cache/link-metas'
-import {MeModel} from './me'
-import {resetToTab} from '../../Navigation'
-import {ImageSizesCache} from './cache/image-sizes'
-import {reset as resetNavigation} from '../../Navigation'
-import {logger} from '#/logger'
-
-// TEMPORARY (APP-700)
-// remove after backend testing finishes
-// -prf
-import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
-
-export const appInfo = z.object({
-  build: z.string(),
-  name: z.string(),
-  namespace: z.string(),
-  version: z.string(),
-})
-export type AppInfo = z.infer<typeof appInfo>
-
-export class RootStoreModel {
-  agent: BskyAgent
-  appInfo?: AppInfo
-  session = new SessionModel(this)
-  shell = new ShellUiModel(this)
-  me = new MeModel(this)
-  handleResolutions = new HandleResolutionsCache()
-  profiles = new ProfilesCache(this)
-  posts = new PostsCache(this)
-  linkMetas = new LinkMetasCache(this)
-  imageSizes = new ImageSizesCache()
-
-  constructor(agent: BskyAgent) {
-    this.agent = agent
-    makeAutoObservable(this, {
-      agent: false,
-      serialize: false,
-      hydrate: false,
-    })
-  }
-
-  setAppInfo(info: AppInfo) {
-    this.appInfo = info
-  }
-
-  serialize(): unknown {
-    return {
-      appInfo: this.appInfo,
-      me: this.me.serialize(),
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      if (hasProp(v, 'appInfo')) {
-        const appInfoParsed = appInfo.safeParse(v.appInfo)
-        if (appInfoParsed.success) {
-          this.setAppInfo(appInfoParsed.data)
-        }
-      }
-      if (hasProp(v, 'me')) {
-        this.me.hydrate(v.me)
-      }
-    }
-  }
-
-  /**
-   * Called during init to resume any stored session.
-   */
-  async attemptSessionResumption() {}
-
-  /**
-   * Called by the session model. Refreshes session-oriented state.
-   */
-  async handleSessionChange(
-    agent: BskyAgent,
-    {hadSession}: {hadSession: boolean},
-  ) {
-    logger.debug('RootStoreModel:handleSessionChange')
-    this.agent = agent
-    applyDebugHeader(this.agent)
-    this.me.clear()
-    await this.me.load()
-    if (!hadSession) {
-      await resetNavigation()
-    }
-    this.emitSessionReady()
-  }
-
-  /**
-   * Called by the session model. Handles session drops by informing the user.
-   */
-  async handleSessionDrop() {
-    logger.debug('RootStoreModel:handleSessionDrop')
-    resetToTab('HomeTab')
-    this.me.clear()
-    this.emitSessionDropped()
-  }
-
-  /**
-   * Clears all session-oriented state, previously called on LOGOUT
-   */
-  clearAllSessionState() {
-    logger.debug('RootStoreModel:clearAllSessionState')
-    resetToTab('HomeTab')
-    this.me.clear()
-  }
-
-  /**
-   * Periodic poll for new session state.
-   */
-  async updateSessionState() {
-    if (!this.session.hasSession) {
-      return
-    }
-    try {
-      await this.me.updateIfNeeded()
-    } catch (e: any) {
-      logger.error('Failed to fetch latest state', {error: e})
-    }
-  }
-
-  // global event bus
-  // =
-  // - some events need to be passed around between views and models
-  //   in order to keep state in sync; these methods are for that
-
-  // a post was deleted by the local user
-  onPostDeleted(handler: (uri: string) => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('post-deleted', handler)
-  }
-  emitPostDeleted(uri: string) {
-    DeviceEventEmitter.emit('post-deleted', uri)
-  }
-
-  // a list was deleted by the local user
-  onListDeleted(handler: (uri: string) => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('list-deleted', handler)
-  }
-  emitListDeleted(uri: string) {
-    DeviceEventEmitter.emit('list-deleted', uri)
-  }
-
-  // the session has started and been fully hydrated
-  onSessionLoaded(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-loaded', handler)
-  }
-  emitSessionLoaded() {
-    DeviceEventEmitter.emit('session-loaded')
-  }
-
-  // the session has completed all setup; good for post-initialization behaviors like triggering modals
-  onSessionReady(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-ready', handler)
-  }
-  emitSessionReady() {
-    DeviceEventEmitter.emit('session-ready')
-  }
-
-  // the session was dropped due to bad/expired refresh tokens
-  onSessionDropped(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-dropped', handler)
-  }
-  emitSessionDropped() {
-    DeviceEventEmitter.emit('session-dropped')
-  }
-
-  // the current screen has changed
-  // TODO is this still needed?
-  onNavigation(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('navigation', handler)
-  }
-  emitNavigation() {
-    DeviceEventEmitter.emit('navigation')
-  }
-
-  // a "soft reset" typically means scrolling to top and loading latest
-  // but it can depend on the screen
-  onScreenSoftReset(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('screen-soft-reset', handler)
-  }
-  emitScreenSoftReset() {
-    DeviceEventEmitter.emit('screen-soft-reset')
-  }
-}
-
-const throwawayInst = new RootStoreModel(
-  new BskyAgent({service: 'http://localhost'}),
-) // this will be replaced by the loader, we just need to supply a value at init
-const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
-export const RootStoreProvider = RootStoreContext.Provider
-export const useStores = () => useContext(RootStoreContext)
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
deleted file mode 100644
index cc681ad34..000000000
--- a/src/state/models/session.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  BskyAgent,
-  ComAtprotoServerDescribeServer as DescribeServer,
-} from '@atproto/api'
-import {RootStoreModel} from './root-store'
-
-export type ServiceDescription = DescribeServer.OutputSchema
-
-export class SessionModel {
-  data: any = {}
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {
-      rootStore: false,
-      hasSession: false,
-    })
-  }
-
-  get currentSession(): any {
-    return undefined
-  }
-
-  get hasSession() {
-    return false
-  }
-
-  clear() {}
-
-  /**
-   * Helper to fetch the accounts config settings from an account.
-   */
-  async describeService(service: string): Promise<ServiceDescription> {
-    const agent = new BskyAgent({service})
-    const res = await agent.com.atproto.server.describeServer({})
-    return res.data
-  }
-
-  /**
-   * Reloads the session from the server. Useful when account details change, like the handle.
-   */
-  async reloadFromServer() {}
-}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
deleted file mode 100644
index 18287c953..000000000
--- a/src/state/models/ui/shell.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {RootStoreModel} from '../root-store'
-import {makeAutoObservable} from 'mobx'
-import {
-  shouldRequestEmailConfirmation,
-  setEmailConfirmationRequested,
-} from '#/state/shell/reminders'
-import {unstable__openModal} from '#/state/modals'
-
-export type ColorMode = 'system' | 'light' | 'dark'
-
-export function isColorMode(v: unknown): v is ColorMode {
-  return v === 'system' || v === 'light' || v === 'dark'
-}
-
-export class ShellUiModel {
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {
-      rootStore: false,
-    })
-
-    this.setupLoginModals()
-  }
-
-  setupLoginModals() {
-    this.rootStore.onSessionReady(() => {
-      if (shouldRequestEmailConfirmation(this.rootStore.session)) {
-        unstable__openModal({name: 'verify-email', showReminder: true})
-        setEmailConfirmationRequested()
-      }
-    })
-  }
-}
diff --git a/src/state/shell/reminders.e2e.ts b/src/state/shell/reminders.e2e.ts
index d7703e9e0..4bd11bf0a 100644
--- a/src/state/shell/reminders.e2e.ts
+++ b/src/state/shell/reminders.e2e.ts
@@ -1,6 +1,4 @@
-import {SessionModel} from '../models/session'
-
-export function shouldRequestEmailConfirmation(_session: SessionModel) {
+export function shouldRequestEmailConfirmation() {
   return false
 }
 
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 0d8172964..3505e86af 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {SafeAreaView} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Login} from 'view/com/auth/login/Login'
 import {CreateAccount} from 'view/com/auth/create/CreateAccount'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -16,7 +15,7 @@ enum ScreenState {
   S_CreateAccount,
 }
 
-export const LoggedOut = observer(function LoggedOutImpl() {
+export function LoggedOut() {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
@@ -58,4 +57,4 @@ export const LoggedOut = observer(function LoggedOutImpl() {
       </ErrorBoundary>
     </SafeAreaView>
   )
-})
+}
diff --git a/src/view/com/auth/Onboarding.tsx b/src/view/com/auth/Onboarding.tsx
index 994f4c149..3a17eb4f8 100644
--- a/src/view/com/auth/Onboarding.tsx
+++ b/src/view/com/auth/Onboarding.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {SafeAreaView} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -10,7 +9,7 @@ import {RecommendedFollows} from './onboarding/RecommendedFollows'
 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
 import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
 
-export const Onboarding = observer(function OnboardingImpl() {
+export function Onboarding() {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
   const onboardingState = useOnboardingState()
@@ -38,4 +37,4 @@ export const Onboarding = observer(function OnboardingImpl() {
       </ErrorBoundary>
     </SafeAreaView>
   )
-})
+}
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
index 7d10f32fc..a52f07531 100644
--- a/src/view/com/auth/create/Policies.tsx
+++ b/src/view/com/auth/create/Policies.tsx
@@ -4,12 +4,14 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
 import {TextLink} from '../../util/Link'
 import {Text} from '../../util/text/Text'
 import {s, colors} from 'lib/styles'
-import {ServiceDescription} from 'state/models/session'
 import {usePalette} from 'lib/hooks/usePalette'
 
+type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
 export const Policies = ({
   serviceDescription,
   needsGuardian,
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
index ab47b411f..c9d19e868 100644
--- a/src/view/com/auth/create/Step1.tsx
+++ b/src/view/com/auth/create/Step1.tsx
@@ -13,7 +13,7 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
+import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
 
 /** STEP 1: Your hosting provider
diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx
index a794665c9..215c393d9 100644
--- a/src/view/com/auth/login/ForgotPasswordForm.tsx
+++ b/src/view/com/auth/login/ForgotPasswordForm.tsx
@@ -9,13 +9,13 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
 import * as EmailValidator from 'email-validator'
 import {BskyAgent} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
-import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
@@ -26,6 +26,8 @@ import {useLingui} from '@lingui/react'
 import {styles} from './styles'
 import {useModalControls} from '#/state/modals'
 
+type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
 export const ForgotPasswordForm = ({
   error,
   serviceUrl,
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index 27d08812c..a2f924f95 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -2,8 +2,7 @@ import React, {useState, useEffect} from 'react'
 import {KeyboardAvoidingView} from 'react-native'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
-import {useStores, DEFAULT_SERVICE} from 'state/index'
-import {ServiceDescription} from 'state/models/session'
+import {DEFAULT_SERVICE} from '#/lib/constants'
 import {usePalette} from 'lib/hooks/usePalette'
 import {logger} from '#/logger'
 import {ChooseAccountForm} from './ChooseAccountForm'
@@ -14,6 +13,7 @@ import {PasswordUpdatedForm} from './PasswordUpdatedForm'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useSession, SessionAccount} from '#/state/session'
+import {useServiceQuery} from '#/state/queries/service'
 
 enum Forms {
   Login,
@@ -25,20 +25,20 @@ enum Forms {
 
 export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   const pal = usePalette('default')
-  const store = useStores()
   const {accounts} = useSession()
   const {track} = useAnalytics()
   const {_} = useLingui()
   const [error, setError] = useState<string>('')
-  const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
   const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
-  const [serviceDescription, setServiceDescription] = useState<
-    ServiceDescription | undefined
-  >(undefined)
   const [initialHandle, setInitialHandle] = useState<string>('')
   const [currentForm, setCurrentForm] = useState<Forms>(
     accounts.length ? Forms.ChooseAccount : Forms.Login,
   )
+  const {
+    data: serviceDescription,
+    error: serviceError,
+    refetch: refetchService,
+  } = useServiceQuery(serviceUrl)
 
   const onSelectAccount = (account?: SessionAccount) => {
     if (account?.service) {
@@ -54,35 +54,21 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
   }
 
   useEffect(() => {
-    let aborted = false
-    setError('')
-    store.session.describeService(serviceUrl).then(
-      desc => {
-        if (aborted) {
-          return
-        }
-        setServiceDescription(desc)
-      },
-      err => {
-        if (aborted) {
-          return
-        }
-        logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
-          error: err,
-        })
-        setError(
-          _(
-            msg`Unable to contact your service. Please check your Internet connection.`,
-          ),
-        )
-      },
-    )
-    return () => {
-      aborted = true
+    if (serviceError) {
+      setError(
+        _(
+          msg`Unable to contact your service. Please check your Internet connection.`,
+        ),
+      )
+      logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
+        error: String(serviceError),
+      })
+    } else {
+      setError('')
     }
-  }, [store.session, serviceUrl, retryDescribeTrigger, _])
+  }, [serviceError, serviceUrl, _])
 
-  const onPressRetryConnect = () => setRetryDescribeTrigger({})
+  const onPressRetryConnect = () => refetchService()
   const onPressForgotPassword = () => {
     track('Signin:PressedForgotPassword')
     setCurrentForm(Forms.ForgotPassword)
diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx
index be3a95131..365f2e253 100644
--- a/src/view/com/auth/login/LoginForm.tsx
+++ b/src/view/com/auth/login/LoginForm.tsx
@@ -10,12 +10,12 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {s} from 'lib/styles'
 import {createFullHandle} from 'lib/strings/handles'
 import {toNiceDomain} from 'lib/strings/url-helpers'
-import {ServiceDescription} from 'state/models/session'
 import {isNetworkError} from 'lib/strings/errors'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
@@ -27,6 +27,8 @@ import {styles} from './styles'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
 
+type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
+
 export const LoginForm = ({
   error,
   serviceUrl,
diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
index d134dae9f..941671bf8 100644
--- a/src/view/com/auth/onboarding/RecommendedFeeds.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints'
 import {Text} from 'view/com/util/text/Text'
@@ -16,9 +15,7 @@ import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds'
 type Props = {
   next: () => void
 }
-export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
-  next,
-}: Props) {
+export function RecommendedFeeds({next}: Props) {
   const pal = usePalette('default')
   const {isTabletOrMobile} = useWebMediaQueries()
   const {isLoading, data} = useSuggestedFeedsQuery()
@@ -146,7 +143,7 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({
       </Mobile>
     </>
   )
-})
+}
 
 const tdStyles = StyleSheet.create({
   container: {
diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
index 2eaf3cf2d..7417e5b06 100644
--- a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api'
 import {Text} from 'view/com/util/text/Text'
@@ -19,7 +18,7 @@ import {
 } from '#/state/queries/preferences'
 import {logger} from '#/logger'
 
-export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
+export function RecommendedFeedsItem({
   item,
 }: {
   item: AppBskyFeedDefs.GeneratorView
@@ -164,4 +163,4 @@ export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({
       </View>
     </View>
   )
-})
+}
diff --git a/src/view/com/auth/onboarding/RecommendedFollows.tsx b/src/view/com/auth/onboarding/RecommendedFollows.tsx
index aa528cbd8..c327a7168 100644
--- a/src/view/com/auth/onboarding/RecommendedFollows.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollows.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
 import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints'
@@ -19,9 +18,7 @@ import {logger} from '#/logger'
 type Props = {
   next: () => void
 }
-export const RecommendedFollows = observer(function RecommendedFollowsImpl({
-  next,
-}: Props) {
+export function RecommendedFollows({next}: Props) {
   const pal = usePalette('default')
   const {isTabletOrMobile} = useWebMediaQueries()
   const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery()
@@ -211,7 +208,7 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
       </Mobile>
     </>
   )
-})
+}
 
 const tdStyles = StyleSheet.create({
   container: {
diff --git a/src/view/com/auth/onboarding/WelcomeDesktop.tsx b/src/view/com/auth/onboarding/WelcomeDesktop.tsx
index c066e9bd5..1a30c17f9 100644
--- a/src/view/com/auth/onboarding/WelcomeDesktop.tsx
+++ b/src/view/com/auth/onboarding/WelcomeDesktop.tsx
@@ -7,16 +7,13 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
 import {Button} from 'view/com/util/forms/Button'
-import {observer} from 'mobx-react-lite'
 
 type Props = {
   next: () => void
   skip: () => void
 }
 
-export const WelcomeDesktop = observer(function WelcomeDesktopImpl({
-  next,
-}: Props) {
+export function WelcomeDesktop({next}: Props) {
   const pal = usePalette('default')
   const horizontal = useMediaQuery({minWidth: 1300})
   const title = (
@@ -105,7 +102,7 @@ export const WelcomeDesktop = observer(function WelcomeDesktopImpl({
       </View>
     </TitleColumnLayout>
   )
-})
+}
 
 const styles = StyleSheet.create({
   row: {
diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx
index ef70a1fe3..2717bff3f 100644
--- a/src/view/com/auth/onboarding/WelcomeMobile.tsx
+++ b/src/view/com/auth/onboarding/WelcomeMobile.tsx
@@ -5,7 +5,6 @@ import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Button} from 'view/com/util/forms/Button'
-import {observer} from 'mobx-react-lite'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {Trans} from '@lingui/macro'
 
@@ -14,10 +13,7 @@ type Props = {
   skip: () => void
 }
 
-export const WelcomeMobile = observer(function WelcomeMobileImpl({
-  next,
-  skip,
-}: Props) {
+export function WelcomeMobile({next, skip}: Props) {
   const pal = usePalette('default')
 
   return (
@@ -102,7 +98,7 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
       />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 4b8b31d6c..5fd89a8bd 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -5,7 +5,6 @@ import {
   StyleSheet,
   TouchableOpacity,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {CenteredView} from '../util/Views'
 import {LoggedOut} from './LoggedOut'
 import {Onboarding} from './Onboarding'
@@ -18,7 +17,7 @@ import {useSession} from '#/state/session'
 export const withAuthRequired = <P extends object>(
   Component: React.ComponentType<P>,
 ): React.FC<P> =>
-  observer(function AuthRequired(props: P) {
+  function AuthRequired(props: P) {
     const {isInitialLoad, hasSession} = useSession()
     const onboardingState = useOnboardingState()
     if (isInitialLoad) {
@@ -31,7 +30,7 @@ export const withAuthRequired = <P extends object>(
       return <Onboarding />
     }
     return <Component {...props} />
-  })
+  }
 
 function Loading() {
   const pal = usePalette('default')
diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx
index 7d90774f2..a10684691 100644
--- a/src/view/com/composer/labels/LabelsBtn.tsx
+++ b/src/view/com/composer/labels/LabelsBtn.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 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 {ShieldExclamation} from 'lib/icons'
@@ -11,7 +10,7 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
 
-export const LabelsBtn = observer(function LabelsBtn({
+export function LabelsBtn({
   labels,
   hasMedia,
   onChange,
@@ -49,7 +48,7 @@ export const LabelsBtn = observer(function LabelsBtn({
       ) : null}
     </Button>
   )
-})
+}
 
 const styles = StyleSheet.create({
   button: {
diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx
index 7ea63ed62..78b1e9ba2 100644
--- a/src/view/com/composer/select-language/SelectLangBtn.tsx
+++ b/src/view/com/composer/select-language/SelectLangBtn.tsx
@@ -1,6 +1,5 @@
 import React, {useCallback, useMemo} from 'react'
 import {StyleSheet, Keyboard} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -24,7 +23,7 @@ import {
 import {t, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-export const SelectLangBtn = observer(function SelectLangBtn() {
+export function SelectLangBtn() {
   const pal = usePalette('default')
   const {_} = useLingui()
   const {openModal} = useModalControls()
@@ -117,7 +116,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
       )}
     </DropdownButton>
   )
-})
+}
 
 const styles = StyleSheet.create({
   button: {
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index bb54a2042..c400aa48d 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -1,6 +1,5 @@
 import React, {useEffect, useRef} from 'react'
 import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Text} from 'view/com/util/text/Text'
@@ -10,7 +9,7 @@ import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
 import {Trans} from '@lingui/macro'
 import {AppBskyActorDefs} from '@atproto/api'
 
-export const Autocomplete = observer(function AutocompleteImpl({
+export function Autocomplete({
   prefix,
   onSelect,
 }: {
@@ -103,7 +102,7 @@ export const Autocomplete = observer(function AutocompleteImpl({
       ) : null}
     </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
index 0c96e4727..af6042306 100644
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ b/src/view/com/composer/useExternalLinkFetch.ts
@@ -1,5 +1,4 @@
 import {useState, useEffect} from 'react'
-import {useStores} from 'state/index'
 import {ImageModel} from 'state/models/media/image'
 import * as apilib from 'lib/api/index'
 import {getLinkMeta} from 'lib/link-meta/link-meta'
@@ -17,6 +16,7 @@ import {
 import {ComposerOpts} from 'state/shell/composer'
 import {POST_IMG_MAX} from 'lib/constants'
 import {logger} from '#/logger'
+import {useSession} from '#/state/session'
 import {useGetPost} from '#/state/queries/post'
 
 export function useExternalLinkFetch({
@@ -24,7 +24,7 @@ export function useExternalLinkFetch({
 }: {
   setQuote: (opts: ComposerOpts['quote']) => void
 }) {
-  const store = useStores()
+  const {agent} = useSession()
   const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
     undefined,
   )
@@ -56,7 +56,7 @@ export function useExternalLinkFetch({
           },
         )
       } else if (isBskyCustomFeedUrl(extLink.uri)) {
-        getFeedAsEmbed(store, extLink.uri).then(
+        getFeedAsEmbed(agent, extLink.uri).then(
           ({embed, meta}) => {
             if (aborted) {
               return
@@ -74,7 +74,7 @@ export function useExternalLinkFetch({
           },
         )
       } else if (isBskyListUrl(extLink.uri)) {
-        getListAsEmbed(store, extLink.uri).then(
+        getListAsEmbed(agent, extLink.uri).then(
           ({embed, meta}) => {
             if (aborted) {
               return
@@ -92,7 +92,7 @@ export function useExternalLinkFetch({
           },
         )
       } else {
-        getLinkMeta(store, extLink.uri).then(meta => {
+        getLinkMeta(agent, extLink.uri).then(meta => {
           if (aborted) {
             return
           }
@@ -134,7 +134,7 @@ export function useExternalLinkFetch({
       })
     }
     return cleanup
-  }, [store, extLink, setQuote, getPost])
+  }, [agent, extLink, setQuote, getPost])
 
   return {extLink, setExtLink}
 }
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
index 7c7ad0616..ea740ec91 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -315,7 +315,6 @@ const ImageItem = ({
       <GestureDetector gesture={composedGesture}>
         <AnimatedImage
           contentFit="contain"
-          // NOTE: Don't pass imageSrc={imageSrc} or MobX will break.
           source={{uri: imageSrc.uri}}
           style={[styles.image, animatedStyle]}
           accessibilityLabel={imageSrc.alt}
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index f73f355ac..2b0b0b149 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -139,7 +139,6 @@ const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => {
         {(!loaded || !imageDimensions) && <ImageLoading />}
         <AnimatedImage
           contentFit="contain"
-          // NOTE: Don't pass imageSrc={imageSrc} or MobX will break.
           source={{uri: imageSrc.uri}}
           style={[styles.image, animatedStyle]}
           accessibilityLabel={imageSrc.alt}
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
index 9996c5641..c78f06ed4 100644
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ b/src/view/com/modals/BirthDateSettings.tsx
@@ -5,7 +5,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
 import {DateInput} from '../util/forms/DateInput'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -103,7 +102,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
   )
 }
 
-export const Component = observer(function Component({}: {}) {
+export function Component({}: {}) {
   const {data: preferences} = usePreferencesQuery()
 
   return !preferences ? (
@@ -111,7 +110,7 @@ export const Component = observer(function Component({}: {}) {
   ) : (
     <Inner preferences={preferences} />
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 6f7a92102..829188b51 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -1,7 +1,6 @@
 import React, {useState} from 'react'
 import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
 import {ScrollView, TextInput} from './util'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
 import {Button} from '../util/forms/Button'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -24,7 +23,7 @@ enum Stages {
 
 export const snapPoints = ['90%']
 
-export const Component = observer(function Component({}: {}) {
+export function Component() {
   const pal = usePalette('default')
   const {agent, currentAccount} = useSession()
   const {updateCurrentAccount} = useSessionApi()
@@ -226,7 +225,7 @@ export const Component = observer(function Component({}: {}) {
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   titleSection: {
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 8dc3311fc..8b42e1b1d 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -2,7 +2,6 @@ import React from 'react'
 import {LabelPreference} from '@atproto/api'
 import {StyleSheet, Pressable, View} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
-import {observer} from 'mobx-react-lite'
 import {ScrollView} from './util'
 import {s, colors, gradients} from 'lib/styles'
 import {Text} from '../util/text/Text'
@@ -28,82 +27,80 @@ import {
 
 export const snapPoints = ['90%']
 
-export const Component = observer(
-  function ContentFilteringSettingsImpl({}: {}) {
-    const {isMobile} = useWebMediaQueries()
-    const pal = usePalette('default')
-    const {_} = useLingui()
-    const {closeModal} = useModalControls()
-    const {data: preferences} = usePreferencesQuery()
+export function Component({}: {}) {
+  const {isMobile} = useWebMediaQueries()
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
+  const {data: preferences} = usePreferencesQuery()
 
-    const onPressDone = React.useCallback(() => {
-      closeModal()
-    }, [closeModal])
+  const onPressDone = React.useCallback(() => {
+    closeModal()
+  }, [closeModal])
 
-    return (
-      <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
-        <Text style={[pal.text, styles.title]}>
-          <Trans>Content Filtering</Trans>
-        </Text>
+  return (
+    <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>Content Filtering</Trans>
+      </Text>
 
-        <ScrollView style={styles.scrollContainer}>
-          <AdultContentEnabledPref />
-          <ContentLabelPref
-            preferences={preferences}
-            labelGroup="nsfw"
-            disabled={!preferences?.adultContentEnabled}
-          />
-          <ContentLabelPref
-            preferences={preferences}
-            labelGroup="nudity"
-            disabled={!preferences?.adultContentEnabled}
-          />
-          <ContentLabelPref
-            preferences={preferences}
-            labelGroup="suggestive"
-            disabled={!preferences?.adultContentEnabled}
-          />
-          <ContentLabelPref
-            preferences={preferences}
-            labelGroup="gore"
-            disabled={!preferences?.adultContentEnabled}
-          />
-          <ContentLabelPref preferences={preferences} labelGroup="hate" />
-          <ContentLabelPref preferences={preferences} labelGroup="spam" />
-          <ContentLabelPref
-            preferences={preferences}
-            labelGroup="impersonation"
-          />
-          <View style={{height: isMobile ? 60 : 0}} />
-        </ScrollView>
+      <ScrollView style={styles.scrollContainer}>
+        <AdultContentEnabledPref />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="nsfw"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="nudity"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="suggestive"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="gore"
+          disabled={!preferences?.adultContentEnabled}
+        />
+        <ContentLabelPref preferences={preferences} labelGroup="hate" />
+        <ContentLabelPref preferences={preferences} labelGroup="spam" />
+        <ContentLabelPref
+          preferences={preferences}
+          labelGroup="impersonation"
+        />
+        <View style={{height: isMobile ? 60 : 0}} />
+      </ScrollView>
 
-        <View
-          style={[
-            styles.btnContainer,
-            isMobile && styles.btnContainerMobile,
-            pal.borderDark,
-          ]}>
-          <Pressable
-            testID="sendReportBtn"
-            onPress={onPressDone}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Done`)}
-            accessibilityHint="">
-            <LinearGradient
-              colors={[gradients.blueLight.start, gradients.blueLight.end]}
-              start={{x: 0, y: 0}}
-              end={{x: 1, y: 1}}
-              style={[styles.btn]}>
-              <Text style={[s.white, s.bold, s.f18]}>
-                <Trans>Done</Trans>
-              </Text>
-            </LinearGradient>
-          </Pressable>
-        </View>
+      <View
+        style={[
+          styles.btnContainer,
+          isMobile && styles.btnContainerMobile,
+          pal.borderDark,
+        ]}>
+        <Pressable
+          testID="sendReportBtn"
+          onPress={onPressDone}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Done`)}
+          accessibilityHint="">
+          <LinearGradient
+            colors={[gradients.blueLight.start, gradients.blueLight.end]}
+            start={{x: 0, y: 0}}
+            end={{x: 1, y: 1}}
+            style={[styles.btn]}>
+            <Text style={[s.white, s.bold, s.f18]}>
+              <Trans>Done</Trans>
+            </Text>
+          </LinearGradient>
+        </Pressable>
       </View>
-    )
-  },
-)
+    </View>
+  )
+}
 
 function AdultContentEnabledPref() {
   const pal = usePalette('default')
@@ -171,7 +168,7 @@ function AdultContentEnabledPref() {
 }
 
 // TODO: Refactor this component to pass labels down to each tab
-const ContentLabelPref = observer(function ContentLabelPrefImpl({
+function ContentLabelPref({
   preferences,
   labelGroup,
   disabled,
@@ -217,7 +214,7 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({
       )}
     </View>
   )
-})
+}
 
 interface SelectGroupProps {
   current: LabelPreference
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index 973c7c3a7..82a826aca 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -5,7 +5,6 @@ import {
   View,
   ActivityIndicator,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {ComAtprotoServerDefs} from '@atproto/api'
 import {
   FontAwesomeIcon,
@@ -129,7 +128,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
   )
 }
 
-const InviteCode = observer(function InviteCodeImpl({
+function InviteCode({
   testID,
   invite,
   used,
@@ -211,7 +210,7 @@ const InviteCode = observer(function InviteCodeImpl({
       ) : null}
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
index cb86387a0..c63bc2a87 100644
--- a/src/view/com/modals/LinkWarning.tsx
+++ b/src/view/com/modals/LinkWarning.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {Linking, SafeAreaView, StyleSheet, View} from 'react-native'
 import {ScrollView} from './util'
-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'
@@ -16,13 +15,7 @@ import {useModalControls} from '#/state/modals'
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({
-  text,
-  href,
-}: {
-  text: string
-  href: string
-}) {
+export function Component({text, href}: {text: string; href: string}) {
   const pal = usePalette('default')
   const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
@@ -97,7 +90,7 @@ export const Component = observer(function Component({
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 function LinkBox({href}: {href: string}) {
   const pal = usePalette('default')
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 38c8bc7ba..d834db40b 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -1,7 +1,6 @@
 import React, {useRef, useEffect} from 'react'
 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 {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -40,7 +39,7 @@ import * as LinkWarningModal from './LinkWarning'
 const DEFAULT_SNAPPOINTS = ['90%']
 const HANDLE_HEIGHT = 24
 
-export const ModalsContainer = observer(function ModalsContainer() {
+export function ModalsContainer() {
   const {isModalActive, activeModals} = useModals()
   const {closeModal} = useModalControls()
   const bottomSheetRef = useRef<BottomSheet>(null)
@@ -198,7 +197,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
       {element}
     </BottomSheet>
   )
-})
+}
 
 const styles = StyleSheet.create({
   handle: {
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 28f6c36c9..74aa7b1a9 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import type {Modal as ModalIface} from '#/state/modals'
@@ -33,7 +32,7 @@ import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as LinkWarningModal from './LinkWarning'
 
-export const ModalsContainer = observer(function ModalsContainer() {
+export function ModalsContainer() {
   const {isModalActive, activeModals} = useModals()
 
   if (!isModalActive) {
@@ -47,7 +46,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
       ))}
     </>
   )
-})
+}
 
 function Modal({modal}: {modal: ModalIface}) {
   const {isModalActive} = useModals()
diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx
index f8b64085d..092dd2d32 100644
--- a/src/view/com/modals/SelfLabel.tsx
+++ b/src/view/com/modals/SelfLabel.tsx
@@ -1,6 +1,5 @@
 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 {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -17,7 +16,7 @@ const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({
+export function Component({
   labels,
   hasMedia,
   onChange,
@@ -161,7 +160,7 @@ export const Component = observer(function Component({
       </View>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index 09a460315..b30293859 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -9,7 +9,7 @@ import {Text} from '../util/text/Text'
 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 {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index 106e05b87..e7a4537c5 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -8,7 +8,6 @@ import {
 } from 'react-native'
 import {Svg, Circle, Path} from 'react-native-svg'
 import {ScrollView, TextInput} from './util'
-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'
@@ -32,11 +31,7 @@ enum Stages {
   ConfirmCode,
 }
 
-export const Component = observer(function Component({
-  showReminder,
-}: {
-  showReminder?: boolean
-}) {
+export function Component({showReminder}: {showReminder?: boolean}) {
   const pal = usePalette('default')
   const {agent, currentAccount} = useSession()
   const {updateCurrentAccount} = useSessionApi()
@@ -244,7 +239,7 @@ export const Component = observer(function Component({
       </ScrollView>
     </SafeAreaView>
   )
-})
+}
 
 function ReminderIllustration() {
   const pal = usePalette('default')
diff --git a/src/view/com/modals/lang-settings/LanguageToggle.tsx b/src/view/com/modals/lang-settings/LanguageToggle.tsx
index 86e38a4d2..45b100f20 100644
--- a/src/view/com/modals/lang-settings/LanguageToggle.tsx
+++ b/src/view/com/modals/lang-settings/LanguageToggle.tsx
@@ -1,11 +1,10 @@
 import React from 'react'
 import {StyleSheet} from 'react-native'
 import {usePalette} from 'lib/hooks/usePalette'
-import {observer} from 'mobx-react-lite'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
 import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages'
 
-export const LanguageToggle = observer(function LanguageToggleImpl({
+export function LanguageToggle({
   code2,
   name,
   onPress,
@@ -39,7 +38,7 @@ export const LanguageToggle = observer(function LanguageToggleImpl({
       style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
     />
   )
-})
+}
 
 const styles = StyleSheet.create({
   languageToggle: {
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
index 7fcb24d58..05cfb8115 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {ScrollView} from '../util'
 import {Text} from '../../util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -19,7 +18,7 @@ import {
 
 export const snapPoints = ['100%']
 
-export const Component = observer(function PostLanguagesSettingsImpl() {
+export function Component() {
   const {closeModal} = useModalControls()
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useLanguagePrefsApi()
@@ -111,7 +110,7 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
       <ConfirmLanguagesButton onPress={onPressDone} />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx
index 5183bdc10..06075547f 100644
--- a/src/view/com/modals/report/Modal.tsx
+++ b/src/view/com/modals/report/Modal.tsx
@@ -2,7 +2,6 @@ import React, {useState, useMemo} from 'react'
 import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {ScrollView} from 'react-native-gesture-handler'
 import {AtUri} 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'
@@ -17,6 +16,7 @@ import {CollectionId} from './types'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useSession} from '#/state/session'
 
 const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
 
@@ -39,7 +39,7 @@ type ReportComponentProps =
     }
 
 export function Component(content: ReportComponentProps) {
-  const store = useStores()
+  const {agent} = useSession()
   const {closeModal} = useModalControls()
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
@@ -70,7 +70,7 @@ export function Component(content: ReportComponentProps) {
       const $type = !isAccountReport
         ? 'com.atproto.repo.strongRef'
         : 'com.atproto.admin.defs#repoRef'
-      await store.agent.createModerationReport({
+      await agent.createModerationReport({
         reasonType: issue,
         subject: {
           $type,
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index b48690a66..298372893 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet} from 'react-native'
 import Animated from 'react-native-reanimated'
-import {observer} from 'mobx-react-lite'
 import {TabBar} from 'view/com/pager/TabBar'
 import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -11,7 +10,7 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
 
-export const FeedsTabBar = observer(function FeedsTabBarImpl(
+export function FeedsTabBar(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
   const {isMobile, isTablet} = useWebMediaQueries()
@@ -22,9 +21,9 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
   } else {
     return null
   }
-})
+}
 
-const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl(
+function FeedsTabBarTablet(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
   const feeds = usePinnedFeedsInfos()
@@ -48,7 +47,7 @@ const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl(
       />
     </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   tabBar: {
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index de985fb7c..e0c0be1ef 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {TabBar} from 'view/com/pager/TabBar'
 import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -20,7 +19,7 @@ import {useShellLayout} from '#/state/shell/shell-layout'
 import {useSession} from '#/state/session'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
 
-export const FeedsTabBar = observer(function FeedsTabBarImpl(
+export function FeedsTabBar(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
   const pal = usePalette('default')
@@ -88,7 +87,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
       />
     </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   tabBar: {
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index fad9f9b4e..99ee38536 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FeedPostSlice} from '#/state/queries/post-feed'
 import {AtUri, moderatePost, ModerationOpts} from '@atproto/api'
 import {Link} from '../util/Link'
@@ -10,7 +9,7 @@ import {FeedItem} from './FeedItem'
 import {usePalette} from 'lib/hooks/usePalette'
 import {makeProfileLink} from 'lib/routes/links'
 
-export const FeedSlice = observer(function FeedSliceImpl({
+export function FeedSlice({
   slice,
   dataUpdatedAt,
   ignoreFilterFor,
@@ -94,7 +93,7 @@ export const FeedSlice = observer(function FeedSliceImpl({
       ))}
     </>
   )
-})
+}
 
 function ViewFullThread({slice}: {slice: FeedPostSlice}) {
   const pal = usePalette('default')
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 2f359018f..cd9855456 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,6 +1,5 @@
 import * as React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {
   AppBskyActorDefs,
   moderateProfile,
@@ -152,7 +151,7 @@ function ProfileCardPills({
   )
 }
 
-const FollowersList = observer(function FollowersListImpl({
+function FollowersList({
   followers,
 }: {
   followers?: AppBskyActorDefs.ProfileView[] | undefined
@@ -196,7 +195,7 @@ const FollowersList = observer(function FollowersListImpl({
       ))}
     </View>
   )
-})
+}
 
 export function ProfileCardWithFollowBtn({
   profile,
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index ef128e877..0e245f0f4 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -12,7 +11,6 @@ import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {CenteredView} from '../util/Views'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
-import {useStores} from 'state/index'
 import {NavigationProp} from 'lib/routes/types'
 import {BACK_HITSLOP} from 'lib/constants'
 import {isNative} from 'platform/detection'
@@ -22,7 +20,7 @@ import {msg} from '@lingui/macro'
 import {useSetDrawerOpen} from '#/state/shell'
 import {emitSoftReset} from '#/state/events'
 
-export const ProfileSubpageHeader = observer(function HeaderImpl({
+export function ProfileSubpageHeader({
   isLoading,
   href,
   title,
@@ -45,7 +43,6 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
     | undefined
   avatarType: UserAvatarType
 }>) {
-  const store = useStores()
   const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
@@ -183,7 +180,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   backBtn: {
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index c5e438f8d..fa5f12f6b 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -6,7 +6,6 @@ import {niceDate} from 'lib/strings/time'
 import {usePalette} from 'lib/hooks/usePalette'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {UserAvatar} from './UserAvatar'
-import {observer} from 'mobx-react-lite'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {isAndroid} from 'platform/detection'
@@ -30,7 +29,7 @@ interface PostMetaOpts {
   style?: StyleProp<ViewStyle>
 }
 
-export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
+export function PostMeta(opts: PostMetaOpts) {
   const pal = usePalette('default')
   const displayName = opts.author.displayName || opts.author.handle
   const handle = opts.author.handle
@@ -92,7 +91,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) {
       </TimeElapsed>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
index c871d9404..e86e37565 100644
--- a/src/view/com/util/SimpleViewHeader.tsx
+++ b/src/view/com/util/SimpleViewHeader.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {
   StyleProp,
   StyleSheet,
@@ -18,7 +17,7 @@ import {useSetDrawerOpen} from '#/state/shell'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
-export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
+export function SimpleViewHeader({
   showBackButton = true,
   style,
   children,
@@ -76,7 +75,7 @@ export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
       {children}
     </Container>
   )
-})
+}
 
 const styles = StyleSheet.create({
   header: {
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index adf2e4f08..082cae59c 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
@@ -15,7 +14,7 @@ import {useSetDrawerOpen} from '#/state/shell'
 
 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
 
-export const ViewHeader = observer(function ViewHeaderImpl({
+export function ViewHeader({
   title,
   canGoBack,
   showBackButton = true,
@@ -108,7 +107,7 @@ export const ViewHeader = observer(function ViewHeaderImpl({
       </Container>
     )
   }
-})
+}
 
 function DesktopWebHeader({
   title,
@@ -140,7 +139,7 @@ function DesktopWebHeader({
   )
 }
 
-const Container = observer(function ContainerImpl({
+function Container({
   children,
   hideOnScroll,
   showBorder,
@@ -178,7 +177,7 @@ const Container = observer(function ContainerImpl({
       {children}
     </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   header: {
diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx
index 5b1d5d888..9787d92fb 100644
--- a/src/view/com/util/fab/FABInner.tsx
+++ b/src/view/com/util/fab/FABInner.tsx
@@ -1,5 +1,4 @@
 import React, {ComponentProps} from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
 import {gradients} from 'lib/styles'
@@ -15,11 +14,7 @@ export interface FABProps
   icon: JSX.Element
 }
 
-export const FABInner = observer(function FABInnerImpl({
-  testID,
-  icon,
-  ...props
-}: FABProps) {
+export function FABInner({testID, icon, ...props}: FABProps) {
   const insets = useSafeAreaInsets()
   const {isMobile, isTablet} = useWebMediaQueries()
   const {fabMinimalShellTransform} = useMinimalShellMode()
@@ -55,7 +50,7 @@ export const FABInner = observer(function FABInnerImpl({
       </Animated.View>
     </TouchableWithoutFeedback>
   )
-})
+}
 
 const styles = StyleSheet.create({
   sizeRegular: {
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 6cbcddc32..b5b6c1b52 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -2,8 +2,8 @@ import React from 'react'
 import {StyleProp, StyleSheet, Pressable, View, ViewStyle} from 'react-native'
 import {Image} from 'expo-image'
 import {clamp} from 'lib/numbers'
-import {useStores} from 'state/index'
 import {Dimensions} from 'lib/media/types'
+import * as imageSizes from 'lib/media/image-sizes'
 
 const MIN_ASPECT_RATIO = 0.33 // 1/3
 const MAX_ASPECT_RATIO = 5 // 5/1
@@ -29,9 +29,8 @@ export function AutoSizedImage({
   style,
   children = null,
 }: Props) {
-  const store = useStores()
   const [dim, setDim] = React.useState<Dimensions | undefined>(
-    dimensionsHint || store.imageSizes.get(uri),
+    dimensionsHint || imageSizes.get(uri),
   )
   const [aspectRatio, setAspectRatio] = React.useState<number>(
     dim ? calc(dim) : 1,
@@ -41,14 +40,14 @@ export function AutoSizedImage({
     if (dim) {
       return
     }
-    store.imageSizes.fetch(uri).then(newDim => {
+    imageSizes.fetch(uri).then(newDim => {
       if (aborted) {
         return
       }
       setDim(newDim)
       setAspectRatio(calc(newDim))
     })
-  }, [dim, setDim, setAspectRatio, store, uri])
+  }, [dim, setDim, setAspectRatio, uri])
 
   if (onPress || onLongPress || onPressIn) {
     return (
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index f9a9387bb..970d3a73a 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -12,7 +11,7 @@ const AnimatedTouchableOpacity =
   Animated.createAnimatedComponent(TouchableOpacity)
 import {isWeb} from 'platform/detection'
 
-export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
+export function LoadLatestBtn({
   onPress,
   label,
   showIndicator,
@@ -44,7 +43,7 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
       {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
     </AnimatedTouchableOpacity>
   )
-})
+}
 
 const styles = StyleSheet.create({
   loadLatest: {
diff --git a/src/view/com/util/post-embeds/ListEmbed.tsx b/src/view/com/util/post-embeds/ListEmbed.tsx
index dbf350039..fc5ad270f 100644
--- a/src/view/com/util/post-embeds/ListEmbed.tsx
+++ b/src/view/com/util/post-embeds/ListEmbed.tsx
@@ -1,12 +1,11 @@
 import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {usePalette} from 'lib/hooks/usePalette'
-import {observer} from 'mobx-react-lite'
 import {ListCard} from 'view/com/lists/ListCard'
 import {AppBskyGraphDefs} from '@atproto/api'
 import {s} from 'lib/styles'
 
-export const ListEmbed = observer(function ListEmbedImpl({
+export function ListEmbed({
   item,
   style,
 }: {
@@ -20,7 +19,7 @@ export const ListEmbed = observer(function ListEmbedImpl({
       <ListCard list={item} style={[style, styles.card]} />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 8c80b0fd5..50db7e81f 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
 import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
@@ -15,130 +14,126 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
 import {emitSoftReset} from '#/state/events'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
-export const HomeScreen = withAuthRequired(
-  observer(function HomeScreenImpl({}: Props) {
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-    const pagerRef = React.useRef<PagerRef>(null)
-    const [selectedPage, setSelectedPage] = React.useState(0)
-    const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([])
-    const {data: preferences} = usePreferencesQuery()
-
-    React.useEffect(() => {
-      if (!preferences?.feeds?.pinned) return
-
-      const pinned = preferences.feeds.pinned
-
-      const feeds: FeedDescriptor[] = []
-
-      for (const uri of pinned) {
-        if (uri.includes('app.bsky.feed.generator')) {
-          feeds.push(`feedgen|${uri}`)
-        } else if (uri.includes('app.bsky.graph.list')) {
-          feeds.push(`list|${uri}`)
-        }
+export const HomeScreen = withAuthRequired(function HomeScreenImpl({}: Props) {
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const pagerRef = React.useRef<PagerRef>(null)
+  const [selectedPage, setSelectedPage] = React.useState(0)
+  const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([])
+  const {data: preferences} = usePreferencesQuery()
+
+  React.useEffect(() => {
+    if (!preferences?.feeds?.pinned) return
+
+    const pinned = preferences.feeds.pinned
+
+    const feeds: FeedDescriptor[] = []
+
+    for (const uri of pinned) {
+      if (uri.includes('app.bsky.feed.generator')) {
+        feeds.push(`feedgen|${uri}`)
+      } else if (uri.includes('app.bsky.graph.list')) {
+        feeds.push(`list|${uri}`)
       }
+    }
 
-      setCustomFeeds(feeds)
+    setCustomFeeds(feeds)
 
-      pagerRef.current?.setPage(0)
-    }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef])
+    pagerRef.current?.setPage(0)
+  }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef])
 
-    const homeFeedParams = React.useMemo<FeedParams>(() => {
-      if (!preferences) return {}
+  const homeFeedParams = React.useMemo<FeedParams>(() => {
+    if (!preferences) return {}
 
-      return {
-        mergeFeedEnabled: Boolean(
-          preferences.feedViewPrefs.lab_mergeFeedEnabled,
-        ),
-        mergeFeedSources: preferences.feeds.saved,
-      }
-    }, [preferences])
+    return {
+      mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
+      mergeFeedSources: preferences.feeds.saved,
+    }
+  }, [preferences])
 
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        setDrawerSwipeDisabled(selectedPage > 0)
-        return () => {
-          setDrawerSwipeDisabled(false)
-        }
-      }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]),
-    )
-
-    const onPageSelected = React.useCallback(
-      (index: number) => {
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      setDrawerSwipeDisabled(selectedPage > 0)
+      return () => {
+        setDrawerSwipeDisabled(false)
+      }
+    }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]),
+  )
+
+  const onPageSelected = React.useCallback(
+    (index: number) => {
+      setMinimalShellMode(false)
+      setSelectedPage(index)
+      setDrawerSwipeDisabled(index > 0)
+    },
+    [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode],
+  )
+
+  const onPressSelected = React.useCallback(() => {
+    emitSoftReset()
+  }, [])
+
+  const onPageScrollStateChanged = React.useCallback(
+    (state: 'idle' | 'dragging' | 'settling') => {
+      if (state === 'dragging') {
         setMinimalShellMode(false)
-        setSelectedPage(index)
-        setDrawerSwipeDisabled(index > 0)
-      },
-      [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode],
-    )
-
-    const onPressSelected = React.useCallback(() => {
-      emitSoftReset()
-    }, [])
-
-    const onPageScrollStateChanged = React.useCallback(
-      (state: 'idle' | 'dragging' | 'settling') => {
-        if (state === 'dragging') {
-          setMinimalShellMode(false)
-        }
-      },
-      [setMinimalShellMode],
-    )
-
-    const renderTabBar = React.useCallback(
-      (props: RenderTabBarFnProps) => {
+      }
+    },
+    [setMinimalShellMode],
+  )
+
+  const renderTabBar = React.useCallback(
+    (props: RenderTabBarFnProps) => {
+      return (
+        <FeedsTabBar
+          key="FEEDS_TAB_BAR"
+          selectedPage={props.selectedPage}
+          onSelect={props.onSelect}
+          testID="homeScreenFeedTabs"
+          onPressSelected={onPressSelected}
+        />
+      )
+    },
+    [onPressSelected],
+  )
+
+  const renderFollowingEmptyState = React.useCallback(() => {
+    return <FollowingEmptyState />
+  }, [])
+
+  const renderCustomFeedEmptyState = React.useCallback(() => {
+    return <CustomFeedEmptyState />
+  }, [])
+
+  return (
+    <Pager
+      ref={pagerRef}
+      testID="homeScreen"
+      onPageSelected={onPageSelected}
+      onPageScrollStateChanged={onPageScrollStateChanged}
+      renderTabBar={renderTabBar}
+      tabBarPosition="top">
+      <FeedPage
+        key="1"
+        testID="followingFeedPage"
+        isPageFocused={selectedPage === 0}
+        feed="home"
+        feedParams={homeFeedParams}
+        renderEmptyState={renderFollowingEmptyState}
+        renderEndOfFeed={FollowingEndOfFeed}
+      />
+      {customFeeds.map((f, index) => {
         return (
-          <FeedsTabBar
-            key="FEEDS_TAB_BAR"
-            selectedPage={props.selectedPage}
-            onSelect={props.onSelect}
-            testID="homeScreenFeedTabs"
-            onPressSelected={onPressSelected}
+          <FeedPage
+            key={f}
+            testID="customFeedPage"
+            isPageFocused={selectedPage === 1 + index}
+            feed={f}
+            renderEmptyState={renderCustomFeedEmptyState}
           />
         )
-      },
-      [onPressSelected],
-    )
-
-    const renderFollowingEmptyState = React.useCallback(() => {
-      return <FollowingEmptyState />
-    }, [])
-
-    const renderCustomFeedEmptyState = React.useCallback(() => {
-      return <CustomFeedEmptyState />
-    }, [])
-
-    return (
-      <Pager
-        ref={pagerRef}
-        testID="homeScreen"
-        onPageSelected={onPageSelected}
-        onPageScrollStateChanged={onPageScrollStateChanged}
-        renderTabBar={renderTabBar}
-        tabBarPosition="top">
-        <FeedPage
-          key="1"
-          testID="followingFeedPage"
-          isPageFocused={selectedPage === 0}
-          feed="home"
-          feedParams={homeFeedParams}
-          renderEmptyState={renderFollowingEmptyState}
-          renderEndOfFeed={FollowingEndOfFeed}
-        />
-        {customFeeds.map((f, index) => {
-          return (
-            <FeedPage
-              key={f}
-              testID="customFeedPage"
-              isPageFocused={selectedPage === 1 + index}
-              feed={f}
-              renderEmptyState={renderCustomFeedEmptyState}
-            />
-          )
-        })}
-      </Pager>
-    )
-  }),
-)
+      })}
+    </Pager>
+  )
+})
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index 677451526..649daea0e 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../com/util/text/Text'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -23,9 +22,7 @@ import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
 
-export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
-  _: Props,
-) {
+export function LanguageSettingsScreen(_: Props) {
   const pal = usePalette('default')
   const langPrefs = useLanguagePrefs()
   const setLangPrefs = useLanguagePrefsApi()
@@ -192,7 +189,7 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index 69c07edae..8680b851b 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ScrollView} from '../com/util/Views'
@@ -15,7 +14,7 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useSetMinimalShellMode} from '#/state/shell'
 
-export const LogScreen = observer(function Log({}: NativeStackScreenProps<
+export function LogScreen({}: NativeStackScreenProps<
   CommonNavigatorParams,
   'Log'
 >) {
@@ -88,7 +87,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
       </ScrollView>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   entry: {
diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx
index eb952c068..37eecf22d 100644
--- a/src/view/screens/Moderation.tsx
+++ b/src/view/screens/Moderation.tsx
@@ -5,7 +5,6 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {s} from 'lib/styles'
@@ -21,7 +20,7 @@ import {useModalControls} from '#/state/modals'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
 export const ModerationScreen = withAuthRequired(
-  observer(function Moderation({}: Props) {
+  function Moderation({}: Props) {
     const pal = usePalette('default')
     const setMinimalShellMode = useSetMinimalShellMode()
     const {screen, track} = useAnalytics()
@@ -111,7 +110,7 @@ export const ModerationScreen = withAuthRequired(
         </Link>
       </CenteredView>
     )
-  }),
+  },
 )
 
 const styles = StyleSheet.create({
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 752f78dce..844a96d11 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -3,7 +3,6 @@ import {StyleSheet, View} from 'react-native'
 import Animated from 'react-native-reanimated'
 import {useFocusEffect} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
@@ -26,83 +25,83 @@ import {CenteredView} from '../com/util/Views'
 import {useComposerControls} from '#/state/shell/composer'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
-export const PostThreadScreen = withAuthRequired(
-  observer(function PostThreadScreenImpl({route}: Props) {
-    const queryClient = useQueryClient()
-    const {fabMinimalShellTransform} = useMinimalShellMode()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {openComposer} = useComposerControls()
-    const safeAreaInsets = useSafeAreaInsets()
-    const {name, rkey} = route.params
-    const {isMobile} = useWebMediaQueries()
-    const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-    const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
+export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({
+  route,
+}: Props) {
+  const queryClient = useQueryClient()
+  const {fabMinimalShellTransform} = useMinimalShellMode()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {openComposer} = useComposerControls()
+  const safeAreaInsets = useSafeAreaInsets()
+  const {name, rkey} = route.params
+  const {isMobile} = useWebMediaQueries()
+  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
+  const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
 
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-      }, [setMinimalShellMode]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
 
-    const onPressReply = React.useCallback(() => {
-      if (!resolvedUri) {
-        return
-      }
-      const thread = queryClient.getQueryData<ThreadNode>(
-        POST_THREAD_RQKEY(resolvedUri.uri),
-      )
-      if (thread?.type !== 'post') {
-        return
-      }
-      openComposer({
-        replyTo: {
-          uri: thread.post.uri,
-          cid: thread.post.cid,
-          text: thread.record.text,
-          author: {
-            handle: thread.post.author.handle,
-            displayName: thread.post.author.displayName,
-            avatar: thread.post.author.avatar,
-          },
+  const onPressReply = React.useCallback(() => {
+    if (!resolvedUri) {
+      return
+    }
+    const thread = queryClient.getQueryData<ThreadNode>(
+      POST_THREAD_RQKEY(resolvedUri.uri),
+    )
+    if (thread?.type !== 'post') {
+      return
+    }
+    openComposer({
+      replyTo: {
+        uri: thread.post.uri,
+        cid: thread.post.cid,
+        text: thread.record.text,
+        author: {
+          handle: thread.post.author.handle,
+          displayName: thread.post.author.displayName,
+          avatar: thread.post.author.avatar,
         },
-        onPost: () =>
-          queryClient.invalidateQueries({
-            queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
-          }),
-      })
-    }, [openComposer, queryClient, resolvedUri])
+      },
+      onPost: () =>
+        queryClient.invalidateQueries({
+          queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
+        }),
+    })
+  }, [openComposer, queryClient, resolvedUri])
 
-    return (
-      <View style={s.hContentRegion}>
-        {isMobile && <ViewHeader title="Post" />}
-        <View style={s.flex1}>
-          {uriError ? (
-            <CenteredView>
-              <ErrorMessage message={String(uriError)} />
-            </CenteredView>
-          ) : (
-            <PostThreadComponent
-              uri={resolvedUri?.uri}
-              onPressReply={onPressReply}
-            />
-          )}
-        </View>
-        {isMobile && (
-          <Animated.View
-            style={[
-              styles.prompt,
-              fabMinimalShellTransform,
-              {
-                bottom: clamp(safeAreaInsets.bottom, 15, 30),
-              },
-            ]}>
-            <ComposePrompt onPressCompose={onPressReply} />
-          </Animated.View>
+  return (
+    <View style={s.hContentRegion}>
+      {isMobile && <ViewHeader title="Post" />}
+      <View style={s.flex1}>
+        {uriError ? (
+          <CenteredView>
+            <ErrorMessage message={String(uriError)} />
+          </CenteredView>
+        ) : (
+          <PostThreadComponent
+            uri={resolvedUri?.uri}
+            onPressReply={onPressReply}
+          />
         )}
       </View>
-    )
-  }),
-)
+      {isMobile && (
+        <Animated.View
+          style={[
+            styles.prompt,
+            fabMinimalShellTransform,
+            {
+              bottom: clamp(safeAreaInsets.bottom, 15, 30),
+            },
+          ]}>
+          <ComposePrompt onPressCompose={onPressReply} />
+        </Animated.View>
+      )}
+    </View>
+  )
+})
 
 const styles = StyleSheet.create({
   prompt: {
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx
index 7b240ded0..2fd0eff37 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -1,6 +1,5 @@
 import React, {useState} from 'react'
 import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Slider} from '@miblanchard/react-native-slider'
 import {Text} from '../com/util/text/Text'
@@ -72,9 +71,7 @@ type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'PreferencesHomeFeed'
 >
-export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
-  navigation,
-}: Props) {
+export function PreferencesHomeFeed({navigation}: Props) {
   const pal = usePalette('default')
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
@@ -308,7 +305,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 2386f6445..7bd87b712 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -6,7 +6,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../com/util/text/Text'
 import {s, colors} from 'lib/styles'
@@ -25,9 +24,7 @@ import {
 } from '#/state/queries/preferences'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
-export const PreferencesThreads = observer(function PreferencesThreadsImpl({
-  navigation,
-}: Props) {
+export function PreferencesThreads({navigation}: Props) {
   const pal = usePalette('default')
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
@@ -162,7 +159,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index f62790be6..62f5f1b36 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -15,7 +15,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {colors, s} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
 import {FeedDescriptor} from '#/state/queries/post-feed'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
@@ -71,7 +70,7 @@ interface SectionRef {
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
 export const ProfileFeedScreen = withAuthRequired(
-  observer(function ProfileFeedScreenImpl(props: Props) {
+  function ProfileFeedScreenImpl(props: Props) {
     const {rkey, name: handleOrDid} = props.route.params
 
     const pal = usePalette('default')
@@ -129,7 +128,7 @@ export const ProfileFeedScreen = withAuthRequired(
         </View>
       </CenteredView>
     )
-  }),
+  },
 )
 
 function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
@@ -154,7 +153,7 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
   )
 }
 
-export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({
+export function ProfileFeedScreenInner({
   preferences,
   feedInfo,
 }: {
@@ -485,7 +484,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   },
 )
 
-const AboutSection = observer(function AboutPageImpl({
+function AboutSection({
   feedOwnerDid,
   feedRkey,
   feedInfo,
@@ -606,7 +605,7 @@ const AboutSection = observer(function AboutPageImpl({
       </View>
     </ScrollView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   btn: {
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index c7abcf090..e3e50ca24 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -14,7 +14,6 @@ import {track} from '#/lib/analytics/analytics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {observer} from 'mobx-react-lite'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
@@ -146,7 +145,7 @@ export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) {
   )
 })
 
-const ListItem = observer(function ListItemImpl({
+function ListItem({
   feedUri,
   isPinned,
 }: {
@@ -269,7 +268,7 @@ const ListItem = observer(function ListItemImpl({
       </TouchableOpacity>
     </Pressable>
   )
-})
+}
 
 const styles = StyleSheet.create({
   desktopContainer: {
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 3f7ef146a..916fd2a53 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -19,11 +19,9 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import * as AppInfo from 'lib/app-info'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {ScrollView} from '../com/util/Views'
 import {ViewHeader} from '../com/util/ViewHeader'
@@ -45,7 +43,7 @@ import {formatCount} from 'view/com/util/numeric/format'
 import Clipboard from '@react-native-clipboard/clipboard'
 import {makeProfileLink} from 'lib/routes/links'
 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
-import {logger} from '#/logger'
+import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
 import {useModalControls} from '#/state/modals'
 import {
   useSetMinimalShellMode,
@@ -69,6 +67,7 @@ import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
 import {STATUS_PAGE_URL} from 'lib/constants'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
@@ -135,630 +134,620 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
-export const SettingsScreen = withAuthRequired(
-  observer(function Settings({}: Props) {
-    const colorMode = useColorMode()
-    const setColorMode = useSetColorMode()
-    const pal = usePalette('default')
-    const store = useStores()
-    const {_} = useLingui()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const requireAltTextEnabled = useRequireAltTextEnabled()
-    const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
-    const onboardingDispatch = useOnboardingDispatch()
-    const navigation = useNavigation<NavigationProp>()
-    const {isMobile} = useWebMediaQueries()
-    const {screen, track} = useAnalytics()
-    const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
-      store.agent,
-    )
-    const {openModal} = useModalControls()
-    const {isSwitchingAccounts, accounts, currentAccount} = useSession()
-    const {clearCurrentAccount} = useSessionApi()
-    const {mutate: clearPreferences} = useClearPreferencesMutation()
-    const {data: invites} = useInviteCodesQuery()
-    const invitesAvailable = invites?.available?.length ?? 0
-
-    const primaryBg = useCustomPalette<ViewStyle>({
-      light: {backgroundColor: colors.blue0},
-      dark: {backgroundColor: colors.blue6},
-    })
-    const primaryText = useCustomPalette<TextStyle>({
-      light: {color: colors.blue3},
-      dark: {color: colors.blue2},
-    })
+export const SettingsScreen = withAuthRequired(function Settings({}: Props) {
+  const queryClient = useQueryClient()
+  const colorMode = useColorMode()
+  const setColorMode = useSetColorMode()
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const requireAltTextEnabled = useRequireAltTextEnabled()
+  const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
+  const onboardingDispatch = useOnboardingDispatch()
+  const navigation = useNavigation<NavigationProp>()
+  const {isMobile} = useWebMediaQueries()
+  const {screen, track} = useAnalytics()
+  const {openModal} = useModalControls()
+  const {isSwitchingAccounts, accounts, currentAccount, agent} = useSession()
+  const {clearCurrentAccount} = useSessionApi()
+  const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(agent)
+  const {mutate: clearPreferences} = useClearPreferencesMutation()
+  const {data: invites} = useInviteCodesQuery()
+  const invitesAvailable = invites?.available?.length ?? 0
+
+  const primaryBg = useCustomPalette<ViewStyle>({
+    light: {backgroundColor: colors.blue0},
+    dark: {backgroundColor: colors.blue6},
+  })
+  const primaryText = useCustomPalette<TextStyle>({
+    light: {color: colors.blue3},
+    dark: {color: colors.blue2},
+  })
+
+  const dangerBg = useCustomPalette<ViewStyle>({
+    light: {backgroundColor: colors.red1},
+    dark: {backgroundColor: colors.red7},
+  })
+  const dangerText = useCustomPalette<TextStyle>({
+    light: {color: colors.red4},
+    dark: {color: colors.red2},
+  })
+
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('Settings')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    const dangerBg = useCustomPalette<ViewStyle>({
-      light: {backgroundColor: colors.red1},
-      dark: {backgroundColor: colors.red7},
-    })
-    const dangerText = useCustomPalette<TextStyle>({
-      light: {color: colors.red4},
-      dark: {color: colors.red2},
+  const onPressAddAccount = React.useCallback(() => {
+    track('Settings:AddAccountButtonClicked')
+    navigation.navigate('HomeTab')
+    navigation.dispatch(StackActions.popToTop())
+    clearCurrentAccount()
+  }, [track, navigation, clearCurrentAccount])
+
+  const onPressChangeHandle = React.useCallback(() => {
+    track('Settings:ChangeHandleButtonClicked')
+    openModal({
+      name: 'change-handle',
+      onChanged() {
+        if (currentAccount) {
+          // refresh my profile
+          queryClient.invalidateQueries({
+            queryKey: RQKEY_PROFILE(currentAccount.did),
+          })
+        }
+      },
     })
+  }, [track, queryClient, openModal, currentAccount])
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('Settings')
-        setMinimalShellMode(false)
-      }, [screen, setMinimalShellMode]),
-    )
+  const onPressInviteCodes = React.useCallback(() => {
+    track('Settings:InvitecodesButtonClicked')
+    openModal({name: 'invite-codes'})
+  }, [track, openModal])
+
+  const onPressLanguageSettings = React.useCallback(() => {
+    navigation.navigate('LanguageSettings')
+  }, [navigation])
 
-    const onPressAddAccount = React.useCallback(() => {
-      track('Settings:AddAccountButtonClicked')
-      navigation.navigate('HomeTab')
-      navigation.dispatch(StackActions.popToTop())
-      clearCurrentAccount()
-    }, [track, navigation, clearCurrentAccount])
-
-    const onPressChangeHandle = React.useCallback(() => {
-      track('Settings:ChangeHandleButtonClicked')
-      openModal({
-        name: 'change-handle',
-        onChanged() {
-          store.session.reloadFromServer().then(
-            () => {
-              Toast.show('Your handle has been updated')
-            },
-            err => {
-              logger.error('Failed to reload from server after handle update', {
-                error: err,
-              })
-            },
-          )
-        },
-      })
-    }, [track, store, openModal])
-
-    const onPressInviteCodes = React.useCallback(() => {
-      track('Settings:InvitecodesButtonClicked')
-      openModal({name: 'invite-codes'})
-    }, [track, openModal])
-
-    const onPressLanguageSettings = React.useCallback(() => {
-      navigation.navigate('LanguageSettings')
-    }, [navigation])
-
-    const onPressDeleteAccount = React.useCallback(() => {
-      openModal({name: 'delete-account'})
-    }, [openModal])
-
-    const onPressResetPreferences = React.useCallback(async () => {
-      clearPreferences()
-    }, [clearPreferences])
-
-    const onPressResetOnboarding = React.useCallback(async () => {
-      onboardingDispatch({type: 'start'})
-      Toast.show('Onboarding reset')
-    }, [onboardingDispatch])
-
-    const onPressBuildInfo = React.useCallback(() => {
-      Clipboard.setString(
-        `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
-      )
-      Toast.show('Copied build version to clipboard')
-    }, [])
-
-    const openHomeFeedPreferences = React.useCallback(() => {
-      navigation.navigate('PreferencesHomeFeed')
-    }, [navigation])
-
-    const openThreadsPreferences = React.useCallback(() => {
-      navigation.navigate('PreferencesThreads')
-    }, [navigation])
-
-    const onPressAppPasswords = React.useCallback(() => {
-      navigation.navigate('AppPasswords')
-    }, [navigation])
-
-    const onPressSystemLog = React.useCallback(() => {
-      navigation.navigate('Log')
-    }, [navigation])
-
-    const onPressStorybook = React.useCallback(() => {
-      navigation.navigate('Debug')
-    }, [navigation])
-
-    const onPressSavedFeeds = React.useCallback(() => {
-      navigation.navigate('SavedFeeds')
-    }, [navigation])
-
-    const onPressStatusPage = React.useCallback(() => {
-      Linking.openURL(STATUS_PAGE_URL)
-    }, [])
-
-    return (
-      <View style={[s.hContentRegion]} testID="settingsScreen">
-        <ViewHeader title="Settings" />
-        <ScrollView
-          style={[s.hContentRegion]}
-          contentContainerStyle={isMobile && pal.viewLight}
-          scrollIndicatorInsets={{right: 1}}>
-          <View style={styles.spacer20} />
-          {currentAccount ? (
-            <>
-              <Text type="xl-bold" style={[pal.text, styles.heading]}>
-                <Trans>Account</Trans>
+  const onPressDeleteAccount = React.useCallback(() => {
+    openModal({name: 'delete-account'})
+  }, [openModal])
+
+  const onPressResetPreferences = React.useCallback(async () => {
+    clearPreferences()
+  }, [clearPreferences])
+
+  const onPressResetOnboarding = React.useCallback(async () => {
+    onboardingDispatch({type: 'start'})
+    Toast.show('Onboarding reset')
+  }, [onboardingDispatch])
+
+  const onPressBuildInfo = React.useCallback(() => {
+    Clipboard.setString(
+      `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
+    )
+    Toast.show('Copied build version to clipboard')
+  }, [])
+
+  const openHomeFeedPreferences = React.useCallback(() => {
+    navigation.navigate('PreferencesHomeFeed')
+  }, [navigation])
+
+  const openThreadsPreferences = React.useCallback(() => {
+    navigation.navigate('PreferencesThreads')
+  }, [navigation])
+
+  const onPressAppPasswords = React.useCallback(() => {
+    navigation.navigate('AppPasswords')
+  }, [navigation])
+
+  const onPressSystemLog = React.useCallback(() => {
+    navigation.navigate('Log')
+  }, [navigation])
+
+  const onPressStorybook = React.useCallback(() => {
+    navigation.navigate('Debug')
+  }, [navigation])
+
+  const onPressSavedFeeds = React.useCallback(() => {
+    navigation.navigate('SavedFeeds')
+  }, [navigation])
+
+  const onPressStatusPage = React.useCallback(() => {
+    Linking.openURL(STATUS_PAGE_URL)
+  }, [])
+
+  return (
+    <View style={[s.hContentRegion]} testID="settingsScreen">
+      <ViewHeader title="Settings" />
+      <ScrollView
+        style={[s.hContentRegion]}
+        contentContainerStyle={isMobile && pal.viewLight}
+        scrollIndicatorInsets={{right: 1}}>
+        <View style={styles.spacer20} />
+        {currentAccount ? (
+          <>
+            <Text type="xl-bold" style={[pal.text, styles.heading]}>
+              <Trans>Account</Trans>
+            </Text>
+            <View style={[styles.infoLine]}>
+              <Text type="lg-medium" style={pal.text}>
+                Email:{' '}
               </Text>
-              <View style={[styles.infoLine]}>
-                <Text type="lg-medium" style={pal.text}>
-                  Email:{' '}
-                </Text>
-                {currentAccount.emailConfirmed && (
-                  <>
-                    <FontAwesomeIcon
-                      icon="check"
-                      size={10}
-                      style={{color: colors.green3, marginRight: 2}}
-                    />
-                  </>
-                )}
-                <Text type="lg" style={pal.text}>
-                  {currentAccount.email}{' '}
+              {currentAccount.emailConfirmed && (
+                <>
+                  <FontAwesomeIcon
+                    icon="check"
+                    size={10}
+                    style={{color: colors.green3, marginRight: 2}}
+                  />
+                </>
+              )}
+              <Text type="lg" style={pal.text}>
+                {currentAccount.email}{' '}
+              </Text>
+              <Link onPress={() => openModal({name: 'change-email'})}>
+                <Text type="lg" style={pal.link}>
+                  <Trans>Change</Trans>
                 </Text>
-                <Link onPress={() => openModal({name: 'change-email'})}>
-                  <Text type="lg" style={pal.link}>
-                    <Trans>Change</Trans>
-                  </Text>
-                </Link>
-              </View>
-              <View style={[styles.infoLine]}>
-                <Text type="lg-medium" style={pal.text}>
-                  <Trans>Birthday:</Trans>{' '}
+              </Link>
+            </View>
+            <View style={[styles.infoLine]}>
+              <Text type="lg-medium" style={pal.text}>
+                <Trans>Birthday:</Trans>{' '}
+              </Text>
+              <Link onPress={() => openModal({name: 'birth-date-settings'})}>
+                <Text type="lg" style={pal.link}>
+                  <Trans>Show</Trans>
                 </Text>
-                <Link onPress={() => openModal({name: 'birth-date-settings'})}>
-                  <Text type="lg" style={pal.link}>
-                    <Trans>Show</Trans>
-                  </Text>
-                </Link>
-              </View>
-              <View style={styles.spacer20} />
-
-              {!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
-            </>
-          ) : null}
-          <View style={[s.flexRow, styles.heading]}>
-            <Text type="xl-bold" style={pal.text}>
-              <Trans>Signed in as</Trans>
-            </Text>
-            <View style={s.flex1} />
+              </Link>
+            </View>
+            <View style={styles.spacer20} />
+
+            {!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
+          </>
+        ) : null}
+        <View style={[s.flexRow, styles.heading]}>
+          <Text type="xl-bold" style={pal.text}>
+            <Trans>Signed in as</Trans>
+          </Text>
+          <View style={s.flex1} />
+        </View>
+
+        {isSwitchingAccounts ? (
+          <View style={[pal.view, styles.linkCard]}>
+            <ActivityIndicator />
           </View>
+        ) : (
+          <SettingsAccountCard account={currentAccount!} />
+        )}
 
-          {isSwitchingAccounts ? (
-            <View style={[pal.view, styles.linkCard]}>
-              <ActivityIndicator />
-            </View>
-          ) : (
-            <SettingsAccountCard account={currentAccount!} />
-          )}
+        {accounts
+          .filter(a => a.did !== currentAccount?.did)
+          .map(account => (
+            <SettingsAccountCard key={account.did} account={account} />
+          ))}
 
-          {accounts
-            .filter(a => a.did !== currentAccount?.did)
-            .map(account => (
-              <SettingsAccountCard key={account.did} account={account} />
-            ))}
+        <TouchableOpacity
+          testID="switchToNewAccountBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Add account`)}
+          accessibilityHint="Create a new Bluesky account">
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon="plus"
+              style={pal.text as FontAwesomeIconStyle}
+            />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>Add account</Trans>
+          </Text>
+        </TouchableOpacity>
 
-          <TouchableOpacity
-            testID="switchToNewAccountBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Add account`)}
-            accessibilityHint="Create a new Bluesky account">
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="plus"
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>Add account</Trans>
-            </Text>
-          </TouchableOpacity>
+        <View style={styles.spacer20} />
 
-          <View style={styles.spacer20} />
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Invite a Friend</Trans>
+        </Text>
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Invite a Friend</Trans>
+        <TouchableOpacity
+          testID="inviteFriendBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Invite`)}
+          accessibilityHint="Opens invite code list">
+          <View
+            style={[
+              styles.iconContainer,
+              invitesAvailable > 0 ? primaryBg : pal.btn,
+            ]}>
+            <FontAwesomeIcon
+              icon="ticket"
+              style={
+                (invitesAvailable > 0
+                  ? primaryText
+                  : pal.text) as FontAwesomeIconStyle
+              }
+            />
+          </View>
+          <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}>
+            {formatCount(invitesAvailable)} invite{' '}
+            {pluralize(invitesAvailable, 'code')} available
           </Text>
+        </TouchableOpacity>
 
-          <TouchableOpacity
-            testID="inviteFriendBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Invite`)}
-            accessibilityHint="Opens invite code list">
-            <View
-              style={[
-                styles.iconContainer,
-                invitesAvailable > 0 ? primaryBg : pal.btn,
-              ]}>
-              <FontAwesomeIcon
-                icon="ticket"
-                style={
-                  (invitesAvailable > 0
-                    ? primaryText
-                    : pal.text) as FontAwesomeIconStyle
-                }
-              />
-            </View>
-            <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}>
-              {formatCount(invitesAvailable)} invite{' '}
-              {pluralize(invitesAvailable, 'code')} available
-            </Text>
-          </TouchableOpacity>
+        <View style={styles.spacer20} />
 
-          <View style={styles.spacer20} />
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Accessibility</Trans>
+        </Text>
+        <View style={[pal.view, styles.toggleCard]}>
+          <ToggleButton
+            type="default-light"
+            label="Require alt text before posting"
+            labelType="lg"
+            isSelected={requireAltTextEnabled}
+            onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)}
+          />
+        </View>
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Accessibility</Trans>
-          </Text>
-          <View style={[pal.view, styles.toggleCard]}>
-            <ToggleButton
-              type="default-light"
-              label="Require alt text before posting"
-              labelType="lg"
-              isSelected={requireAltTextEnabled}
-              onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)}
+        <View style={styles.spacer20} />
+
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Appearance</Trans>
+        </Text>
+        <View>
+          <View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
+            <SelectableBtn
+              selected={colorMode === 'system'}
+              label="System"
+              left
+              onSelect={() => setColorMode('system')}
+              accessibilityHint="Set color theme to system setting"
+            />
+            <SelectableBtn
+              selected={colorMode === 'light'}
+              label="Light"
+              onSelect={() => setColorMode('light')}
+              accessibilityHint="Set color theme to light"
+            />
+            <SelectableBtn
+              selected={colorMode === 'dark'}
+              label="Dark"
+              right
+              onSelect={() => setColorMode('dark')}
+              accessibilityHint="Set color theme to dark"
             />
           </View>
+        </View>
+        <View style={styles.spacer20} />
 
-          <View style={styles.spacer20} />
-
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Appearance</Trans>
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Basics</Trans>
+        </Text>
+        <TouchableOpacity
+          testID="preferencesHomeFeedButton"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={openHomeFeedPreferences}
+          accessibilityRole="button"
+          accessibilityHint=""
+          accessibilityLabel={_(msg`Opens the home feed preferences`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon="sliders"
+              style={pal.text as FontAwesomeIconStyle}
+            />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>Home Feed Preferences</Trans>
           </Text>
-          <View>
-            <View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
-              <SelectableBtn
-                selected={colorMode === 'system'}
-                label="System"
-                left
-                onSelect={() => setColorMode('system')}
-                accessibilityHint="Set color theme to system setting"
-              />
-              <SelectableBtn
-                selected={colorMode === 'light'}
-                label="Light"
-                onSelect={() => setColorMode('light')}
-                accessibilityHint="Set color theme to light"
-              />
-              <SelectableBtn
-                selected={colorMode === 'dark'}
-                label="Dark"
-                right
-                onSelect={() => setColorMode('dark')}
-                accessibilityHint="Set color theme to dark"
-              />
-            </View>
+        </TouchableOpacity>
+        <TouchableOpacity
+          testID="preferencesThreadsButton"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={openThreadsPreferences}
+          accessibilityRole="button"
+          accessibilityHint=""
+          accessibilityLabel={_(msg`Opens the threads preferences`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon={['far', 'comments']}
+              style={pal.text as FontAwesomeIconStyle}
+              size={18}
+            />
           </View>
-          <View style={styles.spacer20} />
-
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Basics</Trans>
+          <Text type="lg" style={pal.text}>
+            <Trans>Thread Preferences</Trans>
           </Text>
-          <TouchableOpacity
-            testID="preferencesHomeFeedButton"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={openHomeFeedPreferences}
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel={_(msg`Opens the home feed preferences`)}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="sliders"
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>Home Feed Preferences</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="preferencesThreadsButton"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={openThreadsPreferences}
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel={_(msg`Opens the threads preferences`)}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon={['far', 'comments']}
-                style={pal.text as FontAwesomeIconStyle}
-                size={18}
-              />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>Thread Preferences</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="savedFeedsBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            accessibilityHint="My Saved Feeds"
-            accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
-            onPress={onPressSavedFeeds}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <HashtagIcon style={pal.text} size={18} strokeWidth={3} />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>My Saved Feeds</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="languageSettingsBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
-            accessibilityRole="button"
-            accessibilityHint="Language settings"
-            accessibilityLabel={_(msg`Opens configurable language settings`)}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="language"
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>Languages</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="moderationBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={
-              isSwitchingAccounts
-                ? undefined
-                : () => navigation.navigate('Moderation')
-            }
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel={_(msg`Opens moderation settings`)}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <HandIcon style={pal.text} size={18} strokeWidth={6} />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>Moderation</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Advanced</Trans>
+        </TouchableOpacity>
+        <TouchableOpacity
+          testID="savedFeedsBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          accessibilityHint="My Saved Feeds"
+          accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
+          onPress={onPressSavedFeeds}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <HashtagIcon style={pal.text} size={18} strokeWidth={3} />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>My Saved Feeds</Trans>
           </Text>
-          <TouchableOpacity
-            testID="appPasswordBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={onPressAppPasswords}
-            accessibilityRole="button"
-            accessibilityHint="Open app password settings"
-            accessibilityLabel={_(msg`Opens the app password settings page`)}>
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="lock"
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-            <Text type="lg" style={pal.text}>
-              <Trans>App passwords</Trans>
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="changeHandleBtn"
-            style={[
-              styles.linkCard,
-              pal.view,
-              isSwitchingAccounts && styles.dimmed,
-            ]}
-            onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Change handle`)}
-            accessibilityHint="Choose a new Bluesky username or create">
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="at"
-                style={pal.text as FontAwesomeIconStyle}
-              />
-            </View>
-            <Text type="lg" style={pal.text} numberOfLines={1}>
-              <Trans>Change handle</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Danger Zone</Trans>
+        </TouchableOpacity>
+        <TouchableOpacity
+          testID="languageSettingsBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
+          accessibilityRole="button"
+          accessibilityHint="Language settings"
+          accessibilityLabel={_(msg`Opens configurable language settings`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon="language"
+              style={pal.text as FontAwesomeIconStyle}
+            />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>Languages</Trans>
           </Text>
-          <TouchableOpacity
-            style={[pal.view, styles.linkCard]}
-            onPress={onPressDeleteAccount}
-            accessible={true}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Delete account`)}
-            accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
-            <View style={[styles.iconContainer, dangerBg]}>
-              <FontAwesomeIcon
-                icon={['far', 'trash-can']}
-                style={dangerText as FontAwesomeIconStyle}
-                size={18}
-              />
-            </View>
-            <Text type="lg" style={dangerText}>
-              <Trans>Delete my account…</Trans>
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            <Trans>Developer Tools</Trans>
+        </TouchableOpacity>
+        <TouchableOpacity
+          testID="moderationBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={
+            isSwitchingAccounts
+              ? undefined
+              : () => navigation.navigate('Moderation')
+          }
+          accessibilityRole="button"
+          accessibilityHint=""
+          accessibilityLabel={_(msg`Opens moderation settings`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <HandIcon style={pal.text} size={18} strokeWidth={6} />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>Moderation</Trans>
           </Text>
-          <TouchableOpacity
-            style={[pal.view, styles.linkCardNoIcon]}
-            onPress={onPressSystemLog}
-            accessibilityRole="button"
-            accessibilityHint="Open system log"
-            accessibilityLabel={_(msg`Opens the system log page`)}>
-            <Text type="lg" style={pal.text}>
-              <Trans>System log</Trans>
-            </Text>
-          </TouchableOpacity>
-          {__DEV__ ? (
-            <ToggleButton
-              type="default-light"
-              label="Experiment: Use AppView Proxy"
-              isSelected={debugHeaderEnabled}
-              onPress={toggleDebugHeader}
+        </TouchableOpacity>
+        <View style={styles.spacer20} />
+
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Advanced</Trans>
+        </Text>
+        <TouchableOpacity
+          testID="appPasswordBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={onPressAppPasswords}
+          accessibilityRole="button"
+          accessibilityHint="Open app password settings"
+          accessibilityLabel={_(msg`Opens the app password settings page`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon="lock"
+              style={pal.text as FontAwesomeIconStyle}
             />
-          ) : null}
-          {__DEV__ ? (
-            <>
-              <TouchableOpacity
-                style={[pal.view, styles.linkCardNoIcon]}
-                onPress={onPressStorybook}
-                accessibilityRole="button"
-                accessibilityHint="Open storybook page"
-                accessibilityLabel={_(msg`Opens the storybook page`)}>
-                <Text type="lg" style={pal.text}>
-                  <Trans>Storybook</Trans>
-                </Text>
-              </TouchableOpacity>
-              <TouchableOpacity
-                style={[pal.view, styles.linkCardNoIcon]}
-                onPress={onPressResetPreferences}
-                accessibilityRole="button"
-                accessibilityHint="Reset preferences"
-                accessibilityLabel={_(msg`Resets the preferences state`)}>
-                <Text type="lg" style={pal.text}>
-                  <Trans>Reset preferences state</Trans>
-                </Text>
-              </TouchableOpacity>
-              <TouchableOpacity
-                style={[pal.view, styles.linkCardNoIcon]}
-                onPress={onPressResetOnboarding}
-                accessibilityRole="button"
-                accessibilityHint="Reset onboarding"
-                accessibilityLabel={_(msg`Resets the onboarding state`)}>
-                <Text type="lg" style={pal.text}>
-                  <Trans>Reset onboarding state</Trans>
-                </Text>
-              </TouchableOpacity>
-            </>
-          ) : null}
-          <View style={[styles.footer]}>
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>App passwords</Trans>
+          </Text>
+        </TouchableOpacity>
+        <TouchableOpacity
+          testID="changeHandleBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Change handle`)}
+          accessibilityHint="Choose a new Bluesky username or create">
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon="at"
+              style={pal.text as FontAwesomeIconStyle}
+            />
+          </View>
+          <Text type="lg" style={pal.text} numberOfLines={1}>
+            <Trans>Change handle</Trans>
+          </Text>
+        </TouchableOpacity>
+        <View style={styles.spacer20} />
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Danger Zone</Trans>
+        </Text>
+        <TouchableOpacity
+          style={[pal.view, styles.linkCard]}
+          onPress={onPressDeleteAccount}
+          accessible={true}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Delete account`)}
+          accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
+          <View style={[styles.iconContainer, dangerBg]}>
+            <FontAwesomeIcon
+              icon={['far', 'trash-can']}
+              style={dangerText as FontAwesomeIconStyle}
+              size={18}
+            />
+          </View>
+          <Text type="lg" style={dangerText}>
+            <Trans>Delete my account…</Trans>
+          </Text>
+        </TouchableOpacity>
+        <View style={styles.spacer20} />
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Developer Tools</Trans>
+        </Text>
+        <TouchableOpacity
+          style={[pal.view, styles.linkCardNoIcon]}
+          onPress={onPressSystemLog}
+          accessibilityRole="button"
+          accessibilityHint="Open system log"
+          accessibilityLabel={_(msg`Opens the system log page`)}>
+          <Text type="lg" style={pal.text}>
+            <Trans>System log</Trans>
+          </Text>
+        </TouchableOpacity>
+        {__DEV__ ? (
+          <ToggleButton
+            type="default-light"
+            label="Experiment: Use AppView Proxy"
+            isSelected={debugHeaderEnabled}
+            onPress={toggleDebugHeader}
+          />
+        ) : null}
+        {__DEV__ ? (
+          <>
             <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
+              onPress={onPressStorybook}
               accessibilityRole="button"
-              onPress={onPressBuildInfo}>
-              <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-                <Trans>
-                  Build version {AppInfo.appVersion} {AppInfo.updateChannel}
-                </Trans>
+              accessibilityHint="Open storybook page"
+              accessibilityLabel={_(msg`Opens the storybook page`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Storybook</Trans>
               </Text>
             </TouchableOpacity>
-            <Text type="sm" style={[pal.textLight]}>
-              &middot; &nbsp;
-            </Text>
             <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
+              onPress={onPressResetPreferences}
               accessibilityRole="button"
-              onPress={onPressStatusPage}>
-              <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
-                <Trans>Status page</Trans>
+              accessibilityHint="Reset preferences"
+              accessibilityLabel={_(msg`Resets the preferences state`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Reset preferences state</Trans>
               </Text>
             </TouchableOpacity>
-          </View>
-          <View style={s.footerSpacer} />
-        </ScrollView>
-      </View>
-    )
-  }),
-)
-
-const EmailConfirmationNotice = observer(
-  function EmailConfirmationNoticeImpl() {
-    const pal = usePalette('default')
-    const palInverted = usePalette('inverted')
-    const {_} = useLingui()
-    const {isMobile} = useWebMediaQueries()
-    const {openModal} = useModalControls()
-
-    return (
-      <View style={{marginBottom: 20}}>
-        <Text type="xl-bold" style={[pal.text, styles.heading]}>
-          <Trans>Verify email</Trans>
-        </Text>
-        <View
-          style={[
-            {
-              paddingVertical: isMobile ? 12 : 0,
-              paddingHorizontal: 18,
-            },
-            pal.view,
-          ]}>
-          <View style={{flexDirection: 'row', marginBottom: 8}}>
-            <Pressable
-              style={[
-                palInverted.view,
-                {
-                  flexDirection: 'row',
-                  gap: 6,
-                  borderRadius: 6,
-                  paddingHorizontal: 12,
-                  paddingVertical: 10,
-                  alignItems: 'center',
-                },
-                isMobile && {flex: 1},
-              ]}
+            <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
+              onPress={onPressResetOnboarding}
               accessibilityRole="button"
-              accessibilityLabel={_(msg`Verify my email`)}
-              accessibilityHint=""
-              onPress={() => openModal({name: 'verify-email'})}>
-              <FontAwesomeIcon
-                icon="envelope"
-                color={palInverted.colors.text}
-                size={16}
-              />
-              <Text type="button" style={palInverted.text}>
-                <Trans>Verify My Email</Trans>
+              accessibilityHint="Reset onboarding"
+              accessibilityLabel={_(msg`Resets the onboarding state`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Reset onboarding state</Trans>
               </Text>
-            </Pressable>
-          </View>
-          <Text style={pal.textLight}>
-            <Trans>Protect your account by verifying your email.</Trans>
+            </TouchableOpacity>
+          </>
+        ) : null}
+        <View style={[styles.footer]}>
+          <TouchableOpacity
+            accessibilityRole="button"
+            onPress={onPressBuildInfo}>
+            <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
+              <Trans>
+                Build version {AppInfo.appVersion} {AppInfo.updateChannel}
+              </Trans>
+            </Text>
+          </TouchableOpacity>
+          <Text type="sm" style={[pal.textLight]}>
+            &middot; &nbsp;
           </Text>
+          <TouchableOpacity
+            accessibilityRole="button"
+            onPress={onPressStatusPage}>
+            <Text type="sm" style={[styles.buildInfo, pal.textLight]}>
+              <Trans>Status page</Trans>
+            </Text>
+          </TouchableOpacity>
         </View>
+        <View style={s.footerSpacer} />
+      </ScrollView>
+    </View>
+  )
+})
+
+function EmailConfirmationNotice() {
+  const pal = usePalette('default')
+  const palInverted = usePalette('inverted')
+  const {_} = useLingui()
+  const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
+
+  return (
+    <View style={{marginBottom: 20}}>
+      <Text type="xl-bold" style={[pal.text, styles.heading]}>
+        <Trans>Verify email</Trans>
+      </Text>
+      <View
+        style={[
+          {
+            paddingVertical: isMobile ? 12 : 0,
+            paddingHorizontal: 18,
+          },
+          pal.view,
+        ]}>
+        <View style={{flexDirection: 'row', marginBottom: 8}}>
+          <Pressable
+            style={[
+              palInverted.view,
+              {
+                flexDirection: 'row',
+                gap: 6,
+                borderRadius: 6,
+                paddingHorizontal: 12,
+                paddingVertical: 10,
+                alignItems: 'center',
+              },
+              isMobile && {flex: 1},
+            ]}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Verify my email`)}
+            accessibilityHint=""
+            onPress={() => openModal({name: 'verify-email'})}>
+            <FontAwesomeIcon
+              icon="envelope"
+              color={palInverted.colors.text}
+              size={16}
+            />
+            <Text type="button" style={palInverted.text}>
+              <Trans>Verify My Email</Trans>
+            </Text>
+          </Pressable>
+        </View>
+        <Text style={pal.textLight}>
+          <Trans>Protect your account by verifying your email.</Trans>
+        </Text>
       </View>
-    )
-  },
-)
+    </View>
+  )
+}
 
 const styles = StyleSheet.create({
   dimmed: {
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 1ee359be0..a67a105bb 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -10,7 +10,6 @@ import {
   ViewStyle,
 } from 'react-native'
 import {useNavigation, StackActions} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -101,7 +100,7 @@ export function DrawerProfileCard({
   )
 }
 
-export const DrawerContent = observer(function DrawerContentImpl() {
+export function DrawerContent() {
   const theme = useTheme()
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -404,7 +403,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       </SafeAreaView>
     </View>
   )
-})
+}
 
 interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> {
   icon: JSX.Element
@@ -458,11 +457,7 @@ function MenuItem({
   )
 }
 
-const InviteCodes = observer(function InviteCodesImpl({
-  style,
-}: {
-  style?: StyleProp<ViewStyle>
-}) {
+function InviteCodes({style}: {style?: StyleProp<ViewStyle>}) {
   const {track} = useAnalytics()
   const setDrawerOpen = useSetDrawerOpen()
   const pal = usePalette('default')
@@ -502,7 +497,7 @@ const InviteCodes = observer(function InviteCodesImpl({
       </Text>
     </TouchableOpacity>
   )
-})
+}
 
 const styles = StyleSheet.create({
   view: {
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index a1502d2e9..ed800679d 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -4,7 +4,6 @@ import Animated from 'react-native-reanimated'
 import {StackActions} from '@react-navigation/native'
 import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {observer} from 'mobx-react-lite'
 import {Text} from 'view/com/util/text/Text'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {clamp} from 'lib/numbers'
@@ -34,9 +33,7 @@ import {useProfileQuery} from '#/state/queries/profile'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
-export const BottomBar = observer(function BottomBarImpl({
-  navigation,
-}: BottomTabBarProps) {
+export function BottomBar({navigation}: BottomTabBarProps) {
   const {openModal} = useModalControls()
   const {currentAccount} = useSession()
   const pal = usePalette('default')
@@ -231,7 +228,7 @@ export const BottomBar = observer(function BottomBarImpl({
       />
     </Animated.View>
   )
-})
+}
 
 interface BtnProps
   extends Pick<
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index d58580502..32fa1943a 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useNavigationState} from '@react-navigation/native'
 import Animated from 'react-native-reanimated'
@@ -24,7 +23,7 @@ import {makeProfileLink} from 'lib/routes/links'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {useSession} from '#/state/session'
 
-export const BottomBarWeb = observer(function BottomBarWebImpl() {
+export function BottomBarWeb() {
   const {currentAccount} = useSession()
   const pal = usePalette('default')
   const safeAreaInsets = useSafeAreaInsets()
@@ -111,7 +110,7 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() {
       </NavItem>
     </Animated.View>
   )
-})
+}
 
 const NavItem: React.FC<{
   children: (props: {isActive: boolean}) => React.ReactChild
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 9cb10517e..dc5e311f4 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -1,13 +1,12 @@
 import React from 'react'
 import {View, StyleSheet} from 'react-native'
 import {useNavigationState} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
 import {usePalette} from 'lib/hooks/usePalette'
 import {TextLink} from 'view/com/util/Link'
 import {getCurrentRoute} from 'lib/routes/helpers'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
 
-export const DesktopFeeds = observer(function DesktopFeeds() {
+export function DesktopFeeds() {
   const pal = usePalette('default')
   const feeds = usePinnedFeedsInfos()
 
@@ -54,7 +53,7 @@ export const DesktopFeeds = observer(function DesktopFeeds() {
       </View>
     </View>
   )
-})
+}
 
 function FeedItem({
   title,
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 3ec68872e..841a49d38 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {PressableWithHover} from 'view/com/util/PressableWithHover'
 import {
@@ -47,7 +46,7 @@ import {useComposerControls} from '#/state/shell/composer'
 import {useFetchHandle} from '#/state/queries/handle'
 import {emitSoftReset} from '#/state/events'
 
-const ProfileCard = observer(function ProfileCardImpl() {
+function ProfileCard() {
   const {currentAccount} = useSession()
   const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did})
   const {isDesktop} = useWebMediaQueries()
@@ -73,7 +72,7 @@ const ProfileCard = observer(function ProfileCardImpl() {
       />
     </View>
   )
-})
+}
 
 function BackBtn() {
   const {isTablet} = useWebMediaQueries()
@@ -117,13 +116,7 @@ interface NavItemProps {
   iconFilled: JSX.Element
   label: string
 }
-const NavItem = observer(function NavItemImpl({
-  count,
-  href,
-  icon,
-  iconFilled,
-  label,
-}: NavItemProps) {
+function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
   const pal = usePalette('default')
   const {currentAccount} = useSession()
   const {isDesktop, isTablet} = useWebMediaQueries()
@@ -192,7 +185,7 @@ const NavItem = observer(function NavItemImpl({
       )}
     </PressableWithHover>
   )
-})
+}
 
 function ComposeBtn() {
   const {currentAccount} = useSession()
@@ -264,7 +257,7 @@ function ComposeBtn() {
   )
 }
 
-export const DesktopLeftNav = observer(function DesktopLeftNav() {
+export function DesktopLeftNav() {
   const {currentAccount} = useSession()
   const pal = usePalette('default')
   const {isDesktop, isTablet} = useWebMediaQueries()
@@ -422,7 +415,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
       <ComposeBtn />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   leftNav: {
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 9e17cdcd9..3b94c12ef 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -16,7 +15,7 @@ import {useModalControls} from '#/state/modals'
 import {useSession} from '#/state/session'
 import {useInviteCodesQuery} from '#/state/queries/invites'
 
-export const DesktopRightNav = observer(function DesktopRightNavImpl() {
+export function DesktopRightNav() {
   const pal = usePalette('default')
   const palError = usePalette('error')
   const {isSandbox, hasSession, currentAccount} = useSession()
@@ -80,9 +79,9 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
       <InviteCodes />
     </View>
   )
-})
+}
 
-const InviteCodes = observer(function InviteCodesImpl() {
+function InviteCodes() {
   const pal = usePalette('default')
   const {openModal} = useModalControls()
   const {data: invites} = useInviteCodesQuery()
@@ -118,7 +117,7 @@ const InviteCodes = observer(function InviteCodesImpl() {
       </Text>
     </TouchableOpacity>
   )
-})
+}
 
 const styles = StyleSheet.create({
   rightNav: {
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index c0eed0787..09f8ba983 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -1,5 +1,4 @@
 import React, {useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
 import {View, StyleSheet, TouchableOpacity} from 'react-native'
 import {DesktopLeftNav} from './desktop/LeftNav'
 import {DesktopRightNav} from './desktop/RightNav'
@@ -76,7 +75,7 @@ function ShellInner() {
   )
 }
 
-export const Shell: React.FC = observer(function ShellImpl() {
+export const Shell: React.FC = function ShellImpl() {
   const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
   return (
     <View style={[s.hContentRegion, pageBg]}>
@@ -85,7 +84,7 @@ export const Shell: React.FC = observer(function ShellImpl() {
       </RoutesContainer>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   bgLight: {