about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/AppPasswords.tsx211
-rw-r--r--src/view/screens/CommunityGuidelines.tsx19
-rw-r--r--src/view/screens/CopyrightPolicy.tsx19
-rw-r--r--src/view/screens/Feeds.tsx695
-rw-r--r--src/view/screens/Home.tsx284
-rw-r--r--src/view/screens/LanguageSettings.tsx153
-rw-r--r--src/view/screens/Lists.tsx138
-rw-r--r--src/view/screens/Log.tsx10
-rw-r--r--src/view/screens/Moderation.tsx185
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx234
-rw-r--r--src/view/screens/ModerationModlists.tsx137
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx231
-rw-r--r--src/view/screens/NotFound.tsx11
-rw-r--r--src/view/screens/Notifications.tsx241
-rw-r--r--src/view/screens/PostLikedBy.tsx10
-rw-r--r--src/view/screens/PostRepostedBy.tsx10
-rw-r--r--src/view/screens/PostThread.tsx161
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx208
-rw-r--r--src/view/screens/PreferencesThreads.tsx170
-rw-r--r--src/view/screens/PrivacyPolicy.tsx19
-rw-r--r--src/view/screens/Profile.tsx668
-rw-r--r--src/view/screens/ProfileFeed.tsx780
-rw-r--r--src/view/screens/ProfileFeedLikedBy.tsx10
-rw-r--r--src/view/screens/ProfileFollowers.tsx10
-rw-r--r--src/view/screens/ProfileFollows.tsx10
-rw-r--r--src/view/screens/ProfileList.tsx640
-rw-r--r--src/view/screens/SavedFeeds.tsx380
-rw-r--r--src/view/screens/Search.tsx1
-rw-r--r--src/view/screens/Search.web.tsx76
-rw-r--r--src/view/screens/Search/Search.tsx658
-rw-r--r--src/view/screens/Search/index.tsx3
-rw-r--r--src/view/screens/Search/index.web.tsx3
-rw-r--r--src/view/screens/SearchMobile.tsx203
-rw-r--r--src/view/screens/Settings.tsx1315
-rw-r--r--src/view/screens/Support.tsx23
-rw-r--r--src/view/screens/TermsOfService.tsx7
36 files changed, 4697 insertions, 3236 deletions
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index 74d293ef4..154035f22 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -1,80 +1,114 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {
+  ActivityIndicator,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {ScrollView} from 'react-native-gesture-handler'
 import {Text} from '../com/util/text/Text'
 import {Button} from '../com/util/forms/Button'
 import * as Toast from '../com/util/Toast'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {useLanguagePrefs} from '#/state/preferences'
+import {
+  useAppPasswordsQuery,
+  useAppPasswordDeleteMutation,
+} from '#/state/queries/app-passwords'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
+import {cleanError} from '#/lib/strings/errors'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
-export const AppPasswords = withAuthRequired(
-  observer(function AppPasswordsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {screen} = useAnalytics()
-    const {isTabletOrDesktop} = useWebMediaQueries()
+export function AppPasswords({}: Props) {
+  const pal = usePalette('default')
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {screen} = useAnalytics()
+  const {isTabletOrDesktop} = useWebMediaQueries()
+  const {openModal} = useModalControls()
+  const {data: appPasswords, error} = useAppPasswordsQuery()
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('AppPasswords')
-        setMinimalShellMode(false)
-      }, [screen, setMinimalShellMode]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('AppPasswords')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    const onAdd = React.useCallback(async () => {
-      store.shell.openModal({name: 'add-app-password'})
-    }, [store])
+  const onAdd = React.useCallback(async () => {
+    openModal({name: 'add-app-password'})
+  }, [openModal])
 
-    // no app passwords (empty) state
-    if (store.me.appPasswords.length === 0) {
-      return (
-        <CenteredView
-          style={[
-            styles.container,
-            isTabletOrDesktop && styles.containerDesktop,
-            pal.view,
-            pal.border,
-          ]}
-          testID="appPasswordsScreen">
-          <AppPasswordsHeader />
-          <View style={[styles.empty, pal.viewLight]}>
-            <Text type="lg" style={[pal.text, styles.emptyText]}>
+  if (error) {
+    return (
+      <CenteredView
+        style={[
+          styles.container,
+          isTabletOrDesktop && styles.containerDesktop,
+          pal.view,
+          pal.border,
+        ]}
+        testID="appPasswordsScreen">
+        <ErrorScreen
+          title="Oops!"
+          message="There was an issue with fetching your app passwords"
+          details={cleanError(error)}
+        />
+      </CenteredView>
+    )
+  }
+
+  // no app passwords (empty) state
+  if (appPasswords?.length === 0) {
+    return (
+      <CenteredView
+        style={[
+          styles.container,
+          isTabletOrDesktop && styles.containerDesktop,
+          pal.view,
+          pal.border,
+        ]}
+        testID="appPasswordsScreen">
+        <AppPasswordsHeader />
+        <View style={[styles.empty, pal.viewLight]}>
+          <Text type="lg" style={[pal.text, styles.emptyText]}>
+            <Trans>
               You have not created any app passwords yet. You can create one by
               pressing the button below.
-            </Text>
-          </View>
-          {!isTabletOrDesktop && <View style={styles.flex1} />}
-          <View
-            style={[
-              styles.btnContainer,
-              isTabletOrDesktop && styles.btnContainerDesktop,
-            ]}>
-            <Button
-              testID="appPasswordBtn"
-              type="primary"
-              label="Add App Password"
-              style={styles.btn}
-              labelStyle={styles.btnLabel}
-              onPress={onAdd}
-            />
-          </View>
-        </CenteredView>
-      )
-    }
+            </Trans>
+          </Text>
+        </View>
+        {!isTabletOrDesktop && <View style={styles.flex1} />}
+        <View
+          style={[
+            styles.btnContainer,
+            isTabletOrDesktop && styles.btnContainerDesktop,
+          ]}>
+          <Button
+            testID="appPasswordBtn"
+            type="primary"
+            label="Add App Password"
+            style={styles.btn}
+            labelStyle={styles.btnLabel}
+            onPress={onAdd}
+          />
+        </View>
+      </CenteredView>
+    )
+  }
 
+  if (appPasswords?.length) {
     // has app passwords
     return (
       <CenteredView
@@ -92,7 +126,7 @@ export const AppPasswords = withAuthRequired(
             pal.border,
             !isTabletOrDesktop && styles.flex1,
           ]}>
-          {store.me.appPasswords.map((password, i) => (
+          {appPasswords.map((password, i) => (
             <AppPassword
               key={password.name}
               testID={`appPassword-${i}`}
@@ -127,15 +161,29 @@ export const AppPasswords = withAuthRequired(
         )}
       </CenteredView>
     )
-  }),
-)
+  }
+
+  return (
+    <CenteredView
+      style={[
+        styles.container,
+        isTabletOrDesktop && styles.containerDesktop,
+        pal.view,
+        pal.border,
+      ]}
+      testID="appPasswordsScreen">
+      <ActivityIndicator />
+    </CenteredView>
+  )
+}
 
 function AppPasswordsHeader() {
   const {isTabletOrDesktop} = useWebMediaQueries()
   const pal = usePalette('default')
+  const {_} = useLingui()
   return (
     <>
-      <ViewHeader title="App Passwords" showOnDesktop />
+      <ViewHeader title={_(msg`App Passwords`)} showOnDesktop />
       <Text
         type="sm"
         style={[
@@ -143,8 +191,10 @@ function AppPasswordsHeader() {
           pal.text,
           isTabletOrDesktop && styles.descriptionDesktop,
         ]}>
-        Use app passwords to login to other Bluesky clients without giving full
-        access to your account or password.
+        <Trans>
+          Use app passwords to login to other Bluesky clients without giving
+          full access to your account or password.
+        </Trans>
       </Text>
     </>
   )
@@ -160,21 +210,24 @@ function AppPassword({
   createdAt: string
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const {openModal} = useModalControls()
+  const {contentLanguages} = useLanguagePrefs()
+  const deleteMutation = useAppPasswordDeleteMutation()
 
   const onDelete = React.useCallback(async () => {
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Delete App Password',
-      message: `Are you sure you want to delete the app password "${name}"?`,
+      title: _(msg`Delete app password`),
+      message: _(
+        msg`Are you sure you want to delete the app password "${name}"?`,
+      ),
       async onPressConfirm() {
-        await store.me.deleteAppPassword(name)
+        await deleteMutation.mutateAsync({name})
         Toast.show('App password deleted')
       },
     })
-  }, [store, name])
-
-  const {contentLanguages} = store.preferences
+  }, [deleteMutation, openModal, name, _])
 
   const primaryLocale =
     contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
@@ -185,22 +238,24 @@ function AppPassword({
       style={[styles.item, pal.border]}
       onPress={onDelete}
       accessibilityRole="button"
-      accessibilityLabel="Delete app password"
+      accessibilityLabel={_(msg`Delete app password`)}
       accessibilityHint="">
       <View>
         <Text type="md-bold" style={pal.text}>
           {name}
         </Text>
         <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
-          Created{' '}
-          {Intl.DateTimeFormat(primaryLocale, {
-            year: 'numeric',
-            month: 'numeric',
-            day: 'numeric',
-            hour: '2-digit',
-            minute: '2-digit',
-            second: '2-digit',
-          }).format(new Date(createdAt))}
+          <Trans>
+            Created{' '}
+            {Intl.DateTimeFormat(primaryLocale, {
+              year: 'numeric',
+              month: 'numeric',
+              day: 'numeric',
+              hour: '2-digit',
+              minute: '2-digit',
+              second: '2-digit',
+            }).format(new Date(createdAt))}
+          </Trans>
         </Text>
       </View>
       <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
diff --git a/src/view/screens/CommunityGuidelines.tsx b/src/view/screens/CommunityGuidelines.tsx
index 712172c3b..1931c6f13 100644
--- a/src/view/screens/CommunityGuidelines.tsx
+++ b/src/view/screens/CommunityGuidelines.tsx
@@ -9,6 +9,8 @@ import {ScrollView} from 'view/com/util/Views'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -16,6 +18,7 @@ type Props = NativeStackScreenProps<
 >
 export const CommunityGuidelinesScreen = (_props: Props) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
 
   useFocusEffect(
@@ -26,16 +29,18 @@ export const CommunityGuidelinesScreen = (_props: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Community Guidelines" />
+      <ViewHeader title={_(msg`Community Guidelines`)} />
       <ScrollView style={[s.hContentRegion, pal.view]}>
         <View style={[s.p20]}>
           <Text style={pal.text}>
-            The Community Guidelines have been moved to{' '}
-            <TextLink
-              style={pal.link}
-              href="https://blueskyweb.xyz/support/community-guidelines"
-              text="blueskyweb.xyz/support/community-guidelines"
-            />
+            <Trans>
+              The Community Guidelines have been moved to{' '}
+              <TextLink
+                style={pal.link}
+                href="https://blueskyweb.xyz/support/community-guidelines"
+                text="blueskyweb.xyz/support/community-guidelines"
+              />
+            </Trans>
           </Text>
         </View>
         <View style={s.footerSpacer} />
diff --git a/src/view/screens/CopyrightPolicy.tsx b/src/view/screens/CopyrightPolicy.tsx
index 816c1c1ee..2026f28c6 100644
--- a/src/view/screens/CopyrightPolicy.tsx
+++ b/src/view/screens/CopyrightPolicy.tsx
@@ -9,10 +9,13 @@ import {ScrollView} from 'view/com/util/Views'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'CopyrightPolicy'>
 export const CopyrightPolicyScreen = (_props: Props) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
 
   useFocusEffect(
@@ -23,16 +26,18 @@ export const CopyrightPolicyScreen = (_props: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Copyright Policy" />
+      <ViewHeader title={_(msg`Copyright Policy`)} />
       <ScrollView style={[s.hContentRegion, pal.view]}>
         <View style={[s.p20]}>
           <Text style={pal.text}>
-            The Copyright Policy has been moved to{' '}
-            <TextLink
-              style={pal.link}
-              href="https://blueskyweb.xyz/support/community-guidelines"
-              text="blueskyweb.xyz/support/community-guidelines"
-            />
+            <Trans>
+              The Copyright Policy has been moved to{' '}
+              <TextLink
+                style={pal.link}
+                href="https://blueskyweb.xyz/support/community-guidelines"
+                text="blueskyweb.xyz/support/community-guidelines"
+              />
+            </Trans>
           </Text>
         </View>
         <View style={s.footerSpacer} />
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 169660a8f..f319fbc39 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,15 +1,12 @@
 import React from 'react'
-import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native'
+import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {FAB} from 'view/com/util/fab/FAB'
 import {Link} from 'view/com/util/Link'
 import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
-import {observer} from 'mobx-react-lite'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ComposeIcon2, CogIcon} from 'lib/icons'
 import {s} from 'lib/styles'
@@ -22,255 +19,525 @@ import {
 import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
 import debounce from 'lodash.debounce'
 import {Text} from 'view/com/util/text/Text'
-import {MyFeedsItem} from 'state/models/ui/my-feeds'
-import {FeedSourceModel} from 'state/models/content/feed-source'
 import {FlatList} from 'view/com/util/Views'
 import {useFocusEffect} from '@react-navigation/native'
 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {
+  useFeedSourceInfoQuery,
+  useGetPopularFeedsQuery,
+  useSearchPopularFeedsMutation,
+} from '#/state/queries/feed'
+import {cleanError} from 'lib/strings/errors'
+import {useComposerControls} from '#/state/shell/composer'
+import {useSession} from '#/state/session'
 
 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
-export const FeedsScreen = withAuthRequired(
-  observer<Props>(function FeedsScreenImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
-    const myFeeds = store.me.myFeeds
-    const [query, setQuery] = React.useState<string>('')
-    const debouncedSearchFeeds = React.useMemo(
-      () => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms
-      [myFeeds],
-    )
 
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        myFeeds.setup()
+type FlatlistSlice =
+  | {
+      type: 'error'
+      key: string
+      error: string
+    }
+  | {
+      type: 'savedFeedsHeader'
+      key: string
+    }
+  | {
+      type: 'savedFeedsLoading'
+      key: string
+      // pendingItems: number,
+    }
+  | {
+      type: 'savedFeedNoResults'
+      key: string
+    }
+  | {
+      type: 'savedFeed'
+      key: string
+      feedUri: string
+    }
+  | {
+      type: 'savedFeedsLoadMore'
+      key: string
+    }
+  | {
+      type: 'popularFeedsHeader'
+      key: string
+    }
+  | {
+      type: 'popularFeedsLoading'
+      key: string
+    }
+  | {
+      type: 'popularFeedsNoResults'
+      key: string
+    }
+  | {
+      type: 'popularFeed'
+      key: string
+      feedUri: string
+    }
+  | {
+      type: 'popularFeedsLoadingMore'
+      key: string
+    }
 
-        const softResetSub = store.onScreenSoftReset(() => myFeeds.refresh())
-        return () => {
-          softResetSub.remove()
-        }
-      }, [store, myFeeds, setMinimalShellMode]),
+export function FeedsScreen(_props: Props) {
+  const pal = usePalette('default')
+  const {openComposer} = useComposerControls()
+  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+  const [query, setQuery] = React.useState('')
+  const [isPTR, setIsPTR] = React.useState(false)
+  const {
+    data: preferences,
+    isLoading: isPreferencesLoading,
+    error: preferencesError,
+  } = usePreferencesQuery()
+  const {
+    data: popularFeeds,
+    isFetching: isPopularFeedsFetching,
+    error: popularFeedsError,
+    refetch: refetchPopularFeeds,
+    fetchNextPage: fetchNextPopularFeedsPage,
+    isFetchingNextPage: isPopularFeedsFetchingNextPage,
+    hasNextPage: hasNextPopularFeedsPage,
+  } = useGetPopularFeedsQuery()
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {
+    data: searchResults,
+    mutate: search,
+    reset: resetSearch,
+    isPending: isSearchPending,
+    error: searchError,
+  } = useSearchPopularFeedsMutation()
+  const {hasSession} = useSession()
+
+  /**
+   * A search query is present. We may not have search results yet.
+   */
+  const isUserSearching = query.length > 1
+  const debouncedSearch = React.useMemo(
+    () => debounce(q => search(q), 500), // debounce for 500ms
+    [search],
+  )
+  const onPressCompose = React.useCallback(() => {
+    openComposer({})
+  }, [openComposer])
+  const onChangeQuery = React.useCallback(
+    (text: string) => {
+      setQuery(text)
+      if (text.length > 1) {
+        debouncedSearch(text)
+      } else {
+        refetchPopularFeeds()
+        resetSearch()
+      }
+    },
+    [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch],
+  )
+  const onPressCancelSearch = React.useCallback(() => {
+    setQuery('')
+    refetchPopularFeeds()
+    resetSearch()
+  }, [refetchPopularFeeds, setQuery, resetSearch])
+  const onSubmitQuery = React.useCallback(() => {
+    debouncedSearch(query)
+  }, [query, debouncedSearch])
+  const onPullToRefresh = React.useCallback(async () => {
+    setIsPTR(true)
+    await refetchPopularFeeds()
+    setIsPTR(false)
+  }, [setIsPTR, refetchPopularFeeds])
+  const onEndReached = React.useCallback(() => {
+    if (
+      isPopularFeedsFetching ||
+      isUserSearching ||
+      !hasNextPopularFeedsPage ||
+      popularFeedsError
     )
-    React.useEffect(() => {
-      // watch for changes to saved/pinned feeds
-      return myFeeds.registerListeners()
-    }, [myFeeds])
+      return
+    fetchNextPopularFeedsPage()
+  }, [
+    isPopularFeedsFetching,
+    isUserSearching,
+    popularFeedsError,
+    hasNextPopularFeedsPage,
+    fetchNextPopularFeedsPage,
+  ])
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
+
+  const items = React.useMemo(() => {
+    let slices: FlatlistSlice[] = []
 
-    const onPressCompose = React.useCallback(() => {
-      store.shell.openComposer({})
-    }, [store])
-    const onChangeQuery = React.useCallback(
-      (text: string) => {
-        setQuery(text)
-        if (text.length > 1) {
-          debouncedSearchFeeds(text)
+    if (hasSession) {
+      slices.push({
+        key: 'savedFeedsHeader',
+        type: 'savedFeedsHeader',
+      })
+
+      if (preferencesError) {
+        slices.push({
+          key: 'savedFeedsError',
+          type: 'error',
+          error: cleanError(preferencesError.toString()),
+        })
+      } else {
+        if (isPreferencesLoading || !preferences?.feeds?.saved) {
+          slices.push({
+            key: 'savedFeedsLoading',
+            type: 'savedFeedsLoading',
+            // pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
+          })
         } else {
-          myFeeds.discovery.refresh()
-        }
-      },
-      [debouncedSearchFeeds, myFeeds.discovery],
-    )
-    const onPressCancelSearch = React.useCallback(() => {
-      setQuery('')
-      myFeeds.discovery.refresh()
-    }, [myFeeds])
-    const onSubmitQuery = React.useCallback(() => {
-      debouncedSearchFeeds(query)
-      debouncedSearchFeeds.flush()
-    }, [debouncedSearchFeeds, query])
+          if (preferences?.feeds?.saved.length === 0) {
+            slices.push({
+              key: 'savedFeedNoResults',
+              type: 'savedFeedNoResults',
+            })
+          } else {
+            const {saved, pinned} = preferences.feeds
 
-    const renderHeaderBtn = React.useCallback(() => {
-      return (
-        <Link
-          href="/settings/saved-feeds"
-          hitSlop={10}
-          accessibilityRole="button"
-          accessibilityLabel="Edit Saved Feeds"
-          accessibilityHint="Opens screen to edit Saved Feeds">
-          <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
-        </Link>
-      )
-    }, [pal])
+            slices = slices.concat(
+              pinned.map(uri => ({
+                key: `savedFeed:${uri}`,
+                type: 'savedFeed',
+                feedUri: uri,
+              })),
+            )
 
-    const onRefresh = React.useCallback(() => {
-      myFeeds.refresh()
-    }, [myFeeds])
+            slices = slices.concat(
+              saved
+                .filter(uri => !pinned.includes(uri))
+                .map(uri => ({
+                  key: `savedFeed:${uri}`,
+                  type: 'savedFeed',
+                  feedUri: uri,
+                })),
+            )
+          }
+        }
+      }
+    }
 
-    const renderItem = React.useCallback(
-      ({item}: {item: MyFeedsItem}) => {
-        if (item.type === 'discover-feeds-loading') {
-          return <FeedFeedLoadingPlaceholder />
-        } else if (item.type === 'spinner') {
-          return (
-            <View style={s.p10}>
-              <ActivityIndicator />
-            </View>
-          )
-        } else if (item.type === 'error') {
-          return <ErrorMessage message={item.error} />
-        } else if (item.type === 'saved-feeds-header') {
-          if (!isMobile) {
-            return (
-              <View
-                style={[
-                  pal.view,
-                  styles.header,
-                  pal.border,
-                  {
-                    borderBottomWidth: 1,
-                  },
-                ]}>
-                <Text type="title-lg" style={[pal.text, s.bold]}>
-                  My Feeds
-                </Text>
-                <Link
-                  href="/settings/saved-feeds"
-                  accessibilityLabel="Edit My Feeds"
-                  accessibilityHint="">
-                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
-                </Link>
-              </View>
+    slices.push({
+      key: 'popularFeedsHeader',
+      type: 'popularFeedsHeader',
+    })
+
+    if (popularFeedsError || searchError) {
+      slices.push({
+        key: 'popularFeedsError',
+        type: 'error',
+        error: cleanError(
+          popularFeedsError?.toString() ?? searchError?.toString() ?? '',
+        ),
+      })
+    } else {
+      if (isUserSearching) {
+        if (isSearchPending || !searchResults) {
+          slices.push({
+            key: 'popularFeedsLoading',
+            type: 'popularFeedsLoading',
+          })
+        } else {
+          if (!searchResults || searchResults?.length === 0) {
+            slices.push({
+              key: 'popularFeedsNoResults',
+              type: 'popularFeedsNoResults',
+            })
+          } else {
+            slices = slices.concat(
+              searchResults.map(feed => ({
+                key: `popularFeed:${feed.uri}`,
+                type: 'popularFeed',
+                feedUri: feed.uri,
+              })),
             )
           }
-          return <View />
-        } else if (item.type === 'saved-feeds-loading') {
-          return (
-            <>
-              {Array.from(Array(item.numItems)).map((_, i) => (
-                <SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
-              ))}
-            </>
-          )
-        } else if (item.type === 'saved-feed') {
-          return <SavedFeed feed={item.feed} />
-        } else if (item.type === 'discover-feeds-header') {
-          return (
-            <>
-              <View
-                style={[
-                  pal.view,
-                  styles.header,
-                  {
-                    marginTop: 16,
-                    paddingLeft: isMobile ? 12 : undefined,
-                    paddingRight: 10,
-                    paddingBottom: isMobile ? 6 : undefined,
-                  },
-                ]}>
-                <Text type="title-lg" style={[pal.text, s.bold]}>
-                  Discover new feeds
-                </Text>
-                {!isMobile && (
-                  <SearchInput
-                    query={query}
-                    onChangeQuery={onChangeQuery}
-                    onPressCancelSearch={onPressCancelSearch}
-                    onSubmitQuery={onSubmitQuery}
-                    style={{flex: 1, maxWidth: 250}}
-                  />
-                )}
-              </View>
-              {isMobile && (
-                <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
-                  <SearchInput
-                    query={query}
-                    onChangeQuery={onChangeQuery}
-                    onPressCancelSearch={onPressCancelSearch}
-                    onSubmitQuery={onSubmitQuery}
-                  />
-                </View>
-              )}
-            </>
-          )
-        } else if (item.type === 'discover-feed') {
-          return (
-            <FeedSourceCard
-              item={item.feed}
-              showSaveBtn
-              showDescription
-              showLikes
-            />
-          )
-        } else if (item.type === 'discover-feeds-no-results') {
+        }
+      } else {
+        if (isPopularFeedsFetching && !popularFeeds?.pages) {
+          slices.push({
+            key: 'popularFeedsLoading',
+            type: 'popularFeedsLoading',
+          })
+        } else {
+          if (
+            !popularFeeds?.pages ||
+            popularFeeds?.pages[0]?.feeds?.length === 0
+          ) {
+            slices.push({
+              key: 'popularFeedsNoResults',
+              type: 'popularFeedsNoResults',
+            })
+          } else {
+            for (const page of popularFeeds.pages || []) {
+              slices = slices.concat(
+                page.feeds
+                  .filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
+                  .map(feed => ({
+                    key: `popularFeed:${feed.uri}`,
+                    type: 'popularFeed',
+                    feedUri: feed.uri,
+                  })),
+              )
+            }
+
+            if (isPopularFeedsFetchingNextPage) {
+              slices.push({
+                key: 'popularFeedsLoadingMore',
+                type: 'popularFeedsLoadingMore',
+              })
+            }
+          }
+        }
+      }
+    }
+
+    return slices
+  }, [
+    hasSession,
+    preferences,
+    isPreferencesLoading,
+    preferencesError,
+    popularFeeds,
+    isPopularFeedsFetching,
+    popularFeedsError,
+    isPopularFeedsFetchingNextPage,
+    searchResults,
+    isSearchPending,
+    searchError,
+    isUserSearching,
+  ])
+
+  const renderHeaderBtn = React.useCallback(() => {
+    return (
+      <Link
+        href="/settings/saved-feeds"
+        hitSlop={10}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Edit Saved Feeds`)}
+        accessibilityHint="Opens screen to edit Saved Feeds">
+        <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
+      </Link>
+    )
+  }, [pal, _])
+
+  const renderItem = React.useCallback(
+    ({item}: {item: FlatlistSlice}) => {
+      if (item.type === 'error') {
+        return <ErrorMessage message={item.error} />
+      } else if (
+        item.type === 'popularFeedsLoadingMore' ||
+        item.type === 'savedFeedsLoading'
+      ) {
+        return (
+          <View style={s.p10}>
+            <ActivityIndicator />
+          </View>
+        )
+      } else if (item.type === 'savedFeedsHeader') {
+        if (!isMobile) {
           return (
             <View
-              style={{
-                paddingHorizontal: 16,
-                paddingTop: 10,
-                paddingBottom: '150%',
-              }}>
-              <Text type="lg" style={pal.textLight}>
-                No results found for "{query}"
+              style={[
+                pal.view,
+                styles.header,
+                pal.border,
+                {
+                  borderBottomWidth: 1,
+                },
+              ]}>
+              <Text type="title-lg" style={[pal.text, s.bold]}>
+                <Trans>My Feeds</Trans>
               </Text>
+              <Link
+                href="/settings/saved-feeds"
+                accessibilityLabel={_(msg`Edit My Feeds`)}
+                accessibilityHint="">
+                <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+              </Link>
             </View>
           )
         }
-        return null
-      },
-      [isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
-    )
+        return <View />
+      } else if (item.type === 'savedFeedNoResults') {
+        return (
+          <View
+            style={{
+              paddingHorizontal: 16,
+              paddingTop: 10,
+            }}>
+            <Text type="lg" style={pal.textLight}>
+              <Trans>You don't have any saved feeds!</Trans>
+            </Text>
+          </View>
+        )
+      } else if (item.type === 'savedFeed') {
+        return <SavedFeed feedUri={item.feedUri} />
+      } else if (item.type === 'popularFeedsHeader') {
+        return (
+          <>
+            <View
+              style={[
+                pal.view,
+                styles.header,
+                {
+                  // This is first in the flatlist without a session -esb
+                  marginTop: hasSession ? 16 : 0,
+                  paddingLeft: isMobile ? 12 : undefined,
+                  paddingRight: 10,
+                  paddingBottom: isMobile ? 6 : undefined,
+                },
+              ]}>
+              <Text type="title-lg" style={[pal.text, s.bold]}>
+                <Trans>Discover new feeds</Trans>
+              </Text>
 
-    return (
-      <View style={[pal.view, styles.container]}>
-        {isMobile && (
-          <ViewHeader
-            title="Feeds"
-            canGoBack={false}
-            renderButton={renderHeaderBtn}
-            showBorder
+              {!isMobile && (
+                <SearchInput
+                  query={query}
+                  onChangeQuery={onChangeQuery}
+                  onPressCancelSearch={onPressCancelSearch}
+                  onSubmitQuery={onSubmitQuery}
+                  style={{flex: 1, maxWidth: 250}}
+                />
+              )}
+            </View>
+
+            {isMobile && (
+              <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
+                <SearchInput
+                  query={query}
+                  onChangeQuery={onChangeQuery}
+                  onPressCancelSearch={onPressCancelSearch}
+                  onSubmitQuery={onSubmitQuery}
+                />
+              </View>
+            )}
+          </>
+        )
+      } else if (item.type === 'popularFeedsLoading') {
+        return <FeedFeedLoadingPlaceholder />
+      } else if (item.type === 'popularFeed') {
+        return (
+          <FeedSourceCard
+            feedUri={item.feedUri}
+            showSaveBtn={hasSession}
+            showDescription
+            showLikes
+            pinOnSave
           />
-        )}
+        )
+      } else if (item.type === 'popularFeedsNoResults') {
+        return (
+          <View
+            style={{
+              paddingHorizontal: 16,
+              paddingTop: 10,
+              paddingBottom: '150%',
+            }}>
+            <Text type="lg" style={pal.textLight}>
+              <Trans>No results found for "{query}"</Trans>
+            </Text>
+          </View>
+        )
+      }
+      return null
+    },
+    [
+      _,
+      hasSession,
+      isMobile,
+      pal,
+      query,
+      onChangeQuery,
+      onPressCancelSearch,
+      onSubmitQuery,
+    ],
+  )
 
-        <FlatList
-          style={[!isTabletOrDesktop && s.flex1, styles.list]}
-          data={myFeeds.items}
-          keyExtractor={item => item._reactKey}
-          contentContainerStyle={styles.contentContainer}
-          refreshControl={
-            <RefreshControl
-              refreshing={myFeeds.isRefreshing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-            />
-          }
-          renderItem={renderItem}
-          initialNumToRender={10}
-          onEndReached={() => myFeeds.loadMore()}
-          extraData={myFeeds.isLoading}
-          // @ts-ignore our .web version only -prf
-          desktopFixedHeight
+  return (
+    <View style={[pal.view, styles.container]}>
+      {isMobile && (
+        <ViewHeader
+          title={_(msg`Feeds`)}
+          canGoBack={false}
+          renderButton={renderHeaderBtn}
+          showBorder
         />
+      )}
+
+      {preferences ? <View /> : <ActivityIndicator />}
+
+      <FlatList
+        style={[!isTabletOrDesktop && s.flex1, styles.list]}
+        data={items}
+        keyExtractor={item => item.key}
+        contentContainerStyle={styles.contentContainer}
+        renderItem={renderItem}
+        refreshControl={
+          <RefreshControl
+            refreshing={isPTR}
+            onRefresh={isUserSearching ? undefined : onPullToRefresh}
+            tintColor={pal.colors.text}
+            titleColor={pal.colors.text}
+          />
+        }
+        initialNumToRender={10}
+        onEndReached={onEndReached}
+        // @ts-ignore our .web version only -prf
+        desktopFixedHeight
+      />
+
+      {hasSession && (
         <FAB
           testID="composeFAB"
           onPress={onPressCompose}
           icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
           accessibilityRole="button"
-          accessibilityLabel="New post"
+          accessibilityLabel={_(msg`New post`)}
           accessibilityHint=""
         />
-      </View>
-    )
-  }),
-)
+      )}
+    </View>
+  )
+}
 
-function SavedFeed({feed}: {feed: FeedSourceModel}) {
+function SavedFeed({feedUri}: {feedUri: string}) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
+  const {data: info, error} = useFeedSourceInfoQuery({uri: feedUri})
+
+  if (!info)
+    return (
+      <SavedFeedLoadingPlaceholder
+        key={`savedFeedLoadingPlaceholder:${feedUri}`}
+      />
+    )
+
   return (
     <Link
-      testID={`saved-feed-${feed.displayName}`}
-      href={feed.href}
+      testID={`saved-feed-${info.displayName}`}
+      href={info.route.href}
       style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]}
       hoverStyle={pal.viewLight}
-      accessibilityLabel={feed.displayName}
+      accessibilityLabel={info.displayName}
       accessibilityHint=""
       asAnchor
       anchorNoUnderline>
-      {feed.error ? (
+      {error ? (
         <View
           style={{width: 28, flexDirection: 'row', justifyContent: 'center'}}>
           <FontAwesomeIcon
@@ -279,17 +546,17 @@ function SavedFeed({feed}: {feed: FeedSourceModel}) {
           />
         </View>
       ) : (
-        <UserAvatar type="algo" size={28} avatar={feed.avatar} />
+        <UserAvatar type="algo" size={28} avatar={info.avatar} />
       )}
       <View
         style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
         <Text type="lg-medium" style={pal.text} numberOfLines={1}>
-          {feed.displayName}
+          {info.displayName}
         </Text>
-        {feed.error ? (
+        {error ? (
           <View style={[styles.offlineSlug, pal.borderDark]}>
             <Text type="xs" style={pal.textLight}>
-              Feed offline
+              <Trans>Feed offline</Trans>
             </Text>
           </View>
         ) : null}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index c58175327..e8001e973 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,154 +1,178 @@
 import React from 'react'
-import {useWindowDimensions} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
-import isEqual from 'lodash.isequal'
+import {View, ActivityIndicator, StyleSheet} from 'react-native'
+import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
 import {FeedsTabBar} from '../com/pager/FeedsTabBar'
-import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {useStores} from 'state/index'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {FeedPage} from 'view/com/feeds/FeedPage'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-
-export const POLL_FREQ = 30e3 // 30sec
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
+import {emitSoftReset} from '#/state/events'
+import {useSession} from '#/state/session'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
-export const HomeScreen = withAuthRequired(
-  observer(function HomeScreenImpl({}: Props) {
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-    const pagerRef = React.useRef<PagerRef>(null)
-    const [selectedPage, setSelectedPage] = React.useState(0)
-    const [customFeeds, setCustomFeeds] = React.useState<PostsFeedModel[]>([])
-    const [requestedCustomFeeds, setRequestedCustomFeeds] = React.useState<
-      string[]
-    >([])
+export function HomeScreen(props: Props) {
+  const {data: preferences} = usePreferencesQuery()
+
+  if (preferences) {
+    return <HomeScreenReady {...props} preferences={preferences} />
+  } else {
+    return (
+      <View style={styles.loading}>
+        <ActivityIndicator size="large" />
+      </View>
+    )
+  }
+}
+
+function HomeScreenReady({
+  preferences,
+}: Props & {
+  preferences: UsePreferencesQueryResponse
+}) {
+  const {hasSession} = useSession()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const [selectedPage, setSelectedPage] = React.useState(0)
+  const isPageFocused = useIsFocused()
 
-    React.useEffect(() => {
-      const pinned = store.preferences.pinnedFeeds
+  /**
+   * Used to ensure that we re-compute `customFeeds` AND force a re-render of
+   * the pager with the new order of feeds.
+   */
+  const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
 
-      if (isEqual(pinned, requestedCustomFeeds)) {
-        // no changes
-        return
+  const customFeeds = React.useMemo(() => {
+    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}`)
       }
+    }
+    return feeds
+  }, [preferences.feeds.pinned])
 
-      const feeds = []
-      for (const uri of pinned) {
-        if (uri.includes('app.bsky.feed.generator')) {
-          const model = new PostsFeedModel(store, 'custom', {feed: uri})
-          feeds.push(model)
-        } else if (uri.includes('app.bsky.graph.list')) {
-          const model = new PostsFeedModel(store, 'list', {list: uri})
-          feeds.push(model)
-        }
+  const homeFeedParams = React.useMemo<FeedParams>(() => {
+    return {
+      mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
+      mergeFeedSources: preferences.feeds.saved,
+    }
+  }, [preferences])
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      setDrawerSwipeDisabled(selectedPage > 0)
+      return () => {
+        setDrawerSwipeDisabled(false)
       }
-      pagerRef.current?.setPage(0)
-      setCustomFeeds(feeds)
-      setRequestedCustomFeeds(pinned)
-    }, [
-      store,
-      store.preferences.pinnedFeeds,
-      customFeeds,
-      setCustomFeeds,
-      pagerRef,
-      requestedCustomFeeds,
-      setRequestedCustomFeeds,
-    ])
+    }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]),
+  )
 
-    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 onPageSelected = React.useCallback(
-      (index: number) => {
+  const onPageScrollStateChanged = React.useCallback(
+    (state: 'idle' | 'dragging' | 'settling') => {
+      if (state === 'dragging') {
         setMinimalShellMode(false)
-        setSelectedPage(index)
-        setDrawerSwipeDisabled(index > 0)
-      },
-      [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode],
-    )
+      }
+    },
+    [setMinimalShellMode],
+  )
 
-    const onPressSelected = React.useCallback(() => {
-      store.emitScreenSoftReset()
-    }, [store])
+  const renderTabBar = React.useCallback(
+    (props: RenderTabBarFnProps) => {
+      return (
+        <FeedsTabBar
+          key="FEEDS_TAB_BAR"
+          selectedPage={props.selectedPage}
+          onSelect={props.onSelect}
+          testID="homeScreenFeedTabs"
+          onPressSelected={onPressSelected}
+        />
+      )
+    },
+    [onPressSelected],
+  )
 
-    const renderTabBar = React.useCallback(
-      (props: RenderTabBarFnProps) => {
+  const renderFollowingEmptyState = React.useCallback(() => {
+    return <FollowingEmptyState />
+  }, [])
+
+  const renderCustomFeedEmptyState = React.useCallback(() => {
+    return <CustomFeedEmptyState />
+  }, [])
+
+  return hasSession ? (
+    <Pager
+      key={pinnedFeedOrderKey}
+      testID="homeScreen"
+      onPageSelected={onPageSelected}
+      onPageScrollStateChanged={onPageScrollStateChanged}
+      renderTabBar={renderTabBar}
+      tabBarPosition="top">
+      <FeedPage
+        key="1"
+        testID="followingFeedPage"
+        isPageFocused={selectedPage === 0 && isPageFocused}
+        feed={homeFeedParams.mergeFeedEnabled ? 'home' : 'following'}
+        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 && isPageFocused}
+            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}
-        renderTabBar={renderTabBar}
-        tabBarPosition="top">
-        <FeedPage
-          key="1"
-          testID="followingFeedPage"
-          isPageFocused={selectedPage === 0}
-          feed={store.me.mainFeed}
-          renderEmptyState={renderFollowingEmptyState}
-          renderEndOfFeed={FollowingEndOfFeed}
-        />
-        {customFeeds.map((f, index) => {
-          return (
-            <FeedPage
-              key={f.reactKey}
-              testID="customFeedPage"
-              isPageFocused={selectedPage === 1 + index}
-              feed={f}
-              renderEmptyState={renderCustomFeedEmptyState}
-            />
-          )
-        })}
-      </Pager>
-    )
-  }),
-)
-
-export function useHeaderOffset() {
-  const {isDesktop, isTablet} = useWebMediaQueries()
-  const {fontScale} = useWindowDimensions()
-  if (isDesktop) {
-    return 0
-  }
-  if (isTablet) {
-    return 50
-  }
-  // default text takes 44px, plus 34px of pad
-  // scale the 44px by the font scale
-  return 34 + 44 * fontScale
+      })}
+    </Pager>
+  ) : (
+    <Pager
+      testID="homeScreen"
+      onPageSelected={onPageSelected}
+      onPageScrollStateChanged={onPageScrollStateChanged}
+      renderTabBar={renderTabBar}
+      tabBarPosition="top">
+      <FeedPage
+        testID="customFeedPage"
+        isPageFocused={isPageFocused}
+        feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`}
+        renderEmptyState={renderCustomFeedEmptyState}
+      />
+    </Pager>
+  )
 }
+
+const styles = StyleSheet.create({
+  loading: {
+    height: '100%',
+    alignContent: 'center',
+    justifyContent: 'center',
+    paddingBottom: 100,
+  },
+})
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index a68a3b5e3..7a2e54dc8 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -1,8 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -16,20 +14,25 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
-import {LANGUAGES} from 'lib/../locale/languages'
+import {APP_LANGUAGES, LANGUAGES} from 'lib/../locale/languages'
 import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
 
-export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
-  _: Props,
-) {
+export function LanguageSettingsScreen(_props: Props) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
   const {isTabletOrDesktop} = useWebMediaQueries()
   const {screen, track} = useAnalytics()
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {openModal} = useModalControls()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -40,26 +43,37 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
 
   const onPressContentLanguages = React.useCallback(() => {
     track('Settings:ContentlanguagesButtonClicked')
-    store.shell.openModal({name: 'content-languages-settings'})
-  }, [track, store])
+    openModal({name: 'content-languages-settings'})
+  }, [track, openModal])
 
   const onChangePrimaryLanguage = React.useCallback(
     (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
-      store.preferences.setPrimaryLanguage(value)
+      if (langPrefs.primaryLanguage !== value) {
+        setLangPrefs.setPrimaryLanguage(value)
+      }
     },
-    [store.preferences],
+    [langPrefs, setLangPrefs],
+  )
+
+  const onChangeAppLanguage = React.useCallback(
+    (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
+      if (langPrefs.appLanguage !== value) {
+        setLangPrefs.setAppLanguage(value)
+      }
+    },
+    [langPrefs, setLangPrefs],
   )
 
   const myLanguages = React.useMemo(() => {
     return (
-      store.preferences.contentLanguages
+      langPrefs.contentLanguages
         .map(lang => LANGUAGES.find(l => l.code2 === lang))
         .filter(Boolean)
         // @ts-ignore
         .map(l => l.name)
         .join(', ')
     )
-  }, [store.preferences.contentLanguages])
+  }, [langPrefs.contentLanguages])
 
   return (
     <CenteredView
@@ -69,20 +83,114 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
         styles.container,
         isTabletOrDesktop && styles.desktopContainer,
       ]}>
-      <ViewHeader title="Language Settings" showOnDesktop />
+      <ViewHeader title={_(msg`Language Settings`)} showOnDesktop />
 
       <View style={{paddingTop: 20, paddingHorizontal: 20}}>
+        {/* APP LANGUAGE */}
+        <View style={{paddingBottom: 20}}>
+          <Text type="title-sm" style={[pal.text, s.pb5]}>
+            <Trans>App Language</Trans>
+          </Text>
+          <Text style={[pal.text, s.pb10]}>
+            <Trans>
+              Select your app language for the default text to display in the
+              app
+            </Trans>
+          </Text>
+
+          <View style={{position: 'relative'}}>
+            <RNPickerSelect
+              value={langPrefs.appLanguage}
+              onValueChange={onChangeAppLanguage}
+              items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
+                label: l.name,
+                value: l.code2,
+                key: l.code2,
+              }))}
+              style={{
+                inputAndroid: {
+                  backgroundColor: pal.viewLight.backgroundColor,
+                  color: pal.text.color,
+                  fontSize: 14,
+                  letterSpacing: 0.5,
+                  fontWeight: '500',
+                  paddingHorizontal: 14,
+                  paddingVertical: 8,
+                  borderRadius: 24,
+                },
+                inputIOS: {
+                  backgroundColor: pal.viewLight.backgroundColor,
+                  color: pal.text.color,
+                  fontSize: 14,
+                  letterSpacing: 0.5,
+                  fontWeight: '500',
+                  paddingHorizontal: 14,
+                  paddingVertical: 8,
+                  borderRadius: 24,
+                },
+                inputWeb: {
+                  // @ts-ignore web only
+                  cursor: 'pointer',
+                  '-moz-appearance': 'none',
+                  '-webkit-appearance': 'none',
+                  appearance: 'none',
+                  outline: 0,
+                  borderWidth: 0,
+                  backgroundColor: pal.viewLight.backgroundColor,
+                  color: pal.text.color,
+                  fontSize: 14,
+                  letterSpacing: 0.5,
+                  fontWeight: '500',
+                  paddingHorizontal: 14,
+                  paddingVertical: 8,
+                  borderRadius: 24,
+                },
+              }}
+            />
+
+            <View
+              style={{
+                position: 'absolute',
+                top: 1,
+                right: 1,
+                bottom: 1,
+                width: 40,
+                backgroundColor: pal.viewLight.backgroundColor,
+                borderRadius: 24,
+                pointerEvents: 'none',
+                alignItems: 'center',
+                justifyContent: 'center',
+              }}>
+              <FontAwesomeIcon
+                icon="chevron-down"
+                style={pal.text as FontAwesomeIconStyle}
+              />
+            </View>
+          </View>
+        </View>
+
+        <View
+          style={{
+            height: 1,
+            backgroundColor: pal.border.borderColor,
+            marginBottom: 20,
+          }}
+        />
+
+        {/* PRIMARY LANGUAGE */}
         <View style={{paddingBottom: 20}}>
           <Text type="title-sm" style={[pal.text, s.pb5]}>
-            Primary Language
+            <Trans>Primary Language</Trans>
           </Text>
           <Text style={[pal.text, s.pb10]}>
-            Select your preferred language for translations in your feed.
+            <Trans>
+              Select your preferred language for translations in your feed.
+            </Trans>
           </Text>
 
           <View style={{position: 'relative'}}>
             <RNPickerSelect
-              value={store.preferences.primaryLanguage}
+              value={langPrefs.primaryLanguage}
               onValueChange={onChangePrimaryLanguage}
               items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
                 label: l.name,
@@ -159,13 +267,16 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
           }}
         />
 
+        {/* CONTENT LANGUAGES */}
         <View style={{paddingBottom: 20}}>
           <Text type="title-sm" style={[pal.text, s.pb5]}>
-            Content Languages
+            <Trans>Content Languages</Trans>
           </Text>
           <Text style={[pal.text, s.pb10]}>
-            Select which languages you want your subscribed feeds to include. If
-            none are selected, all languages will be shown.
+            <Trans>
+              Select which languages you want your subscribed feeds to include.
+              If none are selected, all languages will be shown.
+            </Trans>
           </Text>
 
           <Button
@@ -187,7 +298,7 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index a64b0ca3b..d28db7c6c 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -3,12 +3,8 @@ import {View} from 'react-native'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {AtUri} from '@atproto/api'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {useStores} from 'state/index'
-import {ListsListModel} from 'state/models/lists/lists-list'
-import {ListsList} from 'view/com/lists/ListsList'
+import {MyLists} from '#/view/com/lists/MyLists'
 import {Text} from 'view/com/util/text/Text'
 import {Button} from 'view/com/util/forms/Button'
 import {NavigationProp} from 'lib/routes/types'
@@ -17,78 +13,72 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {Trans} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
-export const ListsScreen = withAuthRequired(
-  observer(function ListsScreenImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {isMobile} = useWebMediaQueries()
-    const navigation = useNavigation<NavigationProp>()
+export function ListsScreen({}: Props) {
+  const pal = usePalette('default')
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {isMobile} = useWebMediaQueries()
+  const navigation = useNavigation<NavigationProp>()
+  const {openModal} = useModalControls()
 
-    const listsLists: ListsListModel = React.useMemo(
-      () => new ListsListModel(store, 'my-curatelists'),
-      [store],
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
 
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        listsLists.refresh()
-      }, [listsLists, setMinimalShellMode]),
-    )
+  const onPressNewList = React.useCallback(() => {
+    openModal({
+      name: 'create-or-edit-list',
+      purpose: 'app.bsky.graph.defs#curatelist',
+      onSave: (uri: string) => {
+        try {
+          const urip = new AtUri(uri)
+          navigation.navigate('ProfileList', {
+            name: urip.hostname,
+            rkey: urip.rkey,
+          })
+        } catch {}
+      },
+    })
+  }, [openModal, navigation])
 
-    const onPressNewList = React.useCallback(() => {
-      store.shell.openModal({
-        name: 'create-or-edit-list',
-        purpose: 'app.bsky.graph.defs#curatelist',
-        onSave: (uri: string) => {
-          try {
-            const urip = new AtUri(uri)
-            navigation.navigate('ProfileList', {
-              name: urip.hostname,
-              rkey: urip.rkey,
-            })
-          } catch {}
-        },
-      })
-    }, [store, navigation])
-
-    return (
-      <View style={s.hContentRegion} testID="listsScreen">
-        <SimpleViewHeader
-          showBackButton={isMobile}
-          style={
-            !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
-          }>
-          <View style={{flex: 1}}>
-            <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
-              User Lists
-            </Text>
-            <Text style={pal.textLight}>
-              Public, shareable lists which can drive feeds.
+  return (
+    <View style={s.hContentRegion} testID="listsScreen">
+      <SimpleViewHeader
+        showBackButton={isMobile}
+        style={
+          !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
+        }>
+        <View style={{flex: 1}}>
+          <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
+            <Trans>User Lists</Trans>
+          </Text>
+          <Text style={pal.textLight}>
+            <Trans>Public, shareable lists which can drive feeds.</Trans>
+          </Text>
+        </View>
+        <View>
+          <Button
+            testID="newUserListBtn"
+            type="default"
+            onPress={onPressNewList}
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              gap: 8,
+            }}>
+            <FontAwesomeIcon icon="plus" color={pal.colors.text} />
+            <Text type="button" style={pal.text}>
+              <Trans>New</Trans>
             </Text>
-          </View>
-          <View>
-            <Button
-              testID="newUserListBtn"
-              type="default"
-              onPress={onPressNewList}
-              style={{
-                flexDirection: 'row',
-                alignItems: 'center',
-                gap: 8,
-              }}>
-              <FontAwesomeIcon icon="plus" color={pal.colors.text} />
-              <Text type="button" style={pal.text}>
-                New
-              </Text>
-            </Button>
-          </View>
-        </SimpleViewHeader>
-        <ListsList listsList={listsLists} style={s.flexGrow1} />
-      </View>
-    )
-  }),
-)
+          </Button>
+        </View>
+      </SimpleViewHeader>
+      <MyLists filter="curate" style={s.flexGrow1} />
+    </View>
+  )
+}
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index f524279a5..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'
@@ -11,13 +10,16 @@ import {Text} from '../com/util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {getEntries} from '#/logger/logDump'
 import {ago} from 'lib/strings/time'
+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'
 >) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const [expanded, setExpanded] = React.useState<string[]>([])
 
@@ -47,7 +49,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
                 <TouchableOpacity
                   style={[styles.entry, pal.border, pal.view]}
                   onPress={toggler(entry.id)}
-                  accessibilityLabel="View debug entry"
+                  accessibilityLabel={_(msg`View debug entry`)}
                   accessibilityHint="Opens additional details for a debug entry">
                   {entry.level === 'debug' ? (
                     <FontAwesomeIcon icon="info" />
@@ -85,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 142f3bce8..4d8d8cad7 100644
--- a/src/view/screens/Moderation.tsx
+++ b/src/view/screens/Moderation.tsx
@@ -5,10 +5,7 @@ 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 {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {CenteredView} from '../com/util/Views'
 import {ViewHeader} from '../com/util/ViewHeader'
@@ -18,101 +15,103 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
-export const ModerationScreen = withAuthRequired(
-  observer(function Moderation({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {screen, track} = useAnalytics()
-    const {isTabletOrDesktop} = useWebMediaQueries()
+export function ModerationScreen({}: Props) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {screen, track} = useAnalytics()
+  const {isTabletOrDesktop} = useWebMediaQueries()
+  const {openModal} = useModalControls()
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('Moderation')
-        setMinimalShellMode(false)
-      }, [screen, setMinimalShellMode]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('Moderation')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    const onPressContentFiltering = React.useCallback(() => {
-      track('Moderation:ContentfilteringButtonClicked')
-      store.shell.openModal({name: 'content-filtering-settings'})
-    }, [track, store])
+  const onPressContentFiltering = React.useCallback(() => {
+    track('Moderation:ContentfilteringButtonClicked')
+    openModal({name: 'content-filtering-settings'})
+  }, [track, openModal])
 
-    return (
-      <CenteredView
-        style={[
-          s.hContentRegion,
-          pal.border,
-          isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
-        ]}
-        testID="moderationScreen">
-        <ViewHeader title="Moderation" showOnDesktop />
-        <View style={styles.spacer} />
-        <TouchableOpacity
-          testID="contentFilteringBtn"
-          style={[styles.linkCard, pal.view]}
-          onPress={onPressContentFiltering}
-          accessibilityRole="tab"
-          accessibilityHint="Content filtering"
-          accessibilityLabel="">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="eye"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Content filtering
-          </Text>
-        </TouchableOpacity>
-        <Link
-          testID="moderationlistsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/modlists">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="users-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Moderation lists
-          </Text>
-        </Link>
-        <Link
-          testID="mutedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/muted-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="user-slash"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Muted accounts
-          </Text>
-        </Link>
-        <Link
-          testID="blockedAccountsBtn"
-          style={[styles.linkCard, pal.view]}
-          href="/moderation/blocked-accounts">
-          <View style={[styles.iconContainer, pal.btn]}>
-            <FontAwesomeIcon
-              icon="ban"
-              style={pal.text as FontAwesomeIconStyle}
-            />
-          </View>
-          <Text type="lg" style={pal.text}>
-            Blocked accounts
-          </Text>
-        </Link>
-      </CenteredView>
-    )
-  }),
-)
+  return (
+    <CenteredView
+      style={[
+        s.hContentRegion,
+        pal.border,
+        isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
+      ]}
+      testID="moderationScreen">
+      <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
+      <View style={styles.spacer} />
+      <TouchableOpacity
+        testID="contentFilteringBtn"
+        style={[styles.linkCard, pal.view]}
+        onPress={onPressContentFiltering}
+        accessibilityRole="tab"
+        accessibilityHint="Content filtering"
+        accessibilityLabel="">
+        <View style={[styles.iconContainer, pal.btn]}>
+          <FontAwesomeIcon
+            icon="eye"
+            style={pal.text as FontAwesomeIconStyle}
+          />
+        </View>
+        <Text type="lg" style={pal.text}>
+          <Trans>Content filtering</Trans>
+        </Text>
+      </TouchableOpacity>
+      <Link
+        testID="moderationlistsBtn"
+        style={[styles.linkCard, pal.view]}
+        href="/moderation/modlists">
+        <View style={[styles.iconContainer, pal.btn]}>
+          <FontAwesomeIcon
+            icon="users-slash"
+            style={pal.text as FontAwesomeIconStyle}
+          />
+        </View>
+        <Text type="lg" style={pal.text}>
+          <Trans>Moderation lists</Trans>
+        </Text>
+      </Link>
+      <Link
+        testID="mutedAccountsBtn"
+        style={[styles.linkCard, pal.view]}
+        href="/moderation/muted-accounts">
+        <View style={[styles.iconContainer, pal.btn]}>
+          <FontAwesomeIcon
+            icon="user-slash"
+            style={pal.text as FontAwesomeIconStyle}
+          />
+        </View>
+        <Text type="lg" style={pal.text}>
+          <Trans>Muted accounts</Trans>
+        </Text>
+      </Link>
+      <Link
+        testID="blockedAccountsBtn"
+        style={[styles.linkCard, pal.view]}
+        href="/moderation/blocked-accounts">
+        <View style={[styles.iconContainer, pal.btn]}>
+          <FontAwesomeIcon
+            icon="ban"
+            style={pal.text as FontAwesomeIconStyle}
+          />
+        </View>
+        <Text type="lg" style={pal.text}>
+          <Trans>Blocked accounts</Trans>
+        </Text>
+      </Link>
+    </CenteredView>
+  )
+}
 
 const styles = StyleSheet.create({
   desktopContainer: {
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index 0dc3b706b..8f6e2f729 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {
   ActivityIndicator,
   FlatList,
@@ -8,133 +8,165 @@ import {
 } from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {BlockedAccountsModel} from 'state/models/lists/blocked-accounts'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
+import {cleanError} from '#/lib/strings/errors'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationBlockedAccounts'
 >
-export const ModerationBlockedAccounts = withAuthRequired(
-  observer(function ModerationBlockedAccountsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {isTabletOrDesktop} = useWebMediaQueries()
-    const {screen} = useAnalytics()
-    const blockedAccounts = useMemo(
-      () => new BlockedAccountsModel(store),
-      [store],
-    )
+export function ModerationBlockedAccounts({}: Props) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {isTabletOrDesktop} = useWebMediaQueries()
+  const {screen} = useAnalytics()
+  const [isPTRing, setIsPTRing] = React.useState(false)
+  const {
+    data,
+    isFetching,
+    isError,
+    error,
+    refetch,
+    hasNextPage,
+    fetchNextPage,
+    isFetchingNextPage,
+  } = useMyBlockedAccountsQuery()
+  const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
+  const profiles = React.useMemo(() => {
+    if (data?.pages) {
+      return data.pages.flatMap(page => page.blocks)
+    }
+    return []
+  }, [data])
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('BlockedAccounts')
-        setMinimalShellMode(false)
-        blockedAccounts.refresh()
-      }, [screen, setMinimalShellMode, blockedAccounts]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('BlockedAccounts')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    const onRefresh = React.useCallback(() => {
-      blockedAccounts.refresh()
-    }, [blockedAccounts])
-    const onEndReached = React.useCallback(() => {
-      blockedAccounts
-        .loadMore()
-        .catch(err =>
-          logger.error('Failed to load more blocked accounts', {error: err}),
-        )
-    }, [blockedAccounts])
+  const onRefresh = React.useCallback(async () => {
+    setIsPTRing(true)
+    try {
+      await refetch()
+    } catch (err) {
+      logger.error('Failed to refresh my muted accounts', {error: err})
+    }
+    setIsPTRing(false)
+  }, [refetch, setIsPTRing])
 
-    const renderItem = ({
-      item,
-      index,
-    }: {
-      item: ActorDefs.ProfileView
-      index: number
-    }) => (
-      <ProfileCard
-        testID={`blockedAccount-${index}`}
-        key={item.did}
-        profile={item}
-      />
-    )
-    return (
-      <CenteredView
+  const onEndReached = React.useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
+
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more of my muted accounts', {error: err})
+    }
+  }, [isFetching, hasNextPage, isError, fetchNextPage])
+
+  const renderItem = ({
+    item,
+    index,
+  }: {
+    item: ActorDefs.ProfileView
+    index: number
+  }) => (
+    <ProfileCard
+      testID={`blockedAccount-${index}`}
+      key={item.did}
+      profile={item}
+    />
+  )
+  return (
+    <CenteredView
+      style={[
+        styles.container,
+        isTabletOrDesktop && styles.containerDesktop,
+        pal.view,
+        pal.border,
+      ]}
+      testID="blockedAccountsScreen">
+      <ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
+      <Text
+        type="sm"
         style={[
-          styles.container,
-          isTabletOrDesktop && styles.containerDesktop,
-          pal.view,
-          pal.border,
-        ]}
-        testID="blockedAccountsScreen">
-        <ViewHeader title="Blocked Accounts" showOnDesktop />
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-          ]}>
+          styles.description,
+          pal.text,
+          isTabletOrDesktop && styles.descriptionDesktop,
+        ]}>
+        <Trans>
           Blocked accounts cannot reply in your threads, mention you, or
           otherwise interact with you. You will not see their content and they
           will be prevented from seeing yours.
-        </Text>
-        {!blockedAccounts.hasContent ? (
-          <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+        </Trans>
+      </Text>
+      {isEmpty ? (
+        <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+          {isError ? (
+            <ErrorScreen
+              title="Oops!"
+              message={cleanError(error)}
+              onPressTryAgain={refetch}
+            />
+          ) : (
             <View style={[styles.empty, pal.viewLight]}>
               <Text type="lg" style={[pal.text, styles.emptyText]}>
-                You have not blocked any accounts yet. To block an account, go
-                to their profile and selected "Block account" from the menu on
-                their account.
+                <Trans>
+                  You have not blocked any accounts yet. To block an account, go
+                  to their profile and selected "Block account" from the menu on
+                  their account.
+                </Trans>
               </Text>
             </View>
-          </View>
-        ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
-            data={blockedAccounts.blocks}
-            keyExtractor={(item: ActorDefs.ProfileView) => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={blockedAccounts.isRefreshing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
-            onEndReached={onEndReached}
-            renderItem={renderItem}
-            initialNumToRender={15}
-            // FIXME(dan)
-            // eslint-disable-next-line react/no-unstable-nested-components
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {blockedAccounts.isLoading && <ActivityIndicator />}
-              </View>
-            )}
-            extraData={blockedAccounts.isLoading}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
-          />
-        )}
-      </CenteredView>
-    )
-  }),
-)
+          )}
+        </View>
+      ) : (
+        <FlatList
+          style={[!isTabletOrDesktop && styles.flex1]}
+          data={profiles}
+          keyExtractor={(item: ActorDefs.ProfileView) => item.did}
+          refreshControl={
+            <RefreshControl
+              refreshing={isPTRing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          onEndReached={onEndReached}
+          renderItem={renderItem}
+          initialNumToRender={15}
+          // FIXME(dan)
+
+          ListFooterComponent={() => (
+            <View style={styles.footer}>
+              {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
+            </View>
+          )}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
+      )}
+    </CenteredView>
+  )
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx
index 8794c6d17..145b35a42 100644
--- a/src/view/screens/ModerationModlists.tsx
+++ b/src/view/screens/ModerationModlists.tsx
@@ -3,12 +3,8 @@ import {View} from 'react-native'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {AtUri} from '@atproto/api'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {useStores} from 'state/index'
-import {ListsListModel} from 'state/models/lists/lists-list'
-import {ListsList} from 'view/com/lists/ListsList'
+import {MyLists} from '#/view/com/lists/MyLists'
 import {Text} from 'view/com/util/text/Text'
 import {Button} from 'view/com/util/forms/Button'
 import {NavigationProp} from 'lib/routes/types'
@@ -17,78 +13,71 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
-export const ModerationModlistsScreen = withAuthRequired(
-  observer(function ModerationModlistsScreenImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {isMobile} = useWebMediaQueries()
-    const navigation = useNavigation<NavigationProp>()
+export function ModerationModlistsScreen({}: Props) {
+  const pal = usePalette('default')
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {isMobile} = useWebMediaQueries()
+  const navigation = useNavigation<NavigationProp>()
+  const {openModal} = useModalControls()
 
-    const mutelists: ListsListModel = React.useMemo(
-      () => new ListsListModel(store, 'my-modlists'),
-      [store],
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
 
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        mutelists.refresh()
-      }, [mutelists, setMinimalShellMode]),
-    )
+  const onPressNewList = React.useCallback(() => {
+    openModal({
+      name: 'create-or-edit-list',
+      purpose: 'app.bsky.graph.defs#modlist',
+      onSave: (uri: string) => {
+        try {
+          const urip = new AtUri(uri)
+          navigation.navigate('ProfileList', {
+            name: urip.hostname,
+            rkey: urip.rkey,
+          })
+        } catch {}
+      },
+    })
+  }, [openModal, navigation])
 
-    const onPressNewList = React.useCallback(() => {
-      store.shell.openModal({
-        name: 'create-or-edit-list',
-        purpose: 'app.bsky.graph.defs#modlist',
-        onSave: (uri: string) => {
-          try {
-            const urip = new AtUri(uri)
-            navigation.navigate('ProfileList', {
-              name: urip.hostname,
-              rkey: urip.rkey,
-            })
-          } catch {}
-        },
-      })
-    }, [store, navigation])
-
-    return (
-      <View style={s.hContentRegion} testID="moderationModlistsScreen">
-        <SimpleViewHeader
-          showBackButton={isMobile}
-          style={
-            !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
-          }>
-          <View style={{flex: 1}}>
-            <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
-              Moderation Lists
-            </Text>
-            <Text style={pal.textLight}>
-              Public, shareable lists of users to mute or block in bulk.
+  return (
+    <View style={s.hContentRegion} testID="moderationModlistsScreen">
+      <SimpleViewHeader
+        showBackButton={isMobile}
+        style={
+          !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
+        }>
+        <View style={{flex: 1}}>
+          <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
+            Moderation Lists
+          </Text>
+          <Text style={pal.textLight}>
+            Public, shareable lists of users to mute or block in bulk.
+          </Text>
+        </View>
+        <View>
+          <Button
+            testID="newModListBtn"
+            type="default"
+            onPress={onPressNewList}
+            style={{
+              flexDirection: 'row',
+              alignItems: 'center',
+              gap: 8,
+            }}>
+            <FontAwesomeIcon icon="plus" color={pal.colors.text} />
+            <Text type="button" style={pal.text}>
+              New
             </Text>
-          </View>
-          <View>
-            <Button
-              testID="newModListBtn"
-              type="default"
-              onPress={onPressNewList}
-              style={{
-                flexDirection: 'row',
-                alignItems: 'center',
-                gap: 8,
-              }}>
-              <FontAwesomeIcon icon="plus" color={pal.colors.text} />
-              <Text type="button" style={pal.text}>
-                New
-              </Text>
-            </Button>
-          </View>
-        </SimpleViewHeader>
-        <ListsList listsList={mutelists} style={s.flexGrow1} />
-      </View>
-    )
-  }),
-)
+          </Button>
+        </View>
+      </SimpleViewHeader>
+      <MyLists filter="mod" style={s.flexGrow1} />
+    </View>
+  )
+}
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 2fa27ee54..41aee9f2f 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {
   ActivityIndicator,
   FlatList,
@@ -8,129 +8,164 @@ import {
 } from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {MutedAccountsModel} from 'state/models/lists/muted-accounts'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
+import {cleanError} from '#/lib/strings/errors'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationMutedAccounts'
 >
-export const ModerationMutedAccounts = withAuthRequired(
-  observer(function ModerationMutedAccountsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {isTabletOrDesktop} = useWebMediaQueries()
-    const {screen} = useAnalytics()
-    const mutedAccounts = useMemo(() => new MutedAccountsModel(store), [store])
+export function ModerationMutedAccounts({}: Props) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {isTabletOrDesktop} = useWebMediaQueries()
+  const {screen} = useAnalytics()
+  const [isPTRing, setIsPTRing] = React.useState(false)
+  const {
+    data,
+    isFetching,
+    isError,
+    error,
+    refetch,
+    hasNextPage,
+    fetchNextPage,
+    isFetchingNextPage,
+  } = useMyMutedAccountsQuery()
+  const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
+  const profiles = React.useMemo(() => {
+    if (data?.pages) {
+      return data.pages.flatMap(page => page.mutes)
+    }
+    return []
+  }, [data])
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('MutedAccounts')
-        setMinimalShellMode(false)
-        mutedAccounts.refresh()
-      }, [screen, setMinimalShellMode, mutedAccounts]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('MutedAccounts')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    const onRefresh = React.useCallback(() => {
-      mutedAccounts.refresh()
-    }, [mutedAccounts])
-    const onEndReached = React.useCallback(() => {
-      mutedAccounts
-        .loadMore()
-        .catch(err =>
-          logger.error('Failed to load more muted accounts', {error: err}),
-        )
-    }, [mutedAccounts])
+  const onRefresh = React.useCallback(async () => {
+    setIsPTRing(true)
+    try {
+      await refetch()
+    } catch (err) {
+      logger.error('Failed to refresh my muted accounts', {error: err})
+    }
+    setIsPTRing(false)
+  }, [refetch, setIsPTRing])
 
-    const renderItem = ({
-      item,
-      index,
-    }: {
-      item: ActorDefs.ProfileView
-      index: number
-    }) => (
-      <ProfileCard
-        testID={`mutedAccount-${index}`}
-        key={item.did}
-        profile={item}
-      />
-    )
-    return (
-      <CenteredView
+  const onEndReached = React.useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
+
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more of my muted accounts', {error: err})
+    }
+  }, [isFetching, hasNextPage, isError, fetchNextPage])
+
+  const renderItem = ({
+    item,
+    index,
+  }: {
+    item: ActorDefs.ProfileView
+    index: number
+  }) => (
+    <ProfileCard
+      testID={`mutedAccount-${index}`}
+      key={item.did}
+      profile={item}
+    />
+  )
+  return (
+    <CenteredView
+      style={[
+        styles.container,
+        isTabletOrDesktop && styles.containerDesktop,
+        pal.view,
+        pal.border,
+      ]}
+      testID="mutedAccountsScreen">
+      <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
+      <Text
+        type="sm"
         style={[
-          styles.container,
-          isTabletOrDesktop && styles.containerDesktop,
-          pal.view,
-          pal.border,
-        ]}
-        testID="mutedAccountsScreen">
-        <ViewHeader title="Muted Accounts" showOnDesktop />
-        <Text
-          type="sm"
-          style={[
-            styles.description,
-            pal.text,
-            isTabletOrDesktop && styles.descriptionDesktop,
-          ]}>
+          styles.description,
+          pal.text,
+          isTabletOrDesktop && styles.descriptionDesktop,
+        ]}>
+        <Trans>
           Muted accounts have their posts removed from your feed and from your
           notifications. Mutes are completely private.
-        </Text>
-        {!mutedAccounts.hasContent ? (
-          <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+        </Trans>
+      </Text>
+      {isEmpty ? (
+        <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
+          {isError ? (
+            <ErrorScreen
+              title="Oops!"
+              message={cleanError(error)}
+              onPressTryAgain={refetch}
+            />
+          ) : (
             <View style={[styles.empty, pal.viewLight]}>
               <Text type="lg" style={[pal.text, styles.emptyText]}>
-                You have not muted any accounts yet. To mute an account, go to
-                their profile and selected "Mute account" from the menu on their
-                account.
+                <Trans>
+                  You have not muted any accounts yet. To mute an account, go to
+                  their profile and selected "Mute account" from the menu on
+                  their account.
+                </Trans>
               </Text>
             </View>
-          </View>
-        ) : (
-          <FlatList
-            style={[!isTabletOrDesktop && styles.flex1]}
-            data={mutedAccounts.mutes}
-            keyExtractor={item => item.did}
-            refreshControl={
-              <RefreshControl
-                refreshing={mutedAccounts.isRefreshing}
-                onRefresh={onRefresh}
-                tintColor={pal.colors.text}
-                titleColor={pal.colors.text}
-              />
-            }
-            onEndReached={onEndReached}
-            renderItem={renderItem}
-            initialNumToRender={15}
-            // FIXME(dan)
-            // eslint-disable-next-line react/no-unstable-nested-components
-            ListFooterComponent={() => (
-              <View style={styles.footer}>
-                {mutedAccounts.isLoading && <ActivityIndicator />}
-              </View>
-            )}
-            extraData={mutedAccounts.isLoading}
-            // @ts-ignore our .web version only -prf
-            desktopFixedHeight
-          />
-        )}
-      </CenteredView>
-    )
-  }),
-)
+          )}
+        </View>
+      ) : (
+        <FlatList
+          style={[!isTabletOrDesktop && styles.flex1]}
+          data={profiles}
+          keyExtractor={item => item.did}
+          refreshControl={
+            <RefreshControl
+              refreshing={isPTRing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          onEndReached={onEndReached}
+          renderItem={renderItem}
+          initialNumToRender={15}
+          // FIXME(dan)
+
+          ListFooterComponent={() => (
+            <View style={styles.footer}>
+              {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
+            </View>
+          )}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
+      )}
+    </CenteredView>
+  )
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index c2125756c..2508a9ed2 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -12,9 +12,12 @@ import {NavigationProp} from 'lib/routes/types'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export const NotFoundScreen = () => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
   const setMinimalShellMode = useSetMinimalShellMode()
 
@@ -36,13 +39,15 @@ export const NotFoundScreen = () => {
 
   return (
     <View testID="notFoundView" style={pal.view}>
-      <ViewHeader title="Page not found" />
+      <ViewHeader title={_(msg`Page not found`)} />
       <View style={styles.container}>
         <Text type="title-2xl" style={[pal.text, s.mb10]}>
-          Page not found
+          <Trans>Page not found</Trans>
         </Text>
         <Text type="md" style={[pal.text, s.mb10]}>
-          We're sorry! We can't find the page you were looking for.
+          <Trans>
+            We're sorry! We can't find the page you were looking for.
+          </Trans>
         </Text>
         <Button
           type="primary"
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index cd482bd1c..3ce1128a6 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,166 +1,135 @@
 import React from 'react'
 import {FlatList, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
+import {useQueryClient} from '@tanstack/react-query'
 import {
   NativeStackScreenProps,
   NotificationsTabNavigatorParams,
 } from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
 import {TextLink} from 'view/com/util/Link'
-import {InvitedUsers} from '../com/notifications/InvitedUsers'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {useStores} from 'state/index'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
-import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {isWeb} from 'platform/detection'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {
+  useUnreadNotifications,
+  useUnreadNotificationsApi,
+} from '#/state/queries/notifications/unread'
+import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
+import {listenSoftReset, emitSoftReset} from '#/state/events'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
-export const NotificationsScreen = withAuthRequired(
-  observer(function NotificationsScreenImpl({}: Props) {
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
-    const scrollElRef = React.useRef<FlatList>(null)
-    const {screen} = useAnalytics()
-    const pal = usePalette('default')
-    const {isDesktop} = useWebMediaQueries()
-
-    const hasNew =
-      store.me.notifications.hasNewLatest &&
-      !store.me.notifications.isRefreshing
-
-    // event handlers
-    // =
-    const onPressTryAgain = React.useCallback(() => {
-      store.me.notifications.refresh()
-    }, [store])
-
-    const scrollToTop = React.useCallback(() => {
-      scrollElRef.current?.scrollToOffset({offset: 0})
-      resetMainScroll()
-    }, [scrollElRef, resetMainScroll])
+export function NotificationsScreen({}: Props) {
+  const {_} = useLingui()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
+  const scrollElRef = React.useRef<FlatList>(null)
+  const {screen} = useAnalytics()
+  const pal = usePalette('default')
+  const {isDesktop} = useWebMediaQueries()
+  const queryClient = useQueryClient()
+  const unreadNotifs = useUnreadNotifications()
+  const unreadApi = useUnreadNotificationsApi()
+  const hasNew = !!unreadNotifs
 
-    const onPressLoadLatest = React.useCallback(() => {
-      scrollToTop()
-      store.me.notifications.refresh()
-    }, [store, scrollToTop])
+  // event handlers
+  // =
+  const scrollToTop = React.useCallback(() => {
+    scrollElRef.current?.scrollToOffset({offset: 0})
+    resetMainScroll()
+  }, [scrollElRef, resetMainScroll])
 
-    // on-visible setup
-    // =
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        logger.debug('NotificationsScreen: Updating feed')
-        const softResetSub = store.onScreenSoftReset(onPressLoadLatest)
-        store.me.notifications.update()
-        screen('Notifications')
+  const onPressLoadLatest = React.useCallback(() => {
+    scrollToTop()
+    if (hasNew) {
+      // render what we have now
+      truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
+    } else {
+      // check with the server
+      unreadApi.checkUnread({invalidate: true})
+    }
+  }, [scrollToTop, queryClient, unreadApi, hasNew])
 
-        return () => {
-          softResetSub.remove()
-          store.me.notifications.markAllRead()
-        }
-      }, [store, screen, onPressLoadLatest, setMinimalShellMode]),
-    )
+  // on-visible setup
+  // =
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      logger.debug('NotificationsScreen: Updating feed')
+      screen('Notifications')
+      return listenSoftReset(onPressLoadLatest)
+    }, [screen, onPressLoadLatest, setMinimalShellMode]),
+  )
 
-    useTabFocusEffect(
-      'Notifications',
-      React.useCallback(
-        isInside => {
-          // on mobile:
-          // fires with `isInside=true` when the user navigates to the root tab
-          // but not when the user goes back to the screen by pressing back
-          // on web:
-          // essentially equivalent to useFocusEffect because we dont used tabbed
-          // navigation
-          if (isInside) {
-            if (isWeb) {
-              store.me.notifications.syncQueue()
-            } else {
-              if (store.me.notifications.unreadCount > 0) {
-                store.me.notifications.refresh()
-              } else {
-                store.me.notifications.syncQueue()
-              }
+  const ListHeaderComponent = React.useCallback(() => {
+    if (isDesktop) {
+      return (
+        <View
+          style={[
+            pal.view,
+            {
+              flexDirection: 'row',
+              alignItems: 'center',
+              justifyContent: 'space-between',
+              paddingHorizontal: 18,
+              paddingVertical: 12,
+            },
+          ]}>
+          <TextLink
+            type="title-lg"
+            href="/notifications"
+            style={[pal.text, {fontWeight: 'bold'}]}
+            text={
+              <>
+                <Trans>Notifications</Trans>{' '}
+                {hasNew && (
+                  <View
+                    style={{
+                      top: -8,
+                      backgroundColor: colors.blue3,
+                      width: 8,
+                      height: 8,
+                      borderRadius: 4,
+                    }}
+                  />
+                )}
+              </>
             }
-          }
-        },
-        [store],
-      ),
-    )
-
-    const ListHeaderComponent = React.useCallback(() => {
-      if (isDesktop) {
-        return (
-          <View
-            style={[
-              pal.view,
-              {
-                flexDirection: 'row',
-                alignItems: 'center',
-                justifyContent: 'space-between',
-                paddingHorizontal: 18,
-                paddingVertical: 12,
-              },
-            ]}>
-            <TextLink
-              type="title-lg"
-              href="/notifications"
-              style={[pal.text, {fontWeight: 'bold'}]}
-              text={
-                <>
-                  Notifications{' '}
-                  {hasNew && (
-                    <View
-                      style={{
-                        top: -8,
-                        backgroundColor: colors.blue3,
-                        width: 8,
-                        height: 8,
-                        borderRadius: 4,
-                      }}
-                    />
-                  )}
-                </>
-              }
-              onPress={() => store.emitScreenSoftReset()}
-            />
-          </View>
-        )
-      }
-      return <></>
-    }, [isDesktop, pal, store, hasNew])
+            onPress={emitSoftReset}
+          />
+        </View>
+      )
+    }
+    return <></>
+  }, [isDesktop, pal, hasNew])
 
-    return (
-      <View testID="notificationsScreen" style={s.hContentRegion}>
-        <ViewHeader title="Notifications" canGoBack={false} />
-        <InvitedUsers />
-        <Feed
-          view={store.me.notifications}
-          onPressTryAgain={onPressTryAgain}
-          onScroll={onMainScroll}
-          scrollElRef={scrollElRef}
-          ListHeaderComponent={ListHeaderComponent}
+  return (
+    <View testID="notificationsScreen" style={s.hContentRegion}>
+      <ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
+      <Feed
+        onScroll={onMainScroll}
+        scrollElRef={scrollElRef}
+        ListHeaderComponent={ListHeaderComponent}
+      />
+      {(isScrolledDown || hasNew) && (
+        <LoadLatestBtn
+          onPress={onPressLoadLatest}
+          label={_(msg`Load new notifications`)}
+          showIndicator={hasNew}
         />
-        {(isScrolledDown || hasNew) && (
-          <LoadLatestBtn
-            onPress={onPressLoadLatest}
-            label="Load new notifications"
-            showIndicator={hasNew}
-          />
-        )}
-      </View>
-    )
-  }),
-)
+      )}
+    </View>
+  )
+}
diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx
index 2f45908b3..7cbb81102 100644
--- a/src/view/screens/PostLikedBy.tsx
+++ b/src/view/screens/PostLikedBy.tsx
@@ -2,17 +2,19 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
-export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
+export const PostLikedByScreen = ({route}: Props) => {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -22,8 +24,8 @@ export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Liked by" />
+      <ViewHeader title={_(msg`Liked by`)} />
       <PostLikedByComponent uri={uri} />
     </View>
   )
-})
+}
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index abe03467a..de95f33bf 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -1,18 +1,20 @@
 import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
-export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
+export const PostRepostedByScreen = ({route}: Props) => {
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -22,8 +24,8 @@ export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Reposted by" />
+      <ViewHeader title={_(msg`Reposted by`)} />
       <PostRepostedByComponent uri={uri} />
     </View>
   )
-})
+}
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 0bdd06269..4b1f51748 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -1,104 +1,107 @@
-import React, {useMemo} from 'react'
-import {InteractionManager, StyleSheet, View} from 'react-native'
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import Animated from 'react-native-reanimated'
 import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
+import {useQueryClient} from '@tanstack/react-query'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
 import {ComposePrompt} from 'view/com/composer/Prompt'
-import {PostThreadModel} from 'state/models/content/post-thread'
-import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {
+  RQKEY as POST_THREAD_RQKEY,
+  ThreadNode,
+} from '#/state/queries/post-thread'
 import {clamp} from 'lodash'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {logger} from '#/logger'
-import {useMinimalShellMode, useSetMinimalShellMode} from '#/state/shell'
-
-const SHELL_FOOTER_HEIGHT = 44
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {ErrorMessage} from '../com/util/error/ErrorMessage'
+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 store = useStores()
-    const minimalShellMode = useMinimalShellMode()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const safeAreaInsets = useSafeAreaInsets()
-    const {name, rkey} = route.params
-    const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-    const view = useMemo<PostThreadModel>(
-      () => new PostThreadModel(store, {uri}),
-      [store, uri],
-    )
-    const {isMobile} = useWebMediaQueries()
-
-    useFocusEffect(
-      React.useCallback(() => {
-        setMinimalShellMode(false)
-        const threadCleanup = view.registerListeners()
+export function PostThreadScreen({route}: Props) {
+  const queryClient = useQueryClient()
+  const {_} = useLingui()
+  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)
 
-        InteractionManager.runAfterInteractions(() => {
-          if (!view.hasLoaded && !view.isLoading) {
-            view.setup().catch(err => {
-              logger.error('Failed to fetch thread', {error: err})
-            })
-          }
-        })
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
 
-        return () => {
-          threadCleanup()
-        }
-      }, [view, setMinimalShellMode]),
+  const onPressReply = React.useCallback(() => {
+    if (!resolvedUri) {
+      return
+    }
+    const thread = queryClient.getQueryData<ThreadNode>(
+      POST_THREAD_RQKEY(resolvedUri.uri),
     )
-
-    const onPressReply = React.useCallback(() => {
-      if (!view.thread) {
-        return
-      }
-      store.shell.openComposer({
-        replyTo: {
-          uri: view.thread.post.uri,
-          cid: view.thread.post.cid,
-          text: view.thread.postRecord?.text as string,
-          author: {
-            handle: view.thread.post.author.handle,
-            displayName: view.thread.post.author.displayName,
-            avatar: view.thread.post.author.avatar,
-          },
+    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: () => view.refresh(),
-      })
-    }, [view, store])
+      },
+      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}>
+  return (
+    <View style={s.hContentRegion}>
+      {isMobile && <ViewHeader title={_(msg`Post`)} />}
+      <View style={s.flex1}>
+        {uriError ? (
+          <CenteredView>
+            <ErrorMessage message={String(uriError)} />
+          </CenteredView>
+        ) : (
           <PostThreadComponent
-            uri={uri}
-            view={view}
+            uri={resolvedUri?.uri}
             onPressReply={onPressReply}
-            treeView={!!store.preferences.thread.lab_treeViewEnabled}
           />
-        </View>
-        {isMobile && !minimalShellMode && (
-          <View
-            style={[
-              styles.prompt,
-              {
-                bottom:
-                  SHELL_FOOTER_HEIGHT + clamp(safeAreaInsets.bottom, 15, 30),
-              },
-            ]}>
-            <ComposePrompt onPressCompose={onPressReply} />
-          </View>
         )}
       </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 21c15931f..fe17be5e8 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -1,10 +1,8 @@
 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'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -14,21 +12,33 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import debounce from 'lodash.debounce'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetFeedViewPreferencesMutation,
+} from '#/state/queries/preferences'
 
-function RepliesThresholdInput({enabled}: {enabled: boolean}) {
-  const store = useStores()
+function RepliesThresholdInput({
+  enabled,
+  initialValue,
+}: {
+  enabled: boolean
+  initialValue: number
+}) {
   const pal = usePalette('default')
-  const [value, setValue] = useState(
-    store.preferences.homeFeed.hideRepliesByLikeCount,
-  )
+  const [value, setValue] = useState(initialValue)
+  const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const save = React.useMemo(
     () =>
       debounce(
         threshold =>
-          store.preferences.setHomeFeedHideRepliesByLikeCount(threshold),
+          setFeedViewPref({
+            hideRepliesByLikeCount: threshold,
+          }),
         500,
       ), // debouce for 500ms
-    [store],
+    [setFeedViewPref],
   )
 
   return (
@@ -61,12 +71,17 @@ type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'PreferencesHomeFeed'
 >
-export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
-  navigation,
-}: Props) {
+export function PreferencesHomeFeed({navigation}: Props) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setFeedViewPref, variables} =
+    useSetFeedViewPreferencesMutation()
+
+  const showReplies = !(
+    variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
+  )
 
   return (
     <CenteredView
@@ -77,14 +92,14 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
         styles.container,
         isTabletOrDesktop && styles.desktopContainer,
       ]}>
-      <ViewHeader title="Home Feed Preferences" showOnDesktop />
+      <ViewHeader title={_(msg`Home Feed Preferences`)} showOnDesktop />
       <View
         style={[
           styles.titleSection,
           isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
         ]}>
         <Text type="xl" style={[pal.textLight, styles.description]}>
-          Fine-tune the content you see on your home screen.
+          <Trans>Fine-tune the content you see on your home screen.</Trans>
         </Text>
       </View>
 
@@ -92,98 +107,175 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
         <View style={styles.cardsContainer}>
           <View style={[pal.viewLight, styles.card]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Show Replies
+              <Trans>Show Replies</Trans>
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Set this setting to "No" to hide all replies from your feed.
+              <Trans>
+                Set this setting to "No" to hide all replies from your feed.
+              </Trans>
             </Text>
             <ToggleButton
               testID="toggleRepliesBtn"
               type="default-light"
-              label={store.preferences.homeFeed.hideReplies ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReplies}
-              onPress={store.preferences.toggleHomeFeedHideReplies}
+              label={showReplies ? 'Yes' : 'No'}
+              isSelected={showReplies}
+              onPress={() =>
+                setFeedViewPref({
+                  hideReplies: !(
+                    variables?.hideReplies ??
+                    preferences?.feedViewPrefs?.hideReplies
+                  ),
+                })
+              }
             />
           </View>
           <View
-            style={[
-              pal.viewLight,
-              styles.card,
-              store.preferences.homeFeed.hideReplies && styles.dimmed,
-            ]}>
+            style={[pal.viewLight, styles.card, !showReplies && styles.dimmed]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Reply Filters
+              <Trans>Reply Filters</Trans>
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Enable this setting to only see replies between people you follow.
+              <Trans>
+                Enable this setting to only see replies between people you
+                follow.
+              </Trans>
             </Text>
             <ToggleButton
               type="default-light"
-              label="Followed users only"
-              isSelected={store.preferences.homeFeed.hideRepliesByUnfollowed}
+              label={_(msg`Followed users only`)}
+              isSelected={Boolean(
+                variables?.hideRepliesByUnfollowed ??
+                  preferences?.feedViewPrefs?.hideRepliesByUnfollowed,
+              )}
               onPress={
-                !store.preferences.homeFeed.hideReplies
-                  ? store.preferences.toggleHomeFeedHideRepliesByUnfollowed
+                showReplies
+                  ? () =>
+                      setFeedViewPref({
+                        hideRepliesByUnfollowed: !(
+                          variables?.hideRepliesByUnfollowed ??
+                          preferences?.feedViewPrefs?.hideRepliesByUnfollowed
+                        ),
+                      })
                   : undefined
               }
               style={[s.mb10]}
             />
             <Text style={[pal.text]}>
-              Adjust the number of likes a reply must have to be shown in your
-              feed.
+              <Trans>
+                Adjust the number of likes a reply must have to be shown in your
+                feed.
+              </Trans>
             </Text>
-            <RepliesThresholdInput
-              enabled={!store.preferences.homeFeed.hideReplies}
-            />
+            {preferences && (
+              <RepliesThresholdInput
+                enabled={showReplies}
+                initialValue={preferences.feedViewPrefs.hideRepliesByLikeCount}
+              />
+            )}
           </View>
 
           <View style={[pal.viewLight, styles.card]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Show Reposts
+              <Trans>Show Reposts</Trans>
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Set this setting to "No" to hide all reposts from your feed.
+              <Trans>
+                Set this setting to "No" to hide all reposts from your feed.
+              </Trans>
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideReposts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReposts}
-              onPress={store.preferences.toggleHomeFeedHideReposts}
+              label={
+                variables?.hideReposts ??
+                preferences?.feedViewPrefs?.hideReposts
+                  ? _(msg`No`)
+                  : _(msg`Yes`)
+              }
+              isSelected={
+                !(
+                  variables?.hideReposts ??
+                  preferences?.feedViewPrefs?.hideReposts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideReposts: !(
+                    variables?.hideReposts ??
+                    preferences?.feedViewPrefs?.hideReposts
+                  ),
+                })
+              }
             />
           </View>
 
           <View style={[pal.viewLight, styles.card]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Show Quote Posts
+              <Trans>Show Quote Posts</Trans>
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Set this setting to "No" to hide all quote posts from your feed.
-              Reposts will still be visible.
+              <Trans>
+                Set this setting to "No" to hide all quote posts from your feed.
+                Reposts will still be visible.
+              </Trans>
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideQuotePosts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideQuotePosts}
-              onPress={store.preferences.toggleHomeFeedHideQuotePosts}
+              label={
+                variables?.hideQuotePosts ??
+                preferences?.feedViewPrefs?.hideQuotePosts
+                  ? _(msg`No`)
+                  : _(msg`Yes`)
+              }
+              isSelected={
+                !(
+                  variables?.hideQuotePosts ??
+                  preferences?.feedViewPrefs?.hideQuotePosts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideQuotePosts: !(
+                    variables?.hideQuotePosts ??
+                    preferences?.feedViewPrefs?.hideQuotePosts
+                  ),
+                })
+              }
             />
           </View>
 
           <View style={[pal.viewLight, styles.card]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show
-              Posts from My Feeds
+              <FontAwesomeIcon icon="flask" color={pal.colors.text} />
+              <Trans>Show Posts from My Feeds</Trans>
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Set this setting to "Yes" to show samples of your saved feeds in
-              your following feed. This is an experimental feature.
+              <Trans>
+                Set this setting to "Yes" to show samples of your saved feeds in
+                your following feed. This is an experimental feature.
+              </Trans>
             </Text>
             <ToggleButton
               type="default-light"
               label={
-                store.preferences.homeFeed.lab_mergeFeedEnabled ? 'Yes' : 'No'
+                variables?.lab_mergeFeedEnabled ??
+                preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ? _(msg`Yes`)
+                  : _(msg`No`)
+              }
+              isSelected={
+                !!(
+                  variables?.lab_mergeFeedEnabled ??
+                  preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  lab_mergeFeedEnabled: !(
+                    variables?.lab_mergeFeedEnabled ??
+                    preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ),
+                })
               }
-              isSelected={!!store.preferences.homeFeed.lab_mergeFeedEnabled}
-              onPress={store.preferences.toggleHomeFeedMergeFeedEnabled}
             />
           </View>
         </View>
@@ -204,14 +296,16 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
           }}
           style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
           accessibilityRole="button"
-          accessibilityLabel="Confirm"
+          accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>Done</Text>
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Done</Trans>
+          </Text>
         </TouchableOpacity>
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index af98a1833..73d941932 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -1,9 +1,13 @@
 import React from 'react'
-import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
+import {
+  ActivityIndicator,
+  ScrollView,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -12,14 +16,30 @@ import {RadioGroup} from 'view/com/util/forms/RadioGroup'
 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetThreadViewPreferencesMutation,
+} 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 store = useStores()
+  const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setThreadViewPrefs, variables} =
+    useSetThreadViewPreferencesMutation()
+
+  const prioritizeFollowedUsers = Boolean(
+    variables?.prioritizeFollowedUsers ??
+      preferences?.threadViewPrefs?.prioritizeFollowedUsers,
+  )
+  const treeViewEnabled = Boolean(
+    variables?.lab_treeViewEnabled ??
+      preferences?.threadViewPrefs?.lab_treeViewEnabled,
+  )
 
   return (
     <CenteredView
@@ -30,78 +50,90 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
         styles.container,
         isTabletOrDesktop && styles.desktopContainer,
       ]}>
-      <ViewHeader title="Thread Preferences" showOnDesktop />
+      <ViewHeader title={_(msg`Thread Preferences`)} showOnDesktop />
       <View
         style={[
           styles.titleSection,
           isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
         ]}>
         <Text type="xl" style={[pal.textLight, styles.description]}>
-          Fine-tune the discussion threads.
+          <Trans>Fine-tune the discussion threads.</Trans>
         </Text>
       </View>
 
-      <ScrollView>
-        <View style={styles.cardsContainer}>
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Sort Replies
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              Sort replies to the same post by:
-            </Text>
-            <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
-              <RadioGroup
+      {preferences ? (
+        <ScrollView>
+          <View style={styles.cardsContainer}>
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Sort Replies</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>Sort replies to the same post by:</Trans>
+              </Text>
+              <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
+                <RadioGroup
+                  type="default-light"
+                  items={[
+                    {key: 'oldest', label: 'Oldest replies first'},
+                    {key: 'newest', label: 'Newest replies first'},
+                    {key: 'most-likes', label: 'Most-liked replies first'},
+                    {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
+                  ]}
+                  onSelect={key => setThreadViewPrefs({sort: key})}
+                  initialSelection={preferences?.threadViewPrefs?.sort}
+                />
+              </View>
+            </View>
+
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Prioritize Your Follows</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Show replies by people you follow before all other replies.
+                </Trans>
+              </Text>
+              <ToggleButton
                 type="default-light"
-                items={[
-                  {key: 'oldest', label: 'Oldest replies first'},
-                  {key: 'newest', label: 'Newest replies first'},
-                  {key: 'most-likes', label: 'Most-liked replies first'},
-                  {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
-                ]}
-                onSelect={store.preferences.setThreadSort}
-                initialSelection={store.preferences.thread.sort}
+                label={prioritizeFollowedUsers ? 'Yes' : 'No'}
+                isSelected={prioritizeFollowedUsers}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    prioritizeFollowedUsers: !prioritizeFollowedUsers,
+                  })
+                }
               />
             </View>
-          </View>
 
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              Prioritize Your Follows
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              Show replies by people you follow before all other replies.
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.prioritizeFollowedUsers ? 'Yes' : 'No'
-              }
-              isSelected={store.preferences.thread.prioritizeFollowedUsers}
-              onPress={store.preferences.togglePrioritizedFollowedUsers}
-            />
-          </View>
-
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded
-              Mode
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              Set this setting to "Yes" to show replies in a threaded view. This
-              is an experimental feature.
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.lab_treeViewEnabled ? 'Yes' : 'No'
-              }
-              isSelected={!!store.preferences.thread.lab_treeViewEnabled}
-              onPress={store.preferences.toggleThreadTreeViewEnabled}
-            />
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
+                <Trans>Threaded Mode</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Set this setting to "Yes" to show replies in a threaded view.
+                  This is an experimental feature.
+                </Trans>
+              </Text>
+              <ToggleButton
+                type="default-light"
+                label={treeViewEnabled ? 'Yes' : 'No'}
+                isSelected={treeViewEnabled}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    lab_treeViewEnabled: !treeViewEnabled,
+                  })
+                }
+              />
+            </View>
           </View>
-        </View>
-      </ScrollView>
+        </ScrollView>
+      ) : (
+        <ActivityIndicator />
+      )}
 
       <View
         style={[
@@ -118,14 +150,16 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
           }}
           style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
           accessibilityRole="button"
-          accessibilityLabel="Confirm"
+          accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
-          <Text style={[s.white, s.bold, s.f18]}>Done</Text>
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Done</Trans>
+          </Text>
         </TouchableOpacity>
       </View>
     </CenteredView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx
index f709c9fda..247afc316 100644
--- a/src/view/screens/PrivacyPolicy.tsx
+++ b/src/view/screens/PrivacyPolicy.tsx
@@ -9,10 +9,13 @@ import {ScrollView} from 'view/com/util/Views'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PrivacyPolicy'>
 export const PrivacyPolicyScreen = (_props: Props) => {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
 
   useFocusEffect(
@@ -23,16 +26,18 @@ export const PrivacyPolicyScreen = (_props: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Privacy Policy" />
+      <ViewHeader title={_(msg`Privacy Policy`)} />
       <ScrollView style={[s.hContentRegion, pal.view]}>
         <View style={[s.p20]}>
           <Text style={pal.text}>
-            The Privacy Policy has been moved to{' '}
-            <TextLink
-              style={pal.link}
-              href="https://blueskyweb.xyz/support/privacy-policy"
-              text="blueskyweb.xyz/support/privacy-policy"
-            />
+            <Trans>
+              The Privacy Policy has been moved to{' '}
+              <TextLink
+                style={pal.link}
+                href="https://blueskyweb.xyz/support/privacy-policy"
+                text="blueskyweb.xyz/support/privacy-policy"
+              />
+            </Trans>
           </Text>
         </View>
         <View style={s.footerSpacer} />
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9a25612ad..4af1b650e 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,317 +1,447 @@
-import React, {useEffect, useState} from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
+import React, {useMemo} from 'react'
+import {StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
+import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {ViewSelector, ViewSelectorHandle} from '../com/util/ViewSelector'
-import {CenteredView} from '../com/util/Views'
+import {CenteredView, FlatList} from '../com/util/Views'
 import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
-import {ProfileUiModel, Sections} from 'state/models/ui/profile'
-import {useStores} from 'state/index'
-import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
+import {Feed} from 'view/com/posts/Feed'
+import {ProfileLists} from '../com/lists/ProfileLists'
+import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
 import {ProfileHeader} from '../com/profile/ProfileHeader'
-import {FeedSlice} from '../com/posts/FeedSlice'
-import {ListCard} from 'view/com/lists/ListCard'
-import {
-  PostFeedLoadingPlaceholder,
-  ProfileCardFeedLoadingPlaceholder,
-} from '../com/util/LoadingPlaceholder'
+import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
-import {ErrorMessage} from '../com/util/error/ErrorMessage'
 import {EmptyState} from '../com/util/EmptyState'
-import {Text} from '../com/util/text/Text'
 import {FAB} from '../com/util/fab/FAB'
 import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {ComposeIcon2} from 'lib/icons'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {FeedSourceModel} from 'state/models/content/feed-source'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
-import {logger} from '#/logger'
-import {useSetMinimalShellMode} from '#/state/shell'
+import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {useResolveDidQuery} from '#/state/queries/resolve-uri'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useSession} from '#/state/session'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {cleanError} from '#/lib/strings/errors'
+import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
+import {useQueryClient} from '@tanstack/react-query'
+import {useComposerControls} from '#/state/shell/composer'
+import {listenSoftReset} from '#/state/events'
+import {truncateAndInvalidate} from '#/state/queries/util'
+
+interface SectionRef {
+  scrollToTop: () => void
+}
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
-export const ProfileScreen = withAuthRequired(
-  observer(function ProfileScreenImpl({route}: Props) {
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {screen, track} = useAnalytics()
-    const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
-    const name = route.params.name === 'me' ? store.me.did : route.params.name
+export function ProfileScreen({route}: Props) {
+  const {currentAccount} = useSession()
+  const name =
+    route.params.name === 'me' ? currentAccount?.did : route.params.name
+  const moderationOpts = useModerationOpts()
+  const {
+    data: resolvedDid,
+    error: resolveError,
+    refetch: refetchDid,
+    isInitialLoading: isInitialLoadingDid,
+  } = useResolveDidQuery(name)
+  const {
+    data: profile,
+    error: profileError,
+    refetch: refetchProfile,
+    isInitialLoading: isInitialLoadingProfile,
+  } = useProfileQuery({
+    did: resolvedDid,
+  })
 
-    useEffect(() => {
-      screen('Profile')
-    }, [screen])
+  const onPressTryAgain = React.useCallback(() => {
+    if (resolveError) {
+      refetchDid()
+    } else {
+      refetchProfile()
+    }
+  }, [resolveError, refetchDid, refetchProfile])
 
-    const [hasSetup, setHasSetup] = useState<boolean>(false)
-    const uiState = React.useMemo(
-      () => new ProfileUiModel(store, {user: name}),
-      [name, store],
+  if (isInitialLoadingDid || isInitialLoadingProfile || !moderationOpts) {
+    return (
+      <CenteredView>
+        <ProfileHeader
+          profile={null}
+          moderation={null}
+          isProfilePreview={true}
+        />
+      </CenteredView>
     )
-    useSetTitle(combinedDisplayName(uiState.profile))
+  }
+  if (resolveError || profileError) {
+    return (
+      <CenteredView>
+        <ErrorScreen
+          testID="profileErrorScreen"
+          title="Oops!"
+          message={cleanError(resolveError || profileError)}
+          onPressTryAgain={onPressTryAgain}
+        />
+      </CenteredView>
+    )
+  }
+  if (profile && moderationOpts) {
+    return (
+      <ProfileScreenLoaded
+        profile={profile}
+        moderationOpts={moderationOpts}
+        hideBackButton={!!route.params.hideBackButton}
+      />
+    )
+  }
+  // should never happen
+  return (
+    <CenteredView>
+      <ErrorScreen
+        testID="profileErrorScreen"
+        title="Oops!"
+        message="Something went wrong and we're not sure what."
+        onPressTryAgain={onPressTryAgain}
+      />
+    </CenteredView>
+  )
+}
 
-    const onSoftReset = React.useCallback(() => {
-      viewSelectorRef.current?.scrollToTop()
-    }, [])
+function ProfileScreenLoaded({
+  profile: profileUnshadowed,
+  moderationOpts,
+  hideBackButton,
+}: {
+  profile: AppBskyActorDefs.ProfileViewDetailed
+  moderationOpts: ModerationOpts
+  hideBackButton: boolean
+}) {
+  const profile = useProfileShadow(profileUnshadowed)
+  const {hasSession, currentAccount} = useSession()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {openComposer} = useComposerControls()
+  const {screen, track} = useAnalytics()
+  const [currentPage, setCurrentPage] = React.useState(0)
+  const {_} = useLingui()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
+  const postsSectionRef = React.useRef<SectionRef>(null)
+  const repliesSectionRef = React.useRef<SectionRef>(null)
+  const mediaSectionRef = React.useRef<SectionRef>(null)
+  const likesSectionRef = React.useRef<SectionRef>(null)
+  const feedsSectionRef = React.useRef<SectionRef>(null)
+  const listsSectionRef = React.useRef<SectionRef>(null)
 
-    useEffect(() => {
-      setHasSetup(false)
-    }, [name])
+  useSetTitle(combinedDisplayName(profile))
 
-    // We don't need this to be reactive, so we can just register the listeners once
-    useEffect(() => {
-      const listCleanup = uiState.lists.registerListeners()
-      return () => listCleanup()
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [])
+  const moderation = useMemo(
+    () => moderateProfile(profile, moderationOpts),
+    [profile, moderationOpts],
+  )
 
-    useFocusEffect(
-      React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        let aborted = false
-        setMinimalShellMode(false)
-        const feedCleanup = uiState.feed.registerListeners()
-        if (!hasSetup) {
-          uiState.setup().then(() => {
-            if (aborted) {
-              return
-            }
-            setHasSetup(true)
-          })
-        }
-        return () => {
-          aborted = true
-          feedCleanup()
-          softResetSub.remove()
-        }
-      }, [store, onSoftReset, uiState, hasSetup, setMinimalShellMode]),
-    )
+  const isMe = profile.did === currentAccount?.did
+  const showRepliesTab = hasSession
+  const showLikesTab = isMe
+  const showFeedsTab = isMe || extraInfoQuery.data?.hasFeedgens
+  const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
+  const sectionTitles = useMemo<string[]>(() => {
+    return [
+      'Posts',
+      showRepliesTab ? 'Posts & Replies' : undefined,
+      'Media',
+      showLikesTab ? 'Likes' : undefined,
+      showFeedsTab ? 'Feeds' : undefined,
+      showListsTab ? 'Lists' : undefined,
+    ].filter(Boolean) as string[]
+  }, [showRepliesTab, showLikesTab, showFeedsTab, showListsTab])
 
-    // events
-    // =
+  let nextIndex = 0
+  const postsIndex = nextIndex++
+  let repliesIndex: number | null = null
+  if (showRepliesTab) {
+    repliesIndex = nextIndex++
+  }
+  const mediaIndex = nextIndex++
+  let likesIndex: number | null = null
+  if (showLikesTab) {
+    likesIndex = nextIndex++
+  }
+  let feedsIndex: number | null = null
+  if (showFeedsTab) {
+    feedsIndex = nextIndex++
+  }
+  let listsIndex: number | null = null
+  if (showListsTab) {
+    listsIndex = nextIndex++
+  }
 
-    const onPressCompose = React.useCallback(() => {
-      track('ProfileScreen:PressCompose')
-      const mention =
-        uiState.profile.handle === store.me.handle ||
-        uiState.profile.handle === 'handle.invalid'
-          ? undefined
-          : uiState.profile.handle
-      store.shell.openComposer({mention})
-    }, [store, track, uiState])
-    const onSelectView = React.useCallback(
-      (index: number) => {
-        uiState.setSelectedViewIndex(index)
-      },
-      [uiState],
-    )
-    const onRefresh = React.useCallback(() => {
-      uiState
-        .refresh()
-        .catch((err: any) =>
-          logger.error('Failed to refresh user profile', {error: err}),
-        )
-    }, [uiState])
-    const onEndReached = React.useCallback(() => {
-      uiState.loadMore().catch((err: any) =>
-        logger.error('Failed to load more entries in user profile', {
-          error: err,
-        }),
-      )
-    }, [uiState])
-    const onPressTryAgain = React.useCallback(() => {
-      uiState.setup()
-    }, [uiState])
+  const scrollSectionToTop = React.useCallback(
+    (index: number) => {
+      if (index === postsIndex) {
+        postsSectionRef.current?.scrollToTop()
+      } else if (index === repliesIndex) {
+        repliesSectionRef.current?.scrollToTop()
+      } else if (index === mediaIndex) {
+        mediaSectionRef.current?.scrollToTop()
+      } else if (index === likesIndex) {
+        likesSectionRef.current?.scrollToTop()
+      } else if (index === feedsIndex) {
+        feedsSectionRef.current?.scrollToTop()
+      } else if (index === listsIndex) {
+        listsSectionRef.current?.scrollToTop()
+      }
+    },
+    [postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
+  )
 
-    // rendering
-    // =
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      screen('Profile')
+      return listenSoftReset(() => {
+        scrollSectionToTop(currentPage)
+      })
+    }, [setMinimalShellMode, screen, currentPage, scrollSectionToTop]),
+  )
 
-    const renderHeader = React.useCallback(() => {
-      if (!uiState) {
-        return <View />
+  useFocusEffect(
+    React.useCallback(() => {
+      setDrawerSwipeDisabled(currentPage > 0)
+      return () => {
+        setDrawerSwipeDisabled(false)
       }
-      return (
-        <ProfileHeader
-          view={uiState.profile}
-          onRefreshAll={onRefresh}
-          hideBackButton={route.params.hideBackButton}
-        />
-      )
-    }, [uiState, onRefresh, route.params.hideBackButton])
+    }, [setDrawerSwipeDisabled, currentPage]),
+  )
 
-    const Footer = React.useMemo(() => {
-      return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined
-    }, [uiState.showLoadingMoreFooter])
-    const renderItem = React.useCallback(
-      (item: any) => {
-        // if section is lists
-        if (uiState.selectedView === Sections.Lists) {
-          if (item === ProfileUiModel.LOADING_ITEM) {
-            return <ProfileCardFeedLoadingPlaceholder />
-          } else if (item._reactKey === '__error__') {
-            return (
-              <View style={s.p5}>
-                <ErrorMessage
-                  message={item.error}
-                  onPressTryAgain={onPressTryAgain}
-                />
-              </View>
-            )
-          } else if (item === ProfileUiModel.EMPTY_ITEM) {
-            return (
-              <EmptyState
-                testID="listsEmpty"
-                icon="list-ul"
-                message="No lists yet!"
-                style={styles.emptyState}
+  // events
+  // =
+
+  const onPressCompose = React.useCallback(() => {
+    track('ProfileScreen:PressCompose')
+    const mention =
+      profile.handle === currentAccount?.handle ||
+      profile.handle === 'handle.invalid'
+        ? undefined
+        : profile.handle
+    openComposer({mention})
+  }, [openComposer, currentAccount, track, profile])
+
+  const onPageSelected = React.useCallback(
+    (i: number) => {
+      setCurrentPage(i)
+    },
+    [setCurrentPage],
+  )
+
+  const onCurrentPageSelected = React.useCallback(
+    (index: number) => {
+      scrollSectionToTop(index)
+    },
+    [scrollSectionToTop],
+  )
+
+  // rendering
+  // =
+
+  const renderHeader = React.useCallback(() => {
+    return (
+      <ProfileHeader
+        profile={profile}
+        moderation={moderation}
+        hideBackButton={hideBackButton}
+      />
+    )
+  }, [profile, moderation, hideBackButton])
+
+  return (
+    <ScreenHider
+      testID="profileView"
+      style={styles.container}
+      screenDescription="profile"
+      moderation={moderation.account}>
+      <PagerWithHeader
+        testID="profilePager"
+        isHeaderReady={true}
+        items={sectionTitles}
+        onPageSelected={onPageSelected}
+        onCurrentPageSelected={onCurrentPageSelected}
+        renderHeader={renderHeader}>
+        {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
+          <FeedSection
+            ref={postsSectionRef}
+            feed={`author|${profile.did}|posts_no_replies`}
+            onScroll={onScroll}
+            headerHeight={headerHeight}
+            isFocused={isFocused}
+            isScrolledDown={isScrolledDown}
+            scrollElRef={
+              scrollElRef as React.MutableRefObject<FlatList<any> | null>
+            }
+          />
+        )}
+        {showRepliesTab
+          ? ({
+              onScroll,
+              headerHeight,
+              isFocused,
+              isScrolledDown,
+              scrollElRef,
+            }) => (
+              <FeedSection
+                ref={repliesSectionRef}
+                feed={`author|${profile.did}|posts_with_replies`}
+                onScroll={onScroll}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                isScrolledDown={isScrolledDown}
+                scrollElRef={
+                  scrollElRef as React.MutableRefObject<FlatList<any> | null>
+                }
               />
             )
-          } else {
-            return <ListCard testID={`list-${item.name}`} list={item} />
-          }
-          // if section is custom algorithms
-        } else if (uiState.selectedView === Sections.CustomAlgorithms) {
-          if (item === ProfileUiModel.LOADING_ITEM) {
-            return <ProfileCardFeedLoadingPlaceholder />
-          } else if (item._reactKey === '__error__') {
-            return (
-              <View style={s.p5}>
-                <ErrorMessage
-                  message={item.error}
-                  onPressTryAgain={onPressTryAgain}
-                />
-              </View>
-            )
-          } else if (item === ProfileUiModel.EMPTY_ITEM) {
-            return (
-              <EmptyState
-                testID="customAlgorithmsEmpty"
-                icon="list-ul"
-                message="No custom algorithms yet!"
-                style={styles.emptyState}
+          : null}
+        {({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
+          <FeedSection
+            ref={mediaSectionRef}
+            feed={`author|${profile.did}|posts_with_media`}
+            onScroll={onScroll}
+            headerHeight={headerHeight}
+            isFocused={isFocused}
+            isScrolledDown={isScrolledDown}
+            scrollElRef={
+              scrollElRef as React.MutableRefObject<FlatList<any> | null>
+            }
+          />
+        )}
+        {showLikesTab
+          ? ({
+              onScroll,
+              headerHeight,
+              isFocused,
+              isScrolledDown,
+              scrollElRef,
+            }) => (
+              <FeedSection
+                ref={likesSectionRef}
+                feed={`likes|${profile.did}`}
+                onScroll={onScroll}
+                headerHeight={headerHeight}
+                isFocused={isFocused}
+                isScrolledDown={isScrolledDown}
+                scrollElRef={
+                  scrollElRef as React.MutableRefObject<FlatList<any> | null>
+                }
               />
             )
-          } else if (item instanceof FeedSourceModel) {
-            return (
-              <FeedSourceCard
-                item={item}
-                showSaveBtn
-                showLikes
-                showDescription
+          : null}
+        {showFeedsTab
+          ? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
+              <ProfileFeedgens
+                ref={feedsSectionRef}
+                did={profile.did}
+                scrollElRef={
+                  scrollElRef as React.MutableRefObject<FlatList<any> | null>
+                }
+                onScroll={onScroll}
+                scrollEventThrottle={1}
+                headerOffset={headerHeight}
+                enabled={isFocused}
               />
             )
-          }
-          // if section is posts or posts & replies
-        } else {
-          if (item === ProfileUiModel.END_ITEM) {
-            return <Text style={styles.endItem}>- end of feed -</Text>
-          } else if (item === ProfileUiModel.LOADING_ITEM) {
-            return <PostFeedLoadingPlaceholder />
-          } else if (item._reactKey === '__error__') {
-            if (uiState.feed.isBlocking) {
-              return (
-                <EmptyState
-                  icon="ban"
-                  message="Posts hidden"
-                  style={styles.emptyState}
-                />
-              )
-            }
-            if (uiState.feed.isBlockedBy) {
-              return (
-                <EmptyState
-                  icon="ban"
-                  message="Posts hidden"
-                  style={styles.emptyState}
-                />
-              )
-            }
-            return (
-              <View style={s.p5}>
-                <ErrorMessage
-                  message={item.error}
-                  onPressTryAgain={onPressTryAgain}
-                />
-              </View>
-            )
-          } else if (item === ProfileUiModel.EMPTY_ITEM) {
-            return (
-              <EmptyState
-                icon={['far', 'message']}
-                message="No posts yet!"
-                style={styles.emptyState}
+          : null}
+        {showListsTab
+          ? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
+              <ProfileLists
+                ref={listsSectionRef}
+                did={profile.did}
+                scrollElRef={
+                  scrollElRef as React.MutableRefObject<FlatList<any> | null>
+                }
+                onScroll={onScroll}
+                scrollEventThrottle={1}
+                headerOffset={headerHeight}
+                enabled={isFocused}
               />
             )
-          } else if (item instanceof PostsFeedSliceModel) {
-            return (
-              <FeedSlice slice={item} ignoreFilterFor={uiState.profile.did} />
-            )
-          }
-        }
-        return <View />
-      },
-      [
-        onPressTryAgain,
-        uiState.selectedView,
-        uiState.profile.did,
-        uiState.feed.isBlocking,
-        uiState.feed.isBlockedBy,
-      ],
-    )
-
-    return (
-      <ScreenHider
-        testID="profileView"
-        style={styles.container}
-        screenDescription="profile"
-        moderation={uiState.profile.moderation.account}>
-        {uiState.profile.hasError ? (
-          <ErrorScreen
-            testID="profileErrorScreen"
-            title="Failed to load profile"
-            message={uiState.profile.error}
-            onPressTryAgain={onPressTryAgain}
-          />
-        ) : uiState.profile.hasLoaded ? (
-          <ViewSelector
-            ref={viewSelectorRef}
-            swipeEnabled={false}
-            sections={uiState.selectorItems}
-            items={uiState.uiItems}
-            renderHeader={renderHeader}
-            renderItem={renderItem}
-            ListFooterComponent={Footer}
-            refreshing={uiState.isRefreshing || false}
-            onSelectView={onSelectView}
-            onRefresh={onRefresh}
-            onEndReached={onEndReached}
-          />
-        ) : (
-          <CenteredView>{renderHeader()}</CenteredView>
-        )}
+          : null}
+      </PagerWithHeader>
+      {hasSession && (
         <FAB
           testID="composeFAB"
           onPress={onPressCompose}
           icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
           accessibilityRole="button"
-          accessibilityLabel="New post"
+          accessibilityLabel={_(msg`New post`)}
           accessibilityHint=""
         />
-      </ScreenHider>
-    )
-  }),
-)
-
-function LoadingMoreFooter() {
-  return (
-    <View style={styles.loadingMoreFooter}>
-      <ActivityIndicator />
-    </View>
+      )}
+    </ScreenHider>
   )
 }
 
+interface FeedSectionProps {
+  feed: FeedDescriptor
+  onScroll: OnScrollHandler
+  headerHeight: number
+  isFocused: boolean
+  isScrolledDown: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | null>
+}
+const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
+  function FeedSectionImpl(
+    {feed, onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef},
+    ref,
+  ) {
+    const queryClient = useQueryClient()
+    const [hasNew, setHasNew] = React.useState(false)
+
+    const onScrollToTop = React.useCallback(() => {
+      scrollElRef.current?.scrollToOffset({offset: -headerHeight})
+      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+      setHasNew(false)
+    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
+    React.useImperativeHandle(ref, () => ({
+      scrollToTop: onScrollToTop,
+    }))
+
+    const renderPostsEmpty = React.useCallback(() => {
+      return <EmptyState icon="feed" message="This feed is empty!" />
+    }, [])
+
+    return (
+      <View>
+        <Feed
+          testID="postsFeed"
+          enabled={isFocused}
+          feed={feed}
+          pollInterval={30e3}
+          scrollElRef={scrollElRef}
+          onHasNew={setHasNew}
+          onScroll={onScroll}
+          scrollEventThrottle={1}
+          renderEmptyState={renderPostsEmpty}
+          headerOffset={headerHeight}
+        />
+        {(isScrolledDown || hasNew) && (
+          <LoadLatestBtn
+            onPress={onScrollToTop}
+            label="Load new posts"
+            showIndicator={hasNew}
+          />
+        )}
+      </View>
+    )
+  },
+)
+
 const styles = StyleSheet.create({
   container: {
     flexDirection: 'column',
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index a4d146d66..3a0bdcc0f 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,25 +1,21 @@
 import React, {useMemo, useCallback} from 'react'
 import {
-  FlatList,
-  NativeScrollEvent,
+  Dimensions,
   StyleSheet,
   View,
   ActivityIndicator,
+  FlatList,
 } from 'react-native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useNavigation} from '@react-navigation/native'
-import {useAnimatedScrollHandler} from 'react-native-reanimated'
+import {useQueryClient} from '@tanstack/react-query'
 import {usePalette} from 'lib/hooks/usePalette'
 import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {colors, s} from 'lib/styles'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
-import {FeedSourceModel} from 'state/models/content/feed-source'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {FeedDescriptor} from '#/state/queries/post-feed'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
 import {Feed} from 'view/com/posts/Feed'
@@ -32,13 +28,13 @@ import {FAB} from 'view/com/util/fab/FAB'
 import {EmptyState} from 'view/com/util/EmptyState'
 import * as Toast from 'view/com/util/Toast'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
-import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {Haptics} from 'lib/haptics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {resolveName} from 'lib/api'
 import {makeCustomFeedLink} from 'lib/routes/links'
 import {pluralize} from 'lib/strings/helpers'
 import {CenteredView, ScrollView} from 'view/com/util/Views'
@@ -47,6 +43,28 @@ import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
 import {ComposeIcon2} from 'lib/icons'
 import {logger} from '#/logger'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
+import {
+  useFeedSourceInfoQuery,
+  FeedSourceFeedInfo,
+  useIsFeedPublicQuery,
+} from '#/state/queries/feed'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {
+  UsePreferencesQueryResponse,
+  usePreferencesQuery,
+  useSaveFeedMutation,
+  useRemoveFeedMutation,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
+import {useComposerControls} from '#/state/shell/composer'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
 const SECTION_TITLES = ['Posts', 'About']
 
@@ -55,315 +73,372 @@ interface SectionRef {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
-export const ProfileFeedScreen = withAuthRequired(
-  observer(function ProfileFeedScreenImpl(props: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const navigation = useNavigation<NavigationProp>()
+export function ProfileFeedScreen(props: Props) {
+  const {rkey, name: handleOrDid} = props.route.params
 
-    const {name: handleOrDid} = props.route.params
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
 
-    const [feedOwnerDid, setFeedOwnerDid] = React.useState<string | undefined>()
-    const [error, setError] = React.useState<string | undefined>()
+  const uri = useMemo(
+    () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
+    [rkey, handleOrDid],
+  )
+  const {error, data: resolvedUri} = useResolveUriQuery(uri)
 
-    const onPressBack = React.useCallback(() => {
-      if (navigation.canGoBack()) {
-        navigation.goBack()
-      } else {
-        navigation.navigate('Home')
-      }
-    }, [navigation])
-
-    React.useEffect(() => {
-      /*
-       * We must resolve the DID of the feed owner before we can fetch the feed.
-       */
-      async function fetchDid() {
-        try {
-          const did = await resolveName(store, handleOrDid)
-          setFeedOwnerDid(did)
-        } catch (e) {
-          setError(
-            `We're sorry, but we were unable to resolve this feed. If this persists, please contact the feed creator, @${handleOrDid}.`,
-          )
-        }
-      }
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
+  if (error) {
+    return (
+      <CenteredView>
+        <View style={[pal.view, pal.border, styles.notFoundContainer]}>
+          <Text type="title-lg" style={[pal.text, s.mb10]}>
+            <Trans>Could not load feed</Trans>
+          </Text>
+          <Text type="md" style={[pal.text, s.mb20]}>
+            {error.toString()}
+          </Text>
 
-      fetchDid()
-    }, [store, handleOrDid, setFeedOwnerDid])
-
-    if (error) {
-      return (
-        <CenteredView>
-          <View style={[pal.view, pal.border, styles.notFoundContainer]}>
-            <Text type="title-lg" style={[pal.text, s.mb10]}>
-              Could not load feed
-            </Text>
-            <Text type="md" style={[pal.text, s.mb20]}>
-              {error}
-            </Text>
-
-            <View style={{flexDirection: 'row'}}>
-              <Button
-                type="default"
-                accessibilityLabel="Go Back"
-                accessibilityHint="Return to previous page"
-                onPress={onPressBack}
-                style={{flexShrink: 1}}>
-                <Text type="button" style={pal.text}>
-                  Go Back
-                </Text>
-              </Button>
-            </View>
+          <View style={{flexDirection: 'row'}}>
+            <Button
+              type="default"
+              accessibilityLabel={_(msg`Go Back`)}
+              accessibilityHint="Return to previous page"
+              onPress={onPressBack}
+              style={{flexShrink: 1}}>
+              <Text type="button" style={pal.text}>
+                <Trans>Go Back</Trans>
+              </Text>
+            </Button>
           </View>
-        </CenteredView>
-      )
-    }
+        </View>
+      </CenteredView>
+    )
+  }
 
-    return feedOwnerDid ? (
-      <ProfileFeedScreenInner {...props} feedOwnerDid={feedOwnerDid} />
-    ) : (
+  return resolvedUri ? (
+    <ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
+  ) : (
+    <CenteredView>
+      <View style={s.p20}>
+        <ActivityIndicator size="large" />
+      </View>
+    </CenteredView>
+  )
+}
+
+function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+  const {data: preferences} = usePreferencesQuery()
+  const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+  const {isLoading: isPublicStatusLoading, data: isPublic} =
+    useIsFeedPublicQuery({uri: feedUri})
+
+  if (!preferences || !info || isPublicStatusLoading) {
+    return (
       <CenteredView>
         <View style={s.p20}>
           <ActivityIndicator size="large" />
         </View>
       </CenteredView>
     )
-  }),
-)
+  }
 
-export const ProfileFeedScreenInner = observer(
-  function ProfileFeedScreenInnerImpl({
-    route,
-    feedOwnerDid,
-  }: Props & {feedOwnerDid: string}) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const {track} = useAnalytics()
-    const feedSectionRef = React.useRef<SectionRef>(null)
-    const {rkey, name: handleOrDid} = route.params
-    const uri = useMemo(
-      () => makeRecordUri(feedOwnerDid, 'app.bsky.feed.generator', rkey),
-      [rkey, feedOwnerDid],
-    )
-    const feedInfo = useCustomFeed(uri)
-    const feed: PostsFeedModel = useMemo(() => {
-      const model = new PostsFeedModel(store, 'custom', {
-        feed: uri,
-      })
-      model.setup()
-      return model
-    }, [store, uri])
-    const isPinned = store.preferences.isPinnedFeed(uri)
-    useSetTitle(feedInfo?.displayName)
-
-    // events
-    // =
-
-    const onToggleSaved = React.useCallback(async () => {
-      try {
-        Haptics.default()
-        if (feedInfo?.isSaved) {
-          await feedInfo?.unsave()
-        } else {
-          await feedInfo?.save()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue updating your feeds, please check your internet connection and try again.',
-        )
-        logger.error('Failed up update feeds', {error: err})
-      }
-    }, [feedInfo])
+  return (
+    <ProfileFeedScreenInner
+      preferences={preferences}
+      feedInfo={info as FeedSourceFeedInfo}
+      isPublic={Boolean(isPublic)}
+    />
+  )
+}
 
-    const onToggleLiked = React.useCallback(async () => {
+export function ProfileFeedScreenInner({
+  preferences,
+  feedInfo,
+  isPublic,
+}: {
+  preferences: UsePreferencesQueryResponse
+  feedInfo: FeedSourceFeedInfo
+  isPublic: boolean
+}) {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {hasSession, currentAccount} = useSession()
+  const {openModal} = useModalControls()
+  const {openComposer} = useComposerControls()
+  const {track} = useAnalytics()
+  const feedSectionRef = React.useRef<SectionRef>(null)
+
+  const {
+    mutateAsync: saveFeed,
+    variables: savedFeed,
+    reset: resetSaveFeed,
+    isPending: isSavePending,
+  } = useSaveFeedMutation()
+  const {
+    mutateAsync: removeFeed,
+    variables: removedFeed,
+    reset: resetRemoveFeed,
+    isPending: isRemovePending,
+  } = useRemoveFeedMutation()
+  const {
+    mutateAsync: pinFeed,
+    variables: pinnedFeed,
+    reset: resetPinFeed,
+    isPending: isPinPending,
+  } = usePinFeedMutation()
+  const {
+    mutateAsync: unpinFeed,
+    variables: unpinnedFeed,
+    reset: resetUnpinFeed,
+    isPending: isUnpinPending,
+  } = useUnpinFeedMutation()
+
+  const isSaved =
+    !removedFeed &&
+    (!!savedFeed || preferences.feeds.saved.includes(feedInfo.uri))
+  const isPinned =
+    !unpinnedFeed &&
+    (!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri))
+
+  useSetTitle(feedInfo?.displayName)
+
+  const onToggleSaved = React.useCallback(async () => {
+    try {
       Haptics.default()
-      try {
-        if (feedInfo?.isLiked) {
-          await feedInfo?.unlike()
-        } else {
-          await feedInfo?.like()
-        }
-      } catch (err) {
-        Toast.show(
-          'There was an an issue contacting the server, please check your internet connection and try again.',
-        )
-        logger.error('Failed up toggle like', {error: err})
+
+      if (isSaved) {
+        await removeFeed({uri: feedInfo.uri})
+        resetRemoveFeed()
+      } else {
+        await saveFeed({uri: feedInfo.uri})
+        resetSaveFeed()
       }
-    }, [feedInfo])
+    } catch (err) {
+      Toast.show(
+        'There was an an issue updating your feeds, please check your internet connection and try again.',
+      )
+      logger.error('Failed up update feeds', {error: err})
+    }
+  }, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed])
 
-    const onTogglePinned = React.useCallback(async () => {
+  const onTogglePinned = React.useCallback(async () => {
+    try {
       Haptics.default()
-      if (feedInfo) {
-        feedInfo.togglePin().catch(e => {
-          Toast.show('There was an issue contacting the server')
-          logger.error('Failed to toggle pinned feed', {error: e})
-        })
-      }
-    }, [feedInfo])
-
-    const onPressShare = React.useCallback(() => {
-      const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`)
-      shareUrl(url)
-      track('CustomFeed:Share')
-    }, [handleOrDid, rkey, track])
-
-    const onPressReport = React.useCallback(() => {
-      if (!feedInfo) return
-      store.shell.openModal({
-        name: 'report',
-        uri: feedInfo.uri,
-        cid: feedInfo.cid,
-      })
-    }, [store, feedInfo])
-
-    const onCurrentPageSelected = React.useCallback(
-      (index: number) => {
-        if (index === 0) {
-          feedSectionRef.current?.scrollToTop()
-        }
-      },
-      [feedSectionRef],
-    )
 
-    // render
-    // =
+      if (isPinned) {
+        await unpinFeed({uri: feedInfo.uri})
+        resetUnpinFeed()
+      } else {
+        await pinFeed({uri: feedInfo.uri})
+        resetPinFeed()
+      }
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to toggle pinned feed', {error: e})
+    }
+  }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed])
+
+  const onPressShare = React.useCallback(() => {
+    const url = toShareUrl(feedInfo.route.href)
+    shareUrl(url)
+    track('CustomFeed:Share')
+  }, [feedInfo, track])
+
+  const onPressReport = React.useCallback(() => {
+    if (!feedInfo) return
+    openModal({
+      name: 'report',
+      uri: feedInfo.uri,
+      cid: feedInfo.cid,
+    })
+  }, [openModal, feedInfo])
+
+  const onCurrentPageSelected = React.useCallback(
+    (index: number) => {
+      if (index === 0) {
+        feedSectionRef.current?.scrollToTop()
+      }
+    },
+    [feedSectionRef],
+  )
 
-    const dropdownItems: DropdownItem[] = React.useMemo(() => {
-      return [
-        {
-          testID: 'feedHeaderDropdownToggleSavedBtn',
-          label: feedInfo?.isSaved ? 'Remove from my feeds' : 'Add to my feeds',
-          onPress: onToggleSaved,
-          icon: feedInfo?.isSaved
-            ? {
-                ios: {
-                  name: 'trash',
-                },
-                android: 'ic_delete',
-                web: ['far', 'trash-can'],
-              }
-            : {
-                ios: {
-                  name: 'plus',
-                },
-                android: '',
-                web: 'plus',
+  // render
+  // =
+
+  const dropdownItems: DropdownItem[] = React.useMemo(() => {
+    return [
+      hasSession && {
+        testID: 'feedHeaderDropdownToggleSavedBtn',
+        label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
+        onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
+        icon: isSaved
+          ? {
+              ios: {
+                name: 'trash',
               },
-        },
-        {
-          testID: 'feedHeaderDropdownReportBtn',
-          label: 'Report feed',
-          onPress: onPressReport,
-          icon: {
-            ios: {
-              name: 'exclamationmark.triangle',
+              android: 'ic_delete',
+              web: ['far', 'trash-can'],
+            }
+          : {
+              ios: {
+                name: 'plus',
+              },
+              android: '',
+              web: 'plus',
             },
-            android: 'ic_menu_report_image',
-            web: 'circle-exclamation',
+      },
+      hasSession && {
+        testID: 'feedHeaderDropdownReportBtn',
+        label: _(msg`Report feed`),
+        onPress: onPressReport,
+        icon: {
+          ios: {
+            name: 'exclamationmark.triangle',
           },
+          android: 'ic_menu_report_image',
+          web: 'circle-exclamation',
         },
-        {
-          testID: 'feedHeaderDropdownShareBtn',
-          label: 'Share link',
-          onPress: onPressShare,
-          icon: {
-            ios: {
-              name: 'square.and.arrow.up',
-            },
-            android: 'ic_menu_share',
-            web: 'share',
+      },
+      {
+        testID: 'feedHeaderDropdownShareBtn',
+        label: _(msg`Share feed`),
+        onPress: onPressShare,
+        icon: {
+          ios: {
+            name: 'square.and.arrow.up',
           },
+          android: 'ic_menu_share',
+          web: 'share',
         },
-      ] as DropdownItem[]
-    }, [feedInfo, onToggleSaved, onPressReport, onPressShare])
-
-    const renderHeader = useCallback(() => {
-      return (
-        <ProfileSubpageHeader
-          isLoading={!feedInfo?.hasLoaded}
-          href={makeCustomFeedLink(feedOwnerDid, rkey)}
-          title={feedInfo?.displayName}
-          avatar={feedInfo?.avatar}
-          isOwner={feedInfo?.isOwner}
-          creator={
-            feedInfo
-              ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
-              : undefined
-          }
-          avatarType="algo">
-          {feedInfo && (
-            <>
-              <Button
-                type="default"
-                label={feedInfo?.isSaved ? 'Unsave' : 'Save'}
-                onPress={onToggleSaved}
-                style={styles.btn}
-              />
-              <Button
-                type={isPinned ? 'default' : 'inverted'}
-                label={isPinned ? 'Unpin' : 'Pin to home'}
-                onPress={onTogglePinned}
-                style={styles.btn}
-              />
-            </>
-          )}
-          <NativeDropdown
-            testID="headerDropdownBtn"
-            items={dropdownItems}
-            accessibilityLabel="More options"
-            accessibilityHint="">
-            <View style={[pal.viewLight, styles.btn]}>
-              <FontAwesomeIcon
-                icon="ellipsis"
-                size={20}
-                color={pal.colors.text}
-              />
-            </View>
-          </NativeDropdown>
-        </ProfileSubpageHeader>
-      )
-    }, [
-      pal,
-      feedOwnerDid,
-      rkey,
-      feedInfo,
-      isPinned,
-      onTogglePinned,
-      onToggleSaved,
-      dropdownItems,
-    ])
-
+      },
+    ].filter(Boolean) as DropdownItem[]
+  }, [
+    hasSession,
+    onToggleSaved,
+    onPressReport,
+    onPressShare,
+    isSaved,
+    isSavePending,
+    isRemovePending,
+    _,
+  ])
+
+  const renderHeader = useCallback(() => {
     return (
-      <View style={s.hContentRegion}>
-        <PagerWithHeader
-          items={SECTION_TITLES}
-          isHeaderReady={feedInfo?.hasLoaded ?? false}
-          renderHeader={renderHeader}
-          onCurrentPageSelected={onCurrentPageSelected}>
-          {({onScroll, headerHeight, isScrolledDown}) => (
+      <ProfileSubpageHeader
+        isLoading={false}
+        href={feedInfo.route.href}
+        title={feedInfo?.displayName}
+        avatar={feedInfo?.avatar}
+        isOwner={feedInfo.creatorDid === currentAccount?.did}
+        creator={
+          feedInfo
+            ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
+            : undefined
+        }
+        avatarType="algo">
+        {feedInfo && hasSession && (
+          <>
+            <Button
+              disabled={isSavePending || isRemovePending}
+              type="default"
+              label={isSaved ? 'Unsave' : 'Save'}
+              onPress={onToggleSaved}
+              style={styles.btn}
+            />
+            <Button
+              testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+              disabled={isPinPending || isUnpinPending}
+              type={isPinned ? 'default' : 'inverted'}
+              label={isPinned ? 'Unpin' : 'Pin to home'}
+              onPress={onTogglePinned}
+              style={styles.btn}
+            />
+          </>
+        )}
+        <NativeDropdown
+          testID="headerDropdownBtn"
+          items={dropdownItems}
+          accessibilityLabel={_(msg`More options`)}
+          accessibilityHint="">
+          <View style={[pal.viewLight, styles.btn]}>
+            <FontAwesomeIcon
+              icon="ellipsis"
+              size={20}
+              color={pal.colors.text}
+            />
+          </View>
+        </NativeDropdown>
+      </ProfileSubpageHeader>
+    )
+  }, [
+    _,
+    hasSession,
+    pal,
+    feedInfo,
+    isPinned,
+    onTogglePinned,
+    onToggleSaved,
+    dropdownItems,
+    currentAccount?.did,
+    isPinPending,
+    isRemovePending,
+    isSavePending,
+    isSaved,
+    isUnpinPending,
+  ])
+
+  return (
+    <View style={s.hContentRegion}>
+      <PagerWithHeader
+        items={SECTION_TITLES}
+        isHeaderReady={true}
+        renderHeader={renderHeader}
+        onCurrentPageSelected={onCurrentPageSelected}>
+        {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) =>
+          isPublic ? (
             <FeedSection
               ref={feedSectionRef}
-              feed={feed}
+              feed={`feedgen|${feedInfo.uri}`}
               onScroll={onScroll}
               headerHeight={headerHeight}
               isScrolledDown={isScrolledDown}
+              scrollElRef={
+                scrollElRef as React.MutableRefObject<FlatList<any> | null>
+              }
+              isFocused={isFocused}
             />
-          )}
-          {({onScroll, headerHeight}) => (
-            <AboutSection
-              feedOwnerDid={feedOwnerDid}
-              feedRkey={rkey}
-              feedInfo={feedInfo}
-              headerHeight={headerHeight}
-              onToggleLiked={onToggleLiked}
-              onScroll={onScroll}
-            />
-          )}
-        </PagerWithHeader>
+          ) : (
+            <CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
+              <NonPublicFeedMessage />
+            </CenteredView>
+          )
+        }
+        {({onScroll, headerHeight, scrollElRef}) => (
+          <AboutSection
+            feedOwnerDid={feedInfo.creatorDid}
+            feedRkey={feedInfo.route.params.rkey}
+            feedInfo={feedInfo}
+            headerHeight={headerHeight}
+            onScroll={onScroll}
+            scrollElRef={
+              scrollElRef as React.MutableRefObject<ScrollView | null>
+            }
+            isOwner={feedInfo.creatorDid === currentAccount?.did}
+          />
+        )}
+      </PagerWithHeader>
+      {hasSession && (
         <FAB
           testID="composeFAB"
-          onPress={() => store.shell.openComposer({})}
+          onPress={() => openComposer({})}
           icon={
             <ComposeIcon2
               strokeWidth={1.5}
@@ -372,32 +447,67 @@ export const ProfileFeedScreenInner = observer(
             />
           }
           accessibilityRole="button"
-          accessibilityLabel="New post"
+          accessibilityLabel={_(msg`New post`)}
           accessibilityHint=""
         />
+      )}
+    </View>
+  )
+}
+
+function NonPublicFeedMessage() {
+  const pal = usePalette('default')
+
+  return (
+    <View
+      style={[
+        pal.border,
+        {
+          padding: 18,
+          borderTopWidth: 1,
+          minHeight: Dimensions.get('window').height * 1.5,
+        },
+      ]}>
+      <View
+        style={[
+          pal.viewLight,
+          {
+            padding: 12,
+            borderRadius: 8,
+          },
+        ]}>
+        <Text style={[pal.text]}>
+          <Trans>
+            Looks like this feed is only available to users with a Bluesky
+            account. Please sign up or sign in to view this feed!
+          </Trans>
+        </Text>
       </View>
-    )
-  },
-)
+    </View>
+  )
+}
 
 interface FeedSectionProps {
-  feed: PostsFeedModel
-  onScroll: (e: NativeScrollEvent) => void
+  feed: FeedDescriptor
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | null>
+  isFocused: boolean
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl(
-    {feed, onScroll, headerHeight, isScrolledDown},
+    {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
     ref,
   ) {
-    const hasNew = feed.hasNewLatest && !feed.isRefreshing
-    const scrollElRef = React.useRef<FlatList>(null)
+    const [hasNew, setHasNew] = React.useState(false)
+    const queryClient = useQueryClient()
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      feed.refresh()
-    }, [feed, scrollElRef, headerHeight])
+      truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+      setHasNew(false)
+    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
 
     React.useImperativeHandle(ref, () => ({
       scrollToTop: onScrollToTop,
@@ -407,13 +517,15 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
       return <EmptyState icon="feed" message="This feed is empty!" />
     }, [])
 
-    const scrollHandler = useAnimatedScrollHandler({onScroll})
     return (
       <View>
         <Feed
+          enabled={isFocused}
           feed={feed}
+          pollInterval={30e3}
           scrollElRef={scrollElRef}
-          onScroll={scrollHandler}
+          onHasNew={setHasNew}
+          onScroll={onScroll}
           scrollEventThrottle={5}
           renderEmptyState={renderPostsEmpty}
           headerOffset={headerHeight}
@@ -430,32 +542,64 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   },
 )
 
-const AboutSection = observer(function AboutPageImpl({
+function AboutSection({
   feedOwnerDid,
   feedRkey,
   feedInfo,
   headerHeight,
-  onToggleLiked,
   onScroll,
+  scrollElRef,
+  isOwner,
 }: {
   feedOwnerDid: string
   feedRkey: string
-  feedInfo: FeedSourceModel | undefined
+  feedInfo: FeedSourceFeedInfo
   headerHeight: number
-  onToggleLiked: () => void
-  onScroll: (e: NativeScrollEvent) => void
+  onScroll: OnScrollHandler
+  scrollElRef: React.MutableRefObject<ScrollView | null>
+  isOwner: boolean
 }) {
   const pal = usePalette('default')
-  const scrollHandler = useAnimatedScrollHandler({onScroll})
+  const {_} = useLingui()
+  const scrollHandler = useAnimatedScrollHandler(onScroll)
+  const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
+  const {hasSession} = useSession()
 
-  if (!feedInfo) {
-    return <View />
-  }
+  const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
+  const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
+    useUnlikeMutation()
+
+  const isLiked = !!likeUri
+  const likeCount =
+    isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount
+
+  const onToggleLiked = React.useCallback(async () => {
+    try {
+      Haptics.default()
+
+      if (isLiked && likeUri) {
+        await unlikeFeed({uri: likeUri})
+        setLikeUri('')
+      } else {
+        const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid})
+        setLikeUri(res.uri)
+      }
+    } catch (err) {
+      Toast.show(
+        'There was an an issue contacting the server, please check your internet connection and try again.',
+      )
+      logger.error('Failed up toggle like', {error: err})
+    }
+  }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed])
 
   return (
     <ScrollView
+      ref={scrollElRef}
       scrollEventThrottle={1}
-      contentContainerStyle={{paddingTop: headerHeight}}
+      contentContainerStyle={{
+        paddingTop: headerHeight,
+        minHeight: Dimensions.get('window').height * 1.5,
+      }}
       onScroll={scrollHandler}>
       <View
         style={[
@@ -467,46 +611,44 @@ const AboutSection = observer(function AboutPageImpl({
           },
           pal.border,
         ]}>
-        {feedInfo.descriptionRT ? (
+        {feedInfo.description ? (
           <RichText
             testID="listDescription"
             type="lg"
             style={pal.text}
-            richText={feedInfo.descriptionRT}
+            richText={feedInfo.description}
           />
         ) : (
           <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
-            No description
+            <Trans>No description</Trans>
           </Text>
         )}
         <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
           <Button
             type="default"
             testID="toggleLikeBtn"
-            accessibilityLabel="Like this feed"
+            accessibilityLabel={_(msg`Like this feed`)}
             accessibilityHint=""
+            disabled={!hasSession || isLikePending || isUnlikePending}
             onPress={onToggleLiked}
             style={{paddingHorizontal: 10}}>
-            {feedInfo?.isLiked ? (
+            {isLiked ? (
               <HeartIconSolid size={19} style={styles.liked} />
             ) : (
               <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
             )}
           </Button>
-          {typeof feedInfo.likeCount === 'number' && (
+          {typeof likeCount === 'number' && (
             <TextLink
               href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-              text={`Liked by ${feedInfo.likeCount} ${pluralize(
-                feedInfo.likeCount,
-                'user',
-              )}`}
+              text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`}
               style={[pal.textLight, s.semiBold]}
             />
           )}
         </View>
         <Text type="md" style={[pal.textLight]} numberOfLines={1}>
           Created by{' '}
-          {feedInfo.isOwner ? (
+          {isOwner ? (
             'you'
           ) : (
             <TextLink
@@ -522,7 +664,7 @@ const AboutSection = observer(function AboutPageImpl({
       </View>
     </ScrollView>
   )
-})
+}
 
 const styles = StyleSheet.create({
   btn: {
diff --git a/src/view/screens/ProfileFeedLikedBy.tsx b/src/view/screens/ProfileFeedLikedBy.tsx
index 4972116f3..0460670e1 100644
--- a/src/view/screens/ProfileFeedLikedBy.tsx
+++ b/src/view/screens/ProfileFeedLikedBy.tsx
@@ -2,17 +2,19 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'>
-export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => {
+export const ProfileFeedLikedByScreen = ({route}: Props) => {
   const setMinimalShellMode = useSetMinimalShellMode()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -22,8 +24,8 @@ export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Liked by" />
+      <ViewHeader title={_(msg`Liked by`)} />
       <PostLikedByComponent uri={uri} />
     </View>
   )
-})
+}
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 49f55bf46..2cad08cb5 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -2,15 +2,17 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
-export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
+export const ProfileFollowersScreen = ({route}: Props) => {
   const {name} = route.params
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -20,8 +22,8 @@ export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Followers" />
+      <ViewHeader title={_(msg`Followers`)} />
       <ProfileFollowersComponent name={name} />
     </View>
   )
-})
+}
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 4f0ff7d67..80502b98b 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -2,15 +2,17 @@ import React from 'react'
 import {View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
-export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
+export const ProfileFollowsScreen = ({route}: Props) => {
   const {name} = route.params
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -20,8 +22,8 @@ export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Following" />
+      <ViewHeader title={_(msg`Following`)} />
       <ProfileFollowsComponent name={name} />
     </View>
   )
-})
+}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index b84732d53..421611764 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -2,7 +2,6 @@ import React, {useCallback, useMemo} from 'react'
 import {
   ActivityIndicator,
   FlatList,
-  NativeScrollEvent,
   Pressable,
   StyleSheet,
   View,
@@ -11,10 +10,8 @@ import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {useNavigation} from '@react-navigation/native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useAnimatedScrollHandler} from 'react-native-reanimated'
-import {observer} from 'mobx-react-lite'
-import {RichText as RichTextAPI} from '@atproto/api'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api'
+import {useQueryClient} from '@tanstack/react-query'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
 import {Feed} from 'view/com/posts/Feed'
@@ -29,23 +26,36 @@ import * as Toast from 'view/com/util/Toast'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {FAB} from 'view/com/util/fab/FAB'
 import {Haptics} from 'lib/haptics'
-import {ListModel} from 'state/models/content/list'
-import {PostsFeedModel} from 'state/models/feeds/posts'
-import {useStores} from 'state/index'
+import {FeedDescriptor} from '#/state/queries/post-feed'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {NavigationProp} from 'lib/routes/types'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {shareUrl} from 'lib/sharing'
-import {resolveName} from 'lib/api'
 import {s} from 'lib/styles'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink, makeListLink} from 'lib/routes/links'
 import {ComposeIcon2} from 'lib/icons'
-import {ListItems} from 'view/com/lists/ListItems'
-import {logger} from '#/logger'
+import {ListMembers} from '#/view/com/lists/ListMembers'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {
+  useListQuery,
+  useListMuteMutation,
+  useListBlockMutation,
+  useListDeleteMutation,
+} from '#/state/queries/list'
+import {cleanError} from '#/lib/strings/errors'
+import {useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell/composer'
+import {isWeb} from '#/platform/detection'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -55,240 +65,220 @@ interface SectionRef {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
-export const ProfileListScreen = withAuthRequired(
-  observer(function ProfileListScreenImpl(props: Props) {
-    const store = useStores()
-    const {name: handleOrDid} = props.route.params
-    const [listOwnerDid, setListOwnerDid] = React.useState<string | undefined>()
-    const [error, setError] = React.useState<string | undefined>()
-
-    React.useEffect(() => {
-      /*
-       * We must resolve the DID of the list owner before we can fetch the list.
-       */
-      async function fetchDid() {
-        try {
-          const did = await resolveName(store, handleOrDid)
-          setListOwnerDid(did)
-        } catch (e) {
-          setError(
-            `We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
-          )
-        }
-      }
-
-      fetchDid()
-    }, [store, handleOrDid, setListOwnerDid])
-
-    if (error) {
-      return (
-        <CenteredView>
-          <ErrorScreen error={error} />
-        </CenteredView>
-      )
-    }
+export function ProfileListScreen(props: Props) {
+  const {name: handleOrDid, rkey} = props.route.params
+  const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
+    AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
+  )
+  const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
 
-    return listOwnerDid ? (
-      <ProfileListScreenInner {...props} listOwnerDid={listOwnerDid} />
-    ) : (
+  if (resolveError) {
+    return (
       <CenteredView>
-        <View style={s.p20}>
-          <ActivityIndicator size="large" />
-        </View>
+        <ErrorScreen
+          error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`}
+        />
       </CenteredView>
     )
-  }),
-)
-
-export const ProfileListScreenInner = observer(
-  function ProfileListScreenInnerImpl({
-    route,
-    listOwnerDid,
-  }: Props & {listOwnerDid: string}) {
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const {rkey} = route.params
-    const feedSectionRef = React.useRef<SectionRef>(null)
-    const aboutSectionRef = React.useRef<SectionRef>(null)
-
-    const list: ListModel = useMemo(() => {
-      const model = new ListModel(
-        store,
-        `at://${listOwnerDid}/app.bsky.graph.list/${rkey}`,
-      )
-      return model
-    }, [store, listOwnerDid, rkey])
-    const feed = useMemo(
-      () => new PostsFeedModel(store, 'list', {list: list.uri}),
-      [store, list],
-    )
-    useSetTitle(list.data?.name)
-
-    useFocusEffect(
-      useCallback(() => {
-        setMinimalShellMode(false)
-        list.loadMore(true).then(() => {
-          if (list.isCuratelist) {
-            feed.setup()
-          }
-        })
-      }, [setMinimalShellMode, list, feed]),
+  }
+  if (listError) {
+    return (
+      <CenteredView>
+        <ErrorScreen error={cleanError(listError)} />
+      </CenteredView>
     )
+  }
+
+  return resolvedUri && list ? (
+    <ProfileListScreenLoaded {...props} uri={resolvedUri.uri} list={list} />
+  ) : (
+    <CenteredView>
+      <View style={s.p20}>
+        <ActivityIndicator size="large" />
+      </View>
+    </CenteredView>
+  )
+}
 
-    const onPressAddUser = useCallback(() => {
-      store.shell.openModal({
-        name: 'list-add-user',
-        list,
-        onAdd() {
-          if (list.isCuratelist) {
-            feed.refresh()
-          }
-        },
-      })
-    }, [store, list, feed])
+function ProfileListScreenLoaded({
+  route,
+  uri,
+  list,
+}: Props & {uri: string; list: AppBskyGraphDefs.ListView}) {
+  const {_} = useLingui()
+  const queryClient = useQueryClient()
+  const {openComposer} = useComposerControls()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {rkey} = route.params
+  const feedSectionRef = React.useRef<SectionRef>(null)
+  const aboutSectionRef = React.useRef<SectionRef>(null)
+  const {openModal} = useModalControls()
+  const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
+
+  useSetTitle(list.name)
+
+  useFocusEffect(
+    useCallback(() => {
+      setMinimalShellMode(false)
+    }, [setMinimalShellMode]),
+  )
 
-    const onCurrentPageSelected = React.useCallback(
-      (index: number) => {
-        if (index === 0) {
-          feedSectionRef.current?.scrollToTop()
-        }
-        if (index === 1) {
-          aboutSectionRef.current?.scrollToTop()
+  const onPressAddUser = useCallback(() => {
+    openModal({
+      name: 'list-add-remove-users',
+      list,
+      onChange() {
+        if (isCurateList) {
+          // TODO(eric) should construct these strings with a fn too
+          truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
         }
       },
-      [feedSectionRef],
-    )
+    })
+  }, [openModal, list, isCurateList, queryClient])
+
+  const onCurrentPageSelected = React.useCallback(
+    (index: number) => {
+      if (index === 0) {
+        feedSectionRef.current?.scrollToTop()
+      } else if (index === 1) {
+        aboutSectionRef.current?.scrollToTop()
+      }
+    },
+    [feedSectionRef],
+  )
 
-    const renderHeader = useCallback(() => {
-      return <Header rkey={rkey} list={list} />
-    }, [rkey, list])
+  const renderHeader = useCallback(() => {
+    return <Header rkey={rkey} list={list} />
+  }, [rkey, list])
 
-    if (list.isCuratelist) {
-      return (
-        <View style={s.hContentRegion}>
-          <PagerWithHeader
-            items={SECTION_TITLES_CURATE}
-            isHeaderReady={list.hasLoaded}
-            renderHeader={renderHeader}
-            onCurrentPageSelected={onCurrentPageSelected}>
-            {({onScroll, headerHeight, isScrolledDown}) => (
-              <FeedSection
-                ref={feedSectionRef}
-                feed={feed}
-                onScroll={onScroll}
-                headerHeight={headerHeight}
-                isScrolledDown={isScrolledDown}
-              />
-            )}
-            {({onScroll, headerHeight, isScrolledDown}) => (
-              <AboutSection
-                ref={aboutSectionRef}
-                list={list}
-                descriptionRT={list.descriptionRT}
-                creator={list.data ? list.data.creator : undefined}
-                isCurateList={list.isCuratelist}
-                isOwner={list.isOwner}
-                onPressAddUser={onPressAddUser}
-                onScroll={onScroll}
-                headerHeight={headerHeight}
-                isScrolledDown={isScrolledDown}
-              />
-            )}
-          </PagerWithHeader>
-          <FAB
-            testID="composeFAB"
-            onPress={() => store.shell.openComposer({})}
-            icon={
-              <ComposeIcon2
-                strokeWidth={1.5}
-                size={29}
-                style={{color: 'white'}}
-              />
-            }
-            accessibilityRole="button"
-            accessibilityLabel="New post"
-            accessibilityHint=""
-          />
-        </View>
-      )
-    }
-    if (list.isModlist) {
-      return (
-        <View style={s.hContentRegion}>
-          <PagerWithHeader
-            items={SECTION_TITLES_MOD}
-            isHeaderReady={list.hasLoaded}
-            renderHeader={renderHeader}>
-            {({onScroll, headerHeight, isScrolledDown}) => (
-              <AboutSection
-                list={list}
-                descriptionRT={list.descriptionRT}
-                creator={list.data ? list.data.creator : undefined}
-                isCurateList={list.isCuratelist}
-                isOwner={list.isOwner}
-                onPressAddUser={onPressAddUser}
-                onScroll={onScroll}
-                headerHeight={headerHeight}
-                isScrolledDown={isScrolledDown}
-              />
-            )}
-          </PagerWithHeader>
-          <FAB
-            testID="composeFAB"
-            onPress={() => store.shell.openComposer({})}
-            icon={
-              <ComposeIcon2
-                strokeWidth={1.5}
-                size={29}
-                style={{color: 'white'}}
-              />
-            }
-            accessibilityRole="button"
-            accessibilityLabel="New post"
-            accessibilityHint=""
-          />
-        </View>
-      )
-    }
+  if (isCurateList) {
     return (
-      <CenteredView sideBorders style={s.hContentRegion}>
-        <Header rkey={rkey} list={list} />
-        {list.error ? <ErrorScreen error={list.error} /> : null}
-      </CenteredView>
+      <View style={s.hContentRegion}>
+        <PagerWithHeader
+          items={SECTION_TITLES_CURATE}
+          isHeaderReady={true}
+          renderHeader={renderHeader}
+          onCurrentPageSelected={onCurrentPageSelected}>
+          {({
+            onScroll,
+            headerHeight,
+            isScrolledDown,
+            scrollElRef,
+            isFocused,
+          }) => (
+            <FeedSection
+              ref={feedSectionRef}
+              feed={`list|${uri}`}
+              scrollElRef={
+                scrollElRef as React.MutableRefObject<FlatList<any> | null>
+              }
+              onScroll={onScroll}
+              headerHeight={headerHeight}
+              isScrolledDown={isScrolledDown}
+              isFocused={isFocused}
+            />
+          )}
+          {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
+            <AboutSection
+              ref={aboutSectionRef}
+              scrollElRef={
+                scrollElRef as React.MutableRefObject<FlatList<any> | null>
+              }
+              list={list}
+              onPressAddUser={onPressAddUser}
+              onScroll={onScroll}
+              headerHeight={headerHeight}
+              isScrolledDown={isScrolledDown}
+            />
+          )}
+        </PagerWithHeader>
+        <FAB
+          testID="composeFAB"
+          onPress={() => openComposer({})}
+          icon={
+            <ComposeIcon2
+              strokeWidth={1.5}
+              size={29}
+              style={{color: 'white'}}
+            />
+          }
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`New post`)}
+          accessibilityHint=""
+        />
+      </View>
     )
-  },
-)
+  }
+  return (
+    <View style={s.hContentRegion}>
+      <PagerWithHeader
+        items={SECTION_TITLES_MOD}
+        isHeaderReady={true}
+        renderHeader={renderHeader}>
+        {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
+          <AboutSection
+            list={list}
+            scrollElRef={
+              scrollElRef as React.MutableRefObject<FlatList<any> | null>
+            }
+            onPressAddUser={onPressAddUser}
+            onScroll={onScroll}
+            headerHeight={headerHeight}
+            isScrolledDown={isScrolledDown}
+          />
+        )}
+      </PagerWithHeader>
+      <FAB
+        testID="composeFAB"
+        onPress={() => openComposer({})}
+        icon={
+          <ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} />
+        }
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`New post`)}
+        accessibilityHint=""
+      />
+    </View>
+  )
+}
 
-const Header = observer(function HeaderImpl({
-  rkey,
-  list,
-}: {
-  rkey: string
-  list: ListModel
-}) {
+function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
-  const store = useStores()
+  const {_} = useLingui()
   const navigation = useNavigation<NavigationProp>()
+  const {currentAccount} = useSession()
+  const {openModal, closeModal} = useModalControls()
+  const listMuteMutation = useListMuteMutation()
+  const listBlockMutation = useListBlockMutation()
+  const listDeleteMutation = useListDeleteMutation()
+  const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
+  const isModList = list.purpose === 'app.bsky.graph.defs#modlist'
+  const isPinned = false // TODO
+  const isBlocking = !!list.viewer?.blocked
+  const isMuting = !!list.viewer?.muted
+  const isOwner = list.creator.did === currentAccount?.did
 
   const onTogglePinned = useCallback(async () => {
     Haptics.default()
-    list.togglePin().catch(e => {
-      Toast.show('There was an issue contacting the server')
-      logger.error('Failed to toggle pinned list', {error: e})
-    })
-  }, [list])
+    // TODO
+    // list.togglePin().catch(e => {
+    //   Toast.show('There was an issue contacting the server')
+    //   logger.error('Failed to toggle pinned list', {error: e})
+    // })
+  }, [])
 
   const onSubscribeMute = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Mute these accounts?',
-      message:
-        'Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.',
+      title: _(msg`Mute these accounts?`),
+      message: _(
+        msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
+      ),
       confirmBtnText: 'Mute this List',
       async onPressConfirm() {
         try {
-          await list.mute()
+          await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
           Toast.show('List muted')
         } catch {
           Toast.show(
@@ -297,32 +287,33 @@ const Header = observer(function HeaderImpl({
         }
       },
       onPressCancel() {
-        store.shell.closeModal()
+        closeModal()
       },
     })
-  }, [store, list])
+  }, [openModal, closeModal, list, listMuteMutation, _])
 
   const onUnsubscribeMute = useCallback(async () => {
     try {
-      await list.unmute()
+      await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
       Toast.show('List unmuted')
     } catch {
       Toast.show(
         'There was an issue. Please check your internet connection and try again.',
       )
     }
-  }, [list])
+  }, [list, listMuteMutation])
 
   const onSubscribeBlock = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Block these accounts?',
-      message:
-        'Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
+      title: _(msg`Block these accounts?`),
+      message: _(
+        msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
+      ),
       confirmBtnText: 'Block this List',
       async onPressConfirm() {
         try {
-          await list.block()
+          await listBlockMutation.mutateAsync({uri: list.uri, block: true})
           Toast.show('List blocked')
         } catch {
           Toast.show(
@@ -331,39 +322,36 @@ const Header = observer(function HeaderImpl({
         }
       },
       onPressCancel() {
-        store.shell.closeModal()
+        closeModal()
       },
     })
-  }, [store, list])
+  }, [openModal, closeModal, list, listBlockMutation, _])
 
   const onUnsubscribeBlock = useCallback(async () => {
     try {
-      await list.unblock()
+      await listBlockMutation.mutateAsync({uri: list.uri, block: false})
       Toast.show('List unblocked')
     } catch {
       Toast.show(
         'There was an issue. Please check your internet connection and try again.',
       )
     }
-  }, [list])
+  }, [list, listBlockMutation])
 
   const onPressEdit = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'create-or-edit-list',
       list,
-      onSave() {
-        list.refresh()
-      },
     })
-  }, [store, list])
+  }, [openModal, list])
 
   const onPressDelete = useCallback(() => {
-    store.shell.openModal({
+    openModal({
       name: 'confirm',
-      title: 'Delete List',
-      message: 'Are you sure?',
+      title: _(msg`Delete List`),
+      message: _(msg`Are you sure?`),
       async onPressConfirm() {
-        await list.delete()
+        await listDeleteMutation.mutateAsync({uri: list.uri})
         Toast.show('List deleted')
         if (navigation.canGoBack()) {
           navigation.goBack()
@@ -372,30 +360,26 @@ const Header = observer(function HeaderImpl({
         }
       },
     })
-  }, [store, list, navigation])
+  }, [openModal, list, listDeleteMutation, navigation, _])
 
   const onPressReport = useCallback(() => {
-    if (!list.data) return
-    store.shell.openModal({
+    openModal({
       name: 'report',
       uri: list.uri,
-      cid: list.data.cid,
+      cid: list.cid,
     })
-  }, [store, list])
+  }, [openModal, list])
 
   const onPressShare = useCallback(() => {
-    const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`)
+    const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
     shareUrl(url)
-  }, [list.creatorDid, rkey])
+  }, [list, rkey])
 
   const dropdownItems: DropdownItem[] = useMemo(() => {
-    if (!list.hasLoaded) {
-      return []
-    }
     let items: DropdownItem[] = [
       {
         testID: 'listHeaderDropdownShareBtn',
-        label: 'Share',
+        label: isWeb ? _(msg`Copy link to list`) : _(msg`Share`),
         onPress: onPressShare,
         icon: {
           ios: {
@@ -406,11 +390,11 @@ const Header = observer(function HeaderImpl({
         },
       },
     ]
-    if (list.isOwner) {
+    if (isOwner) {
       items.push({label: 'separator'})
       items.push({
         testID: 'listHeaderDropdownEditBtn',
-        label: 'Edit List Details',
+        label: _(msg`Edit list details`),
         onPress: onPressEdit,
         icon: {
           ios: {
@@ -422,7 +406,7 @@ const Header = observer(function HeaderImpl({
       })
       items.push({
         testID: 'listHeaderDropdownDeleteBtn',
-        label: 'Delete List',
+        label: _(msg`Delete List`),
         onPress: onPressDelete,
         icon: {
           ios: {
@@ -436,7 +420,7 @@ const Header = observer(function HeaderImpl({
       items.push({label: 'separator'})
       items.push({
         testID: 'listHeaderDropdownReportBtn',
-        label: 'Report List',
+        label: _(msg`Report List`),
         onPress: onPressReport,
         icon: {
           ios: {
@@ -448,20 +432,13 @@ const Header = observer(function HeaderImpl({
       })
     }
     return items
-  }, [
-    list.hasLoaded,
-    list.isOwner,
-    onPressShare,
-    onPressEdit,
-    onPressDelete,
-    onPressReport,
-  ])
+  }, [isOwner, onPressShare, onPressEdit, onPressDelete, onPressReport, _])
 
   const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
     return [
       {
         testID: 'subscribeDropdownMuteBtn',
-        label: 'Mute accounts',
+        label: _(msg`Mute accounts`),
         onPress: onSubscribeMute,
         icon: {
           ios: {
@@ -473,7 +450,7 @@ const Header = observer(function HeaderImpl({
       },
       {
         testID: 'subscribeDropdownBlockBtn',
-        label: 'Block accounts',
+        label: _(msg`Block accounts`),
         onPress: onSubscribeBlock,
         icon: {
           ios: {
@@ -484,36 +461,32 @@ const Header = observer(function HeaderImpl({
         },
       },
     ]
-  }, [onSubscribeMute, onSubscribeBlock])
+  }, [onSubscribeMute, onSubscribeBlock, _])
 
   return (
     <ProfileSubpageHeader
-      isLoading={!list.hasLoaded}
-      href={makeListLink(
-        list.data?.creator.handle || list.data?.creator.did || '',
-        rkey,
-      )}
-      title={list.data?.name || 'User list'}
-      avatar={list.data?.avatar}
-      isOwner={list.isOwner}
-      creator={list.data?.creator}
+      href={makeListLink(list.creator.handle || list.creator.did || '', rkey)}
+      title={list.name}
+      avatar={list.avatar}
+      isOwner={list.creator.did === currentAccount?.did}
+      creator={list.creator}
       avatarType="list">
-      {list.isCuratelist || list.isPinned ? (
+      {isCurateList || isPinned ? (
         <Button
           testID={list.isPinned ? 'unpinBtn' : 'pinBtn'}
           type={list.isPinned ? 'default' : 'inverted'}
           label={list.isPinned ? 'Unpin' : 'Pin to home'}
           onPress={onTogglePinned}
         />
-      ) : list.isModlist ? (
-        list.isBlocking ? (
+      ) : isModList ? (
+        isBlocking ? (
           <Button
             testID="unblockBtn"
             type="default"
             label="Unblock"
             onPress={onUnsubscribeBlock}
           />
-        ) : list.isMuting ? (
+        ) : isMuting ? (
           <Button
             testID="unmuteBtn"
             type="default"
@@ -524,10 +497,12 @@ const Header = observer(function HeaderImpl({
           <NativeDropdown
             testID="subscribeBtn"
             items={subscribeDropdownItems}
-            accessibilityLabel="Subscribe to this list"
+            accessibilityLabel={_(msg`Subscribe to this list`)}
             accessibilityHint="">
             <View style={[palInverted.view, styles.btn]}>
-              <Text style={palInverted.text}>Subscribe</Text>
+              <Text style={palInverted.text}>
+                <Trans>Subscribe</Trans>
+              </Text>
             </View>
           </NativeDropdown>
         )
@@ -535,7 +510,7 @@ const Header = observer(function HeaderImpl({
       <NativeDropdown
         testID="headerDropdownBtn"
         items={dropdownItems}
-        accessibilityLabel="More options"
+        accessibilityLabel={_(msg`More options`)}
         accessibilityHint="">
         <View style={[pal.viewLight, styles.btn]}>
           <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
@@ -543,26 +518,29 @@ const Header = observer(function HeaderImpl({
       </NativeDropdown>
     </ProfileSubpageHeader>
   )
-})
+}
 
 interface FeedSectionProps {
-  feed: PostsFeedModel
-  onScroll: (e: NativeScrollEvent) => void
+  feed: FeedDescriptor
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | null>
+  isFocused: boolean
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl(
-    {feed, onScroll, headerHeight, isScrolledDown},
+    {feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused},
     ref,
   ) {
-    const hasNew = feed.hasNewLatest && !feed.isRefreshing
-    const scrollElRef = React.useRef<FlatList>(null)
+    const queryClient = useQueryClient()
+    const [hasNew, setHasNew] = React.useState(false)
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      feed.refresh()
-    }, [feed, scrollElRef, headerHeight])
+      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
+      setHasNew(false)
+    }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
     React.useImperativeHandle(ref, () => ({
       scrollToTop: onScrollToTop,
     }))
@@ -571,14 +549,16 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
       return <EmptyState icon="feed" message="This feed is empty!" />
     }, [])
 
-    const scrollHandler = useAnimatedScrollHandler({onScroll})
     return (
       <View>
         <Feed
           testID="listFeed"
+          enabled={isFocused}
           feed={feed}
+          pollInterval={30e3}
           scrollElRef={scrollElRef}
-          onScroll={scrollHandler}
+          onHasNew={setHasNew}
+          onScroll={onScroll}
           scrollEventThrottle={1}
           renderEmptyState={renderPostsEmpty}
           headerOffset={headerHeight}
@@ -596,34 +576,35 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
 )
 
 interface AboutSectionProps {
-  list: ListModel
-  descriptionRT: RichTextAPI | null
-  creator: {did: string; handle: string} | undefined
-  isCurateList: boolean | undefined
-  isOwner: boolean | undefined
+  list: AppBskyGraphDefs.ListView
   onPressAddUser: () => void
-  onScroll: (e: NativeScrollEvent) => void
+  onScroll: OnScrollHandler
   headerHeight: number
   isScrolledDown: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | null>
 }
 const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
   function AboutSectionImpl(
-    {
-      list,
-      descriptionRT,
-      creator,
-      isCurateList,
-      isOwner,
-      onPressAddUser,
-      onScroll,
-      headerHeight,
-      isScrolledDown,
-    },
+    {list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef},
     ref,
   ) {
     const pal = usePalette('default')
+    const {_} = useLingui()
     const {isMobile} = useWebMediaQueries()
-    const scrollElRef = React.useRef<FlatList>(null)
+    const {currentAccount} = useSession()
+    const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
+    const isOwner = list.creator.did === currentAccount?.did
+
+    const descriptionRT = useMemo(
+      () =>
+        list.description
+          ? new RichTextAPI({
+              text: list.description,
+              facets: list.descriptionFacets,
+            })
+          : undefined,
+      [list],
+    )
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
@@ -634,9 +615,6 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
     }))
 
     const renderHeader = React.useCallback(() => {
-      if (!list.data) {
-        return <View />
-      }
       return (
         <View>
           <View
@@ -660,7 +638,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 testID="listDescriptionEmpty"
                 type="lg"
                 style={[{fontStyle: 'italic'}, pal.textLight]}>
-                No description
+                <Trans>No description</Trans>
               </Text>
             )}
             <Text type="md" style={[pal.textLight]} numberOfLines={1}>
@@ -669,8 +647,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 'you'
               ) : (
                 <TextLink
-                  text={sanitizeHandle(creator?.handle || '', '@')}
-                  href={creator ? makeProfileLink(creator) : ''}
+                  text={sanitizeHandle(list.creator.handle || '', '@')}
+                  href={makeProfileLink(list.creator)}
                   style={pal.textLight}
                 />
               )}
@@ -686,12 +664,14 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                 paddingBottom: isMobile ? 14 : 18,
               },
             ]}>
-            <Text type="lg-bold">Users</Text>
+            <Text type="lg-bold">
+              <Trans>Users</Trans>
+            </Text>
             {isOwner && (
               <Pressable
                 testID="addUserBtn"
                 accessibilityRole="button"
-                accessibilityLabel="Add a user to this list"
+                accessibilityLabel={_(msg`Add a user to this list`)}
                 accessibilityHint=""
                 onPress={onPressAddUser}
                 style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
@@ -700,7 +680,9 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
                   color={pal.colors.link}
                   size={16}
                 />
-                <Text style={pal.link}>Add</Text>
+                <Text style={pal.link}>
+                  <Trans>Add</Trans>
+                </Text>
               </Pressable>
             )}
           </View>
@@ -708,13 +690,13 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
       )
     }, [
       pal,
-      list.data,
+      list,
       isMobile,
       descriptionRT,
-      creator,
       isCurateList,
       isOwner,
       onPressAddUser,
+      _,
     ])
 
     const renderEmptyState = useCallback(() => {
@@ -727,17 +709,16 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
       )
     }, [])
 
-    const scrollHandler = useAnimatedScrollHandler({onScroll})
     return (
       <View>
-        <ListItems
+        <ListMembers
           testID="listItems"
+          list={list.uri}
           scrollElRef={scrollElRef}
           renderHeader={renderHeader}
           renderEmptyState={renderEmptyState}
-          list={list}
           headerOffset={headerHeight}
-          onScroll={scrollHandler}
+          onScroll={onScroll}
           scrollEventThrottle={1}
         />
         {isScrolledDown && (
@@ -755,6 +736,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
 function ErrorScreen({error}: {error: string}) {
   const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
+  const {_} = useLingui()
   const onPressBack = useCallback(() => {
     if (navigation.canGoBack()) {
       navigation.goBack()
@@ -776,7 +758,7 @@ function ErrorScreen({error}: {error: string}) {
         },
       ]}>
       <Text type="title-lg" style={[pal.text, s.mb10]}>
-        Could not load list
+        <Trans>Could not load list</Trans>
       </Text>
       <Text type="md" style={[pal.text, s.mb20]}>
         {error}
@@ -785,12 +767,12 @@ function ErrorScreen({error}: {error: string}) {
       <View style={{flexDirection: 'row'}}>
         <Button
           type="default"
-          accessibilityLabel="Go Back"
+          accessibilityLabel={_(msg`Go Back`)}
           accessibilityHint="Return to previous page"
           onPress={onPressBack}
           style={{flexShrink: 1}}>
           <Text type="button" style={pal.text}>
-            Go Back
+            <Trans>Go Back</Trans>
           </Text>
         </Button>
       </View>
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 487f56643..858a58a3c 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,33 +1,32 @@
-import React, {useCallback, useMemo} from 'react'
-import {
-  StyleSheet,
-  View,
-  ActivityIndicator,
-  Pressable,
-  TouchableOpacity,
-} from 'react-native'
+import React from 'react'
+import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
+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 {useStores} from 'state/index'
-import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {ScrollView, CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {s, colors} from 'lib/styles'
 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {FeedSourceModel} from 'state/models/content/feed-source'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as Toast from 'view/com/util/Toast'
 import {Haptics} from 'lib/haptics'
 import {TextLink} from 'view/com/util/Link'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+  useSetSaveFeedsMutation,
+} from '#/state/queries/preferences'
+import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -43,99 +42,118 @@ const HITSLOP_BOTTOM = {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
-export const SavedFeeds = withAuthRequired(
-  observer(function SavedFeedsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
-    const {screen} = useAnalytics()
-    const setMinimalShellMode = useSetMinimalShellMode()
+export function SavedFeeds({}: Props) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+  const {screen} = useAnalytics()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {data: preferences} = usePreferencesQuery()
+  const {
+    mutateAsync: setSavedFeeds,
+    variables: optimisticSavedFeedsResponse,
+    reset: resetSaveFeedsMutationState,
+    error: setSavedFeedsError,
+  } = useSetSaveFeedsMutation()
+
+  /*
+   * Use optimistic data if exists and no error, otherwise fallback to remote
+   * data
+   */
+  const currentFeeds =
+    optimisticSavedFeedsResponse && !setSavedFeedsError
+      ? optimisticSavedFeedsResponse
+      : preferences?.feeds || {saved: [], pinned: []}
+  const unpinned = currentFeeds.saved.filter(f => {
+    return !currentFeeds.pinned?.includes(f)
+  })
 
-    const savedFeeds = useMemo(() => {
-      const model = new SavedFeedsModel(store)
-      model.refresh()
-      return model
-    }, [store])
-    useFocusEffect(
-      useCallback(() => {
-        screen('SavedFeeds')
-        setMinimalShellMode(false)
-        savedFeeds.refresh()
-      }, [screen, setMinimalShellMode, savedFeeds]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('SavedFeeds')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    return (
-      <CenteredView
-        style={[
-          s.hContentRegion,
-          pal.border,
-          isTabletOrDesktop && styles.desktopContainer,
-        ]}>
-        <ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
-        <ScrollView style={s.flex1}>
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Pinned Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.pinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any pinned feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.pinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+  return (
+    <CenteredView
+      style={[
+        s.hContentRegion,
+        pal.border,
+        isTabletOrDesktop && styles.desktopContainer,
+      ]}>
+      <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
+      <ScrollView style={s.flex1}>
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            <Trans>Pinned Feeds</Trans>
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !currentFeeds.pinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                <Trans>You don't have any pinned feeds.</Trans>
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Saved Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.unpinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any saved feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.unpinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+            currentFeeds.pinned.map(uri => (
+              <ListItem
+                key={uri}
+                feedUri={uri}
+                isPinned
+                setSavedFeeds={setSavedFeeds}
+                resetSaveFeedsMutationState={resetSaveFeedsMutationState}
+                currentFeeds={currentFeeds}
+              />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            <Trans>Saved Feeds</Trans>
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !unpinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                <Trans>You don't have any saved feeds.</Trans>
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
+            unpinned.map(uri => (
+              <ListItem
+                key={uri}
+                feedUri={uri}
+                isPinned={false}
+                setSavedFeeds={setSavedFeeds}
+                resetSaveFeedsMutationState={resetSaveFeedsMutationState}
+                currentFeeds={currentFeeds}
+              />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
 
-          <View style={styles.footerText}>
-            <Text type="sm" style={pal.textLight}>
+        <View style={styles.footerText}>
+          <Text type="sm" style={pal.textLight}>
+            <Trans>
               Feeds are custom algorithms that users build with a little coding
               expertise.{' '}
               <TextLink
@@ -145,48 +163,95 @@ export const SavedFeeds = withAuthRequired(
                 text="See this guide"
               />{' '}
               for more information.
-            </Text>
-          </View>
-          <View style={{height: 100}} />
-        </ScrollView>
-      </CenteredView>
-    )
-  }),
-)
+            </Trans>
+          </Text>
+        </View>
+        <View style={{height: 100}} />
+      </ScrollView>
+    </CenteredView>
+  )
+}
 
-const ListItem = observer(function ListItemImpl({
-  savedFeeds,
-  item,
+function ListItem({
+  feedUri,
+  isPinned,
+  currentFeeds,
+  setSavedFeeds,
+  resetSaveFeedsMutationState,
 }: {
-  savedFeeds: SavedFeedsModel
-  item: FeedSourceModel
+  feedUri: string // uri
+  isPinned: boolean
+  currentFeeds: {saved: string[]; pinned: string[]}
+  setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
+  resetSaveFeedsMutationState: ReturnType<
+    typeof useSetSaveFeedsMutation
+  >['reset']
 }) {
   const pal = usePalette('default')
-  const isPinned = item.isPinned
+  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
+  const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
+    useUnpinFeedMutation()
+  const isPending = isPinPending || isUnpinPending
 
-  const onTogglePinned = useCallback(() => {
+  const onTogglePinned = React.useCallback(async () => {
     Haptics.default()
-    item.togglePin().catch(e => {
+
+    try {
+      resetSaveFeedsMutationState()
+
+      if (isPinned) {
+        await unpinFeed({uri: feedUri})
+      } else {
+        await pinFeed({uri: feedUri})
+      }
+    } catch (e) {
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to toggle pinned feed', {error: e})
-    })
-  }, [item])
-  const onPressUp = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'up').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
-  const onPressDown = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'down').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
+    }
+  }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
+
+  const onPressUp = React.useCallback(async () => {
+    if (!isPinned) return
+
+    // create new array, do not mutate
+    const pinned = [...currentFeeds.pinned]
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index === 0) return
+    ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: currentFeeds.saved, pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
+
+  const onPressDown = React.useCallback(async () => {
+    if (!isPinned) return
+
+    const pinned = [...currentFeeds.pinned]
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index >= pinned.length - 1) return
+    ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: currentFeeds.saved, pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
 
   return (
     <Pressable
@@ -194,43 +259,62 @@ const ListItem = observer(function ListItemImpl({
       style={[styles.itemContainer, pal.border]}>
       {isPinned ? (
         <View style={styles.webArrowButtonsContainer}>
-          <TouchableOpacity
+          <Pressable
+            disabled={isPending}
             accessibilityRole="button"
             onPress={onPressUp}
-            hitSlop={HITSLOP_TOP}>
+            hitSlop={HITSLOP_TOP}
+            style={state => ({
+              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+            })}>
             <FontAwesomeIcon
               icon="arrow-up"
               size={12}
               style={[pal.text, styles.webArrowUpButton]}
             />
-          </TouchableOpacity>
-          <TouchableOpacity
+          </Pressable>
+          <Pressable
+            disabled={isPending}
             accessibilityRole="button"
             onPress={onPressDown}
-            hitSlop={HITSLOP_BOTTOM}>
+            hitSlop={HITSLOP_BOTTOM}
+            style={state => ({
+              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+            })}>
             <FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
-          </TouchableOpacity>
+          </Pressable>
         </View>
       ) : null}
       <FeedSourceCard
-        key={item.uri}
-        item={item}
-        showSaveBtn
+        key={feedUri}
+        feedUri={feedUri}
         style={styles.noBorder}
+        showSaveBtn
+        LoadingComponent={
+          <FeedLoadingPlaceholder
+            style={{flex: 1}}
+            showLowerPlaceholder={false}
+            showTopBorder={false}
+          />
+        }
       />
-      <TouchableOpacity
+      <Pressable
+        disabled={isPending}
         accessibilityRole="button"
         hitSlop={10}
-        onPress={onTogglePinned}>
+        onPress={onTogglePinned}
+        style={state => ({
+          opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+        })}>
         <FontAwesomeIcon
           icon="thumb-tack"
           size={20}
           color={isPinned ? colors.blue3 : pal.colors.icon}
         />
-      </TouchableOpacity>
+      </Pressable>
     </Pressable>
   )
-})
+}
 
 const styles = StyleSheet.create({
   desktopContainer: {
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
deleted file mode 100644
index bf9857df4..000000000
--- a/src/view/screens/Search.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './SearchMobile'
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
deleted file mode 100644
index 2d0c0288a..000000000
--- a/src/view/screens/Search.web.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react'
-import {View, StyleSheet} from 'react-native'
-import {SearchUIModel} from 'state/models/ui/search'
-import {FoafsModel} from 'state/models/discovery/foafs'
-import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {Suggestions} from 'view/com/search/Suggestions'
-import {SearchResults} from 'view/com/search/SearchResults'
-import {observer} from 'mobx-react-lite'
-import {
-  NativeStackScreenProps,
-  SearchTabNavigatorParams,
-} from 'lib/routes/types'
-import {useStores} from 'state/index'
-import {CenteredView} from 'view/com/util/Views'
-import * as Mobile from './SearchMobile'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-
-type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
-export const SearchScreen = withAuthRequired(
-  observer(function SearchScreenImpl({navigation, route}: Props) {
-    const store = useStores()
-    const params = route.params || {}
-    const foafs = React.useMemo<FoafsModel>(
-      () => new FoafsModel(store),
-      [store],
-    )
-    const suggestedActors = React.useMemo<SuggestedActorsModel>(
-      () => new SuggestedActorsModel(store),
-      [store],
-    )
-    const searchUIModel = React.useMemo<SearchUIModel | undefined>(
-      () => (params.q ? new SearchUIModel(store) : undefined),
-      [params.q, store],
-    )
-
-    React.useEffect(() => {
-      if (params.q && searchUIModel) {
-        searchUIModel.fetch(params.q)
-      }
-      if (!foafs.hasData) {
-        foafs.fetch()
-      }
-      if (!suggestedActors.hasLoaded) {
-        suggestedActors.loadMore(true)
-      }
-    }, [foafs, suggestedActors, searchUIModel, params.q])
-
-    const {isDesktop} = useWebMediaQueries()
-
-    if (searchUIModel) {
-      return (
-        <View style={styles.scrollContainer}>
-          <SearchResults model={searchUIModel} />
-        </View>
-      )
-    }
-
-    if (!isDesktop) {
-      return (
-        <CenteredView style={styles.scrollContainer}>
-          <Mobile.SearchScreen navigation={navigation} route={route} />
-        </CenteredView>
-      )
-    }
-
-    return <Suggestions foafs={foafs} suggestedActors={suggestedActors} />
-  }),
-)
-
-const styles = StyleSheet.create({
-  scrollContainer: {
-    height: '100%',
-    overflowY: 'auto',
-  },
-})
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
new file mode 100644
index 000000000..f031abcc2
--- /dev/null
+++ b/src/view/screens/Search/Search.tsx
@@ -0,0 +1,658 @@
+import React from 'react'
+import {
+  View,
+  StyleSheet,
+  ActivityIndicator,
+  RefreshControl,
+  TextInput,
+  Pressable,
+  Platform,
+} from 'react-native'
+import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views'
+import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {useFocusEffect} from '@react-navigation/native'
+
+import {logger} from '#/logger'
+import {
+  NativeStackScreenProps,
+  SearchTabNavigatorParams,
+} from 'lib/routes/types'
+import {Text} from '#/view/com/util/text/Text'
+import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
+import {Post} from '#/view/com/post/Post'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
+import {HITSLOP_10} from '#/lib/constants'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+import {useSession} from '#/state/session'
+import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
+import {useSearchPostsQuery} from '#/state/queries/search-posts'
+import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
+import {useSetDrawerOpen} from '#/state/shell'
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {MagnifyingGlassIcon} from '#/lib/icons'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {SearchResultCard} from '#/view/shell/desktop/Search'
+import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
+import {isWeb} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {s} from '#/lib/styles'
+
+function Loader() {
+  const pal = usePalette('default')
+  const {isMobile} = useWebMediaQueries()
+  return (
+    <CenteredView
+      style={[
+        // @ts-ignore web only -prf
+        {
+          padding: 18,
+          height: isWeb ? '100vh' : undefined,
+        },
+        pal.border,
+      ]}
+      sideBorders={!isMobile}>
+      <ActivityIndicator />
+    </CenteredView>
+  )
+}
+
+function EmptyState({message, error}: {message: string; error?: string}) {
+  const pal = usePalette('default')
+  const {isMobile} = useWebMediaQueries()
+
+  return (
+    <CenteredView
+      sideBorders={!isMobile}
+      style={[
+        pal.border,
+        // @ts-ignore web only -prf
+        {
+          padding: 18,
+          height: isWeb ? '100vh' : undefined,
+        },
+      ]}>
+      <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
+        <Text style={[pal.text]}>
+          <Trans>{message}</Trans>
+        </Text>
+
+        {error && (
+          <>
+            <View
+              style={[
+                {
+                  marginVertical: 12,
+                  height: 1,
+                  width: '100%',
+                  backgroundColor: pal.text.color,
+                  opacity: 0.2,
+                },
+              ]}
+            />
+
+            <Text style={[pal.textLight]}>
+              <Trans>Error:</Trans> {error}
+            </Text>
+          </>
+        )}
+      </View>
+    </CenteredView>
+  )
+}
+
+function SearchScreenSuggestedFollows() {
+  const pal = usePalette('default')
+  const {currentAccount} = useSession()
+  const [suggestions, setSuggestions] = React.useState<
+    AppBskyActorDefs.ProfileViewBasic[]
+  >([])
+  const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor()
+
+  React.useEffect(() => {
+    async function getSuggestions() {
+      const friends = await getSuggestedFollowsByActor(
+        currentAccount!.did,
+      ).then(friendsRes => friendsRes.suggestions)
+
+      if (!friends) return // :(
+
+      const friendsOfFriends = new Map<
+        string,
+        AppBskyActorDefs.ProfileViewBasic
+      >()
+
+      await Promise.all(
+        friends.slice(0, 4).map(friend =>
+          getSuggestedFollowsByActor(friend.did).then(foafsRes => {
+            for (const user of foafsRes.suggestions) {
+              friendsOfFriends.set(user.did, user)
+            }
+          }),
+        ),
+      )
+
+      setSuggestions(Array.from(friendsOfFriends.values()))
+    }
+
+    try {
+      getSuggestions()
+    } catch (e) {
+      logger.error(`SearchScreenSuggestedFollows: failed to get suggestions`, {
+        error: e,
+      })
+    }
+  }, [currentAccount, setSuggestions, getSuggestedFollowsByActor])
+
+  return suggestions.length ? (
+    <FlatList
+      data={suggestions}
+      renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />}
+      keyExtractor={item => item.did}
+      // @ts-ignore web only -prf
+      desktopFixedHeight
+      contentContainerStyle={{paddingBottom: 1200}}
+    />
+  ) : (
+    <CenteredView sideBorders style={[pal.border, s.hContentRegion]}>
+      <ProfileCardFeedLoadingPlaceholder />
+      <ProfileCardFeedLoadingPlaceholder />
+    </CenteredView>
+  )
+}
+
+type SearchResultSlice =
+  | {
+      type: 'post'
+      key: string
+      post: AppBskyFeedDefs.PostView
+    }
+  | {
+      type: 'loadingMore'
+      key: string
+    }
+
+function SearchScreenPostResults({query}: {query: string}) {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const [isPTR, setIsPTR] = React.useState(false)
+  const {
+    isFetched,
+    data: results,
+    isFetching,
+    error,
+    refetch,
+    fetchNextPage,
+    isFetchingNextPage,
+    hasNextPage,
+  } = useSearchPostsQuery({query})
+
+  const onPullToRefresh = React.useCallback(async () => {
+    setIsPTR(true)
+    await refetch()
+    setIsPTR(false)
+  }, [setIsPTR, refetch])
+  const onEndReached = React.useCallback(() => {
+    if (isFetching || !hasNextPage || error) return
+    fetchNextPage()
+  }, [isFetching, error, hasNextPage, fetchNextPage])
+
+  const posts = React.useMemo(() => {
+    return results?.pages.flatMap(page => page.posts) || []
+  }, [results])
+  const items = React.useMemo(() => {
+    let temp: SearchResultSlice[] = []
+
+    for (const post of posts) {
+      temp.push({
+        type: 'post',
+        key: post.uri,
+        post,
+      })
+    }
+
+    if (isFetchingNextPage) {
+      temp.push({
+        type: 'loadingMore',
+        key: 'loadingMore',
+      })
+    }
+
+    return temp
+  }, [posts, isFetchingNextPage])
+
+  return error ? (
+    <EmptyState
+      message={_(
+        msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
+      )}
+      error={error.toString()}
+    />
+  ) : (
+    <>
+      {isFetched ? (
+        <>
+          {posts.length ? (
+            <FlatList
+              data={items}
+              renderItem={({item}) => {
+                if (item.type === 'post') {
+                  return <Post post={item.post} />
+                } else {
+                  return <Loader />
+                }
+              }}
+              keyExtractor={item => item.key}
+              refreshControl={
+                <RefreshControl
+                  refreshing={isPTR}
+                  onRefresh={onPullToRefresh}
+                  tintColor={pal.colors.text}
+                  titleColor={pal.colors.text}
+                />
+              }
+              onEndReached={onEndReached}
+              // @ts-ignore web only -prf
+              desktopFixedHeight
+              contentContainerStyle={{paddingBottom: 100}}
+            />
+          ) : (
+            <EmptyState message={_(msg`No results found for ${query}`)} />
+          )}
+        </>
+      ) : (
+        <Loader />
+      )}
+    </>
+  )
+}
+
+function SearchScreenUserResults({query}: {query: string}) {
+  const {_} = useLingui()
+  const [isFetched, setIsFetched] = React.useState(false)
+  const [results, setResults] = React.useState<
+    AppBskyActorDefs.ProfileViewBasic[]
+  >([])
+  const search = useActorAutocompleteFn()
+
+  React.useEffect(() => {
+    async function getResults() {
+      try {
+        const searchResults = await search({query, limit: 30})
+
+        if (searchResults) {
+          setResults(searchResults)
+        }
+      } catch (e: any) {
+        logger.error(`SearchScreenUserResults: failed to get results`, {
+          error: e.toString(),
+        })
+      } finally {
+        setIsFetched(true)
+      }
+    }
+
+    if (query) {
+      getResults()
+    } else {
+      setResults([])
+      setIsFetched(false)
+    }
+  }, [query, search, setResults])
+
+  return isFetched ? (
+    <>
+      {results.length ? (
+        <FlatList
+          data={results}
+          renderItem={({item}) => (
+            <ProfileCardWithFollowBtn profile={item} noBg />
+          )}
+          keyExtractor={item => item.did}
+          // @ts-ignore web only -prf
+          desktopFixedHeight
+          contentContainerStyle={{paddingBottom: 100}}
+        />
+      ) : (
+        <EmptyState message={_(msg`No results found for ${query}`)} />
+      )}
+    </>
+  ) : (
+    <Loader />
+  )
+}
+
+const SECTIONS = ['Posts', 'Users']
+export function SearchScreenInner({query}: {query?: string}) {
+  const pal = usePalette('default')
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const {hasSession} = useSession()
+  const {isDesktop} = useWebMediaQueries()
+
+  const onPageSelected = React.useCallback(
+    (index: number) => {
+      setMinimalShellMode(false)
+      setDrawerSwipeDisabled(index > 0)
+    },
+    [setDrawerSwipeDisabled, setMinimalShellMode],
+  )
+
+  return query ? (
+    <Pager
+      tabBarPosition="top"
+      onPageSelected={onPageSelected}
+      renderTabBar={props => (
+        <CenteredView sideBorders style={pal.border}>
+          <TabBar items={SECTIONS} {...props} />
+        </CenteredView>
+      )}
+      initialPage={0}>
+      <View>
+        <SearchScreenPostResults query={query} />
+      </View>
+      <View>
+        <SearchScreenUserResults query={query} />
+      </View>
+    </Pager>
+  ) : hasSession ? (
+    <View>
+      <CenteredView sideBorders style={pal.border}>
+        <Text
+          type="title"
+          style={[
+            pal.text,
+            pal.border,
+            {
+              display: 'flex',
+              paddingVertical: 12,
+              paddingHorizontal: 18,
+              fontWeight: 'bold',
+            },
+          ]}>
+          <Trans>Suggested Follows</Trans>
+        </Text>
+      </CenteredView>
+
+      <SearchScreenSuggestedFollows />
+    </View>
+  ) : (
+    <CenteredView sideBorders style={pal.border}>
+      <View
+        // @ts-ignore web only -esb
+        style={{
+          height: Platform.select({web: '100vh'}),
+        }}>
+        {isDesktop && (
+          <Text
+            type="title"
+            style={[
+              pal.text,
+              pal.border,
+              {
+                display: 'flex',
+                paddingVertical: 12,
+                paddingHorizontal: 18,
+                fontWeight: 'bold',
+                borderBottomWidth: 1,
+              },
+            ]}>
+            <Trans>Search</Trans>
+          </Text>
+        )}
+
+        <Text
+          style={[
+            pal.textLight,
+            {textAlign: 'center', paddingVertical: 12, paddingHorizontal: 18},
+          ]}>
+          <Trans>Search for posts and users.</Trans>
+        </Text>
+      </View>
+    </CenteredView>
+  )
+}
+
+export function SearchScreenDesktop(
+  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
+) {
+  const {isDesktop} = useWebMediaQueries()
+
+  return isDesktop ? (
+    <SearchScreenInner query={props.route.params?.q} />
+  ) : (
+    <SearchScreenMobile {...props} />
+  )
+}
+
+export function SearchScreenMobile(
+  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
+) {
+  const theme = useTheme()
+  const textInput = React.useRef<TextInput>(null)
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {track} = useAnalytics()
+  const setDrawerOpen = useSetDrawerOpen()
+  const moderationOpts = useModerationOpts()
+  const search = useActorAutocompleteFn()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {isTablet} = useWebMediaQueries()
+
+  const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>(
+    undefined,
+  )
+  const [isFetching, setIsFetching] = React.useState<boolean>(false)
+  const [query, setQuery] = React.useState<string>(props.route?.params?.q || '')
+  const [searchResults, setSearchResults] = React.useState<
+    AppBskyActorDefs.ProfileViewBasic[]
+  >([])
+  const [inputIsFocused, setInputIsFocused] = React.useState(false)
+  const [showAutocompleteResults, setShowAutocompleteResults] =
+    React.useState(false)
+
+  const onPressMenu = React.useCallback(() => {
+    track('ViewHeader:MenuButtonClicked')
+    setDrawerOpen(true)
+  }, [track, setDrawerOpen])
+  const onPressCancelSearch = React.useCallback(() => {
+    textInput.current?.blur()
+    setQuery('')
+    setShowAutocompleteResults(false)
+    if (searchDebounceTimeout.current)
+      clearTimeout(searchDebounceTimeout.current)
+  }, [textInput])
+  const onPressClearQuery = React.useCallback(() => {
+    setQuery('')
+    setShowAutocompleteResults(false)
+  }, [setQuery])
+  const onChangeText = React.useCallback(
+    async (text: string) => {
+      setQuery(text)
+
+      if (text.length > 0) {
+        setIsFetching(true)
+        setShowAutocompleteResults(true)
+
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+
+        searchDebounceTimeout.current = setTimeout(async () => {
+          const results = await search({query: text, limit: 30})
+
+          if (results) {
+            setSearchResults(results)
+            setIsFetching(false)
+          }
+        }, 300)
+      } else {
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+        setSearchResults([])
+        setIsFetching(false)
+        setShowAutocompleteResults(false)
+      }
+    },
+    [setQuery, search, setSearchResults],
+  )
+  const onSubmit = React.useCallback(() => {
+    setShowAutocompleteResults(false)
+  }, [setShowAutocompleteResults])
+
+  const onSoftReset = React.useCallback(() => {
+    onPressCancelSearch()
+  }, [onPressCancelSearch])
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      return listenSoftReset(onSoftReset)
+    }, [onSoftReset, setMinimalShellMode]),
+  )
+
+  return (
+    <View style={{flex: 1}}>
+      <CenteredView style={[styles.header, pal.border]} sideBorders={isTablet}>
+        <Pressable
+          testID="viewHeaderBackOrMenuBtn"
+          onPress={onPressMenu}
+          hitSlop={HITSLOP_10}
+          style={styles.headerMenuBtn}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Menu`)}
+          accessibilityHint="Access navigation links and settings">
+          <FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
+        </Pressable>
+
+        <View
+          style={[
+            {backgroundColor: pal.colors.backgroundLight},
+            styles.headerSearchContainer,
+          ]}>
+          <MagnifyingGlassIcon
+            style={[pal.icon, styles.headerSearchIcon]}
+            size={21}
+          />
+          <TextInput
+            testID="searchTextInput"
+            ref={textInput}
+            placeholder="Search"
+            placeholderTextColor={pal.colors.textLight}
+            selectTextOnFocus
+            returnKeyType="search"
+            value={query}
+            style={[pal.text, styles.headerSearchInput]}
+            keyboardAppearance={theme.colorScheme}
+            onFocus={() => setInputIsFocused(true)}
+            onBlur={() => setInputIsFocused(false)}
+            onChangeText={onChangeText}
+            onSubmitEditing={onSubmit}
+            autoFocus={false}
+            accessibilityRole="search"
+            accessibilityLabel={_(msg`Search`)}
+            accessibilityHint=""
+            autoCorrect={false}
+            autoCapitalize="none"
+          />
+          {query ? (
+            <Pressable
+              testID="searchTextInputClearBtn"
+              onPress={onPressClearQuery}
+              accessibilityRole="button"
+              accessibilityLabel={_(msg`Clear search query`)}
+              accessibilityHint="">
+              <FontAwesomeIcon
+                icon="xmark"
+                size={16}
+                style={pal.textLight as FontAwesomeIconStyle}
+              />
+            </Pressable>
+          ) : undefined}
+        </View>
+
+        {query || inputIsFocused ? (
+          <View style={styles.headerCancelBtn}>
+            <Pressable onPress={onPressCancelSearch} accessibilityRole="button">
+              <Text style={[pal.text]}>
+                <Trans>Cancel</Trans>
+              </Text>
+            </Pressable>
+          </View>
+        ) : undefined}
+      </CenteredView>
+
+      {showAutocompleteResults && moderationOpts ? (
+        <>
+          {isFetching ? (
+            <Loader />
+          ) : (
+            <ScrollView style={{height: '100%'}}>
+              {searchResults.length ? (
+                searchResults.map((item, i) => (
+                  <SearchResultCard
+                    key={item.did}
+                    profile={item}
+                    moderation={moderateProfile(item, moderationOpts)}
+                    style={i === 0 ? {borderTopWidth: 0} : {}}
+                  />
+                ))
+              ) : (
+                <EmptyState message={_(msg`No results found for ${query}`)} />
+              )}
+
+              <View style={{height: 200}} />
+            </ScrollView>
+          )}
+        </>
+      ) : (
+        <SearchScreenInner query={query} />
+      )}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingVertical: 4,
+  },
+  headerMenuBtn: {
+    width: 30,
+    height: 30,
+    borderRadius: 30,
+    marginRight: 6,
+    paddingBottom: 2,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  headerSearchContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 30,
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+  },
+  headerSearchIcon: {
+    marginRight: 6,
+    alignSelf: 'center',
+  },
+  headerSearchInput: {
+    flex: 1,
+    fontSize: 17,
+  },
+  headerCancelBtn: {
+    paddingLeft: 10,
+  },
+})
diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx
new file mode 100644
index 000000000..a65149bf7
--- /dev/null
+++ b/src/view/screens/Search/index.tsx
@@ -0,0 +1,3 @@
+import {SearchScreenMobile} from '#/view/screens/Search/Search'
+
+export const SearchScreen = SearchScreenMobile
diff --git a/src/view/screens/Search/index.web.tsx b/src/view/screens/Search/index.web.tsx
new file mode 100644
index 000000000..8e039e3cd
--- /dev/null
+++ b/src/view/screens/Search/index.web.tsx
@@ -0,0 +1,3 @@
+import {SearchScreenDesktop} from '#/view/screens/Search/Search'
+
+export const SearchScreen = SearchScreenDesktop
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
deleted file mode 100644
index c1df58ffd..000000000
--- a/src/view/screens/SearchMobile.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import React, {useCallback} from 'react'
-import {
-  StyleSheet,
-  TouchableWithoutFeedback,
-  Keyboard,
-  View,
-} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {FlatList, ScrollView} from 'view/com/util/Views'
-import {
-  NativeStackScreenProps,
-  SearchTabNavigatorParams,
-} from 'lib/routes/types'
-import {observer} from 'mobx-react-lite'
-import {Text} from 'view/com/util/text/Text'
-import {useStores} from 'state/index'
-import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
-import {SearchUIModel} from 'state/models/ui/search'
-import {FoafsModel} from 'state/models/discovery/foafs'
-import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
-import {HeaderWithInput} from 'view/com/search/HeaderWithInput'
-import {Suggestions} from 'view/com/search/Suggestions'
-import {SearchResults} from 'view/com/search/SearchResults'
-import {s} from 'lib/styles'
-import {ProfileCard} from 'view/com/profile/ProfileCard'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
-import {isAndroid, isIOS} from 'platform/detection'
-import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-
-type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
-export const SearchScreen = withAuthRequired(
-  observer<Props>(function SearchScreenImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const setMinimalShellMode = useSetMinimalShellMode()
-    const setIsDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-    const scrollViewRef = React.useRef<ScrollView>(null)
-    const flatListRef = React.useRef<FlatList>(null)
-    const [onMainScroll] = useOnMainScroll()
-    const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
-    const [query, setQuery] = React.useState<string>('')
-    const autocompleteView = React.useMemo<UserAutocompleteModel>(
-      () => new UserAutocompleteModel(store),
-      [store],
-    )
-    const foafs = React.useMemo<FoafsModel>(
-      () => new FoafsModel(store),
-      [store],
-    )
-    const suggestedActors = React.useMemo<SuggestedActorsModel>(
-      () => new SuggestedActorsModel(store),
-      [store],
-    )
-    const [searchUIModel, setSearchUIModel] = React.useState<
-      SearchUIModel | undefined
-    >()
-
-    const onChangeQuery = React.useCallback(
-      (text: string) => {
-        setQuery(text)
-        if (text.length > 0) {
-          autocompleteView.setActive(true)
-          autocompleteView.setPrefix(text)
-        } else {
-          autocompleteView.setActive(false)
-        }
-      },
-      [setQuery, autocompleteView],
-    )
-
-    const onPressClearQuery = React.useCallback(() => {
-      setQuery('')
-    }, [setQuery])
-
-    const onPressCancelSearch = React.useCallback(() => {
-      setQuery('')
-      autocompleteView.setActive(false)
-      setSearchUIModel(undefined)
-      setIsDrawerSwipeDisabled(false)
-    }, [setQuery, autocompleteView, setIsDrawerSwipeDisabled])
-
-    const onSubmitQuery = React.useCallback(() => {
-      if (query.length === 0) {
-        return
-      }
-
-      const model = new SearchUIModel(store)
-      model.fetch(query)
-      setSearchUIModel(model)
-      setIsDrawerSwipeDisabled(true)
-    }, [query, setSearchUIModel, store, setIsDrawerSwipeDisabled])
-
-    const onSoftReset = React.useCallback(() => {
-      scrollViewRef.current?.scrollTo({x: 0, y: 0})
-      flatListRef.current?.scrollToOffset({offset: 0})
-      onPressCancelSearch()
-    }, [scrollViewRef, flatListRef, onPressCancelSearch])
-
-    useFocusEffect(
-      React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        const cleanup = () => {
-          softResetSub.remove()
-        }
-
-        setMinimalShellMode(false)
-        autocompleteView.setup()
-        if (!foafs.hasData) {
-          foafs.fetch()
-        }
-        if (!suggestedActors.hasLoaded) {
-          suggestedActors.loadMore(true)
-        }
-
-        return cleanup
-      }, [
-        store,
-        autocompleteView,
-        foafs,
-        suggestedActors,
-        onSoftReset,
-        setMinimalShellMode,
-      ]),
-    )
-
-    const onPress = useCallback(() => {
-      if (isIOS || isAndroid) {
-        Keyboard.dismiss()
-      }
-    }, [])
-
-    return (
-      <TouchableWithoutFeedback onPress={onPress} accessible={false}>
-        <View style={[pal.view, styles.container]}>
-          <HeaderWithInput
-            isInputFocused={isInputFocused}
-            query={query}
-            setIsInputFocused={setIsInputFocused}
-            onChangeQuery={onChangeQuery}
-            onPressClearQuery={onPressClearQuery}
-            onPressCancelSearch={onPressCancelSearch}
-            onSubmitQuery={onSubmitQuery}
-          />
-          {searchUIModel ? (
-            <SearchResults model={searchUIModel} />
-          ) : !isInputFocused && !query ? (
-            <Suggestions
-              ref={flatListRef}
-              foafs={foafs}
-              suggestedActors={suggestedActors}
-            />
-          ) : (
-            <ScrollView
-              ref={scrollViewRef}
-              testID="searchScrollView"
-              style={pal.view}
-              onScroll={onMainScroll}
-              scrollEventThrottle={100}>
-              {query && autocompleteView.suggestions.length ? (
-                <>
-                  {autocompleteView.suggestions.map((suggestion, index) => (
-                    <ProfileCard
-                      key={suggestion.did}
-                      testID={`searchAutoCompleteResult-${suggestion.handle}`}
-                      profile={suggestion}
-                      noBorder={index === 0}
-                    />
-                  ))}
-                </>
-              ) : query && !autocompleteView.suggestions.length ? (
-                <View>
-                  <Text style={[pal.textLight, styles.searchPrompt]}>
-                    No results found for {autocompleteView.prefix}
-                  </Text>
-                </View>
-              ) : isInputFocused ? (
-                <View>
-                  <Text style={[pal.textLight, styles.searchPrompt]}>
-                    Search for users and posts on the network
-                  </Text>
-                </View>
-              ) : null}
-              <View style={s.footerSpacer} />
-            </ScrollView>
-          )}
-        </View>
-      </TouchableWithoutFeedback>
-    )
-  }),
-)
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-
-  searchPrompt: {
-    textAlign: 'center',
-    paddingTop: 10,
-  },
-})
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index ca4ef2a40..388a5d954 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -10,20 +10,13 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {
-  useFocusEffect,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
 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'
@@ -39,662 +32,766 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {NavigationProp} from 'lib/routes/types'
-import {pluralize} from 'lib/strings/helpers'
 import {HandIcon, HashtagIcon} from 'lib/icons'
-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,
   useColorMode,
   useSetColorMode,
+  useOnboardingDispatch,
 } from '#/state/shell'
+import {
+  useRequireAltTextEnabled,
+  useSetRequireAltTextEnabled,
+} from '#/state/preferences'
+import {
+  useSession,
+  useSessionApi,
+  SessionAccount,
+  getAgent,
+} from '#/state/session'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useClearPreferencesMutation} from '#/state/queries/preferences'
+import {useInviteCodesQuery} from '#/state/queries/invites'
+import {clear as clearStorage} from '#/state/persisted/store'
+import {clearLegacyStorage} from '#/state/persisted/legacy'
 
 // TEMPORARY (APP-700)
 // remove after backend testing finishes
 // -prf
 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'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+
+function SettingsAccountCard({account}: {account: SessionAccount}) {
+  const pal = usePalette('default')
+  const {isSwitchingAccounts, currentAccount} = useSession()
+  const {logout} = useSessionApi()
+  const {data: profile} = useProfileQuery({did: account.did})
+  const isCurrentAccount = account.did === currentAccount?.did
+  const {onPressSwitchAccount} = useAccountSwitcher()
+
+  const contents = (
+    <View style={[pal.view, styles.linkCard]}>
+      <View style={styles.avi}>
+        <UserAvatar size={40} avatar={profile?.avatar} />
+      </View>
+      <View style={[s.flex1]}>
+        <Text type="md-bold" style={pal.text}>
+          {profile?.displayName || account.handle}
+        </Text>
+        <Text type="sm" style={pal.textLight}>
+          {account.handle}
+        </Text>
+      </View>
 
-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 setMinimalShellMode = useSetMinimalShellMode()
-    const navigation = useNavigation<NavigationProp>()
-    const {isMobile} = useWebMediaQueries()
-    const {screen, track} = useAnalytics()
-    const [isSwitching, setIsSwitching, onPressSwitchAccount] =
-      useAccountSwitcher()
-    const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
-      store.agent,
-    )
+      {isCurrentAccount ? (
+        <TouchableOpacity
+          testID="signOutBtn"
+          onPress={logout}
+          accessibilityRole="button"
+          accessibilityLabel="Sign out"
+          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          <Text type="lg" style={pal.link}>
+            Sign out
+          </Text>
+        </TouchableOpacity>
+      ) : (
+        <AccountDropdownBtn account={account} />
+      )}
+    </View>
+  )
+
+  return isCurrentAccount ? (
+    <Link
+      href={makeProfileLink({
+        did: currentAccount?.did,
+        handle: currentAccount?.handle,
+      })}
+      title="Your profile"
+      noFeedback>
+      {contents}
+    </Link>
+  ) : (
+    <TouchableOpacity
+      testID={`switchToAccountBtn-${account.handle}`}
+      key={account.did}
+      onPress={
+        isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
+      }
+      accessibilityRole="button"
+      accessibilityLabel={`Switch to ${account.handle}`}
+      accessibilityHint="Switches the account you are logged in to">
+      {contents}
+    </TouchableOpacity>
+  )
+}
 
-    const primaryBg = useCustomPalette<ViewStyle>({
-      light: {backgroundColor: colors.blue0},
-      dark: {backgroundColor: colors.blue6},
-    })
-    const primaryText = useCustomPalette<TextStyle>({
-      light: {color: colors.blue3},
-      dark: {color: colors.blue2},
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+export function SettingsScreen({}: 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} = useSession()
+  const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
+    getAgent(),
+  )
+  const {mutate: clearPreferences} = useClearPreferencesMutation()
+  const {data: invites} = useInviteCodesQuery()
+  const invitesAvailable = invites?.available?.length ?? 0
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+  const closeAllActiveElements = useCloseAllActiveElements()
+
+  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 onPressAddAccount = React.useCallback(() => {
+    track('Settings:AddAccountButtonClicked')
+    setShowLoggedOut(true)
+    closeAllActiveElements()
+  }, [track, setShowLoggedOut, closeAllActiveElements])
+
+  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])
 
-    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 onPressInviteCodes = React.useCallback(() => {
+    track('Settings:InvitecodesButtonClicked')
+    openModal({name: 'invite-codes'})
+  }, [track, openModal])
 
-    useFocusEffect(
-      React.useCallback(() => {
-        screen('Settings')
-        setMinimalShellMode(false)
-      }, [screen, setMinimalShellMode]),
-    )
+  const onPressLanguageSettings = React.useCallback(() => {
+    navigation.navigate('LanguageSettings')
+  }, [navigation])
+
+  const onPressDeleteAccount = React.useCallback(() => {
+    openModal({name: 'delete-account'})
+  }, [openModal])
 
-    const onPressAddAccount = React.useCallback(() => {
-      track('Settings:AddAccountButtonClicked')
-      navigation.navigate('HomeTab')
-      navigation.dispatch(StackActions.popToTop())
-      store.session.clear()
-    }, [track, navigation, store])
-
-    const onPressChangeHandle = React.useCallback(() => {
-      track('Settings:ChangeHandleButtonClicked')
-      store.shell.openModal({
-        name: 'change-handle',
-        onChanged() {
-          setIsSwitching(true)
-          store.session.reloadFromServer().then(
-            () => {
-              setIsSwitching(false)
-              Toast.show('Your handle has been updated')
-            },
-            err => {
-              logger.error('Failed to reload from server after handle update', {
-                error: err,
-              })
-              setIsSwitching(false)
-            },
-          )
-        },
-      })
-    }, [track, store, setIsSwitching])
-
-    const onPressInviteCodes = React.useCallback(() => {
-      track('Settings:InvitecodesButtonClicked')
-      store.shell.openModal({name: 'invite-codes'})
-    }, [track, store])
-
-    const onPressLanguageSettings = React.useCallback(() => {
-      navigation.navigate('LanguageSettings')
-    }, [navigation])
-
-    const onPressSignout = React.useCallback(() => {
-      track('Settings:SignOutButtonClicked')
-      store.session.logout()
-    }, [track, store])
-
-    const onPressDeleteAccount = React.useCallback(() => {
-      store.shell.openModal({name: 'delete-account'})
-    }, [store])
-
-    const onPressResetPreferences = React.useCallback(async () => {
-      await store.preferences.reset()
-      Toast.show('Preferences reset')
-    }, [store])
-
-    const onPressResetOnboarding = React.useCallback(async () => {
-      store.onboarding.reset()
-      Toast.show('Onboarding reset')
-    }, [store])
-
-    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} />
-          {store.session.currentSession !== undefined ? (
-            <>
-              <Text type="xl-bold" style={[pal.text, styles.heading]}>
-                Account
+  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)
+  }, [])
+
+  const clearAllStorage = React.useCallback(async () => {
+    await clearStorage()
+    Toast.show(`Storage cleared, you need to restart the app now.`)
+  }, [])
+  const clearAllLegacyStorage = React.useCallback(async () => {
+    await clearLegacyStorage()
+    Toast.show(`Legacy storage cleared, you need to restart the app now.`)
+  }, [])
+
+  return (
+    <View style={[s.hContentRegion]} testID="settingsScreen">
+      <ViewHeader title={_(msg`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}>
+                <Trans>Email:</Trans>{' '}
               </Text>
-              <View style={[styles.infoLine]}>
-                <Text type="lg-medium" style={pal.text}>
-                  Email:{' '}
-                </Text>
-                {!store.session.emailNeedsConfirmation && (
-                  <>
-                    <FontAwesomeIcon
-                      icon="check"
-                      size={10}
-                      style={{color: colors.green3, marginRight: 2}}
-                    />
-                  </>
-                )}
-                <Text type="lg" style={pal.text}>
-                  {store.session.currentSession?.email}{' '}
-                </Text>
-                <Link
-                  onPress={() => store.shell.openModal({name: 'change-email'})}>
-                  <Text type="lg" style={pal.link}>
-                    Change
-                  </Text>
-                </Link>
-              </View>
-              <View style={[styles.infoLine]}>
-                <Text type="lg-medium" style={pal.text}>
-                  Birthday:{' '}
+              {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={() =>
-                    store.shell.openModal({name: 'birth-date-settings'})
-                  }>
-                  <Text type="lg" style={pal.link}>
-                    Show
-                  </Text>
-                </Link>
-              </View>
-              <View style={styles.spacer20} />
-              <EmailConfirmationNotice />
-            </>
-          ) : null}
-          <View style={[s.flexRow, styles.heading]}>
-            <Text type="xl-bold" style={pal.text}>
-              Signed in as
-            </Text>
-            <View style={s.flex1} />
-          </View>
-          {isSwitching ? (
-            <View style={[pal.view, styles.linkCard]}>
-              <ActivityIndicator />
+              </Link>
             </View>
-          ) : (
-            <Link
-              href={makeProfileLink(store.me)}
-              title="Your profile"
-              noFeedback>
-              <View style={[pal.view, styles.linkCard]}>
-                <View style={styles.avi}>
-                  <UserAvatar size={40} avatar={store.me.avatar} />
-                </View>
-                <View style={[s.flex1]}>
-                  <Text type="md-bold" style={pal.text} numberOfLines={1}>
-                    {store.me.displayName || store.me.handle}
-                  </Text>
-                  <Text type="sm" style={pal.textLight} numberOfLines={1}>
-                    {store.me.handle}
-                  </Text>
-                </View>
-                <TouchableOpacity
-                  testID="signOutBtn"
-                  onPress={isSwitching ? undefined : onPressSignout}
-                  accessibilityRole="button"
-                  accessibilityLabel="Sign out"
-                  accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
-                  <Text type="lg" style={pal.link}>
-                    Sign out
-                  </Text>
-                </TouchableOpacity>
-              </View>
-            </Link>
-          )}
-          {store.session.switchableAccounts.map(account => (
-            <TouchableOpacity
-              testID={`switchToAccountBtn-${account.handle}`}
-              key={account.did}
-              style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
-              onPress={
-                isSwitching ? undefined : () => onPressSwitchAccount(account)
-              }
-              accessibilityRole="button"
-              accessibilityLabel={`Switch to ${account.handle}`}
-              accessibilityHint="Switches the account you are logged in to">
-              <View style={styles.avi}>
-                <UserAvatar size={40} avatar={account.aviUrl} />
-              </View>
-              <View style={[s.flex1]}>
-                <Text type="md-bold" style={pal.text}>
-                  {account.displayName || account.handle}
-                </Text>
-                <Text type="sm" style={pal.textLight}>
-                  {account.handle}
+            <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>
-              </View>
-              <AccountDropdownBtn handle={account.handle} />
-            </TouchableOpacity>
-          ))}
-          <TouchableOpacity
-            testID="switchToNewAccountBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressAddAccount}
-            accessibilityRole="button"
-            accessibilityLabel="Add account"
-            accessibilityHint="Create a new Bluesky account">
-            <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="plus"
-                style={pal.text as FontAwesomeIconStyle}
-              />
+              </Link>
             </View>
-            <Text type="lg" style={pal.text}>
-              Add account
-            </Text>
-          </TouchableOpacity>
+            <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>
 
-          <View style={styles.spacer20} />
+        {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} />
+          ))}
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Invite a Friend
+        <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
-            testID="inviteFriendBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressInviteCodes}
-            accessibilityRole="button"
-            accessibilityLabel="Invite"
-            accessibilityHint="Opens invite code list">
-            <View
-              style={[
-                styles.iconContainer,
-                store.me.invitesAvailable > 0 ? primaryBg : pal.btn,
-              ]}>
-              <FontAwesomeIcon
-                icon="ticket"
-                style={
-                  (store.me.invitesAvailable > 0
-                    ? primaryText
-                    : pal.text) as FontAwesomeIconStyle
-                }
-              />
-            </View>
-            <Text
-              type="lg"
-              style={store.me.invitesAvailable > 0 ? pal.link : pal.text}>
-              {formatCount(store.me.invitesAvailable)} invite{' '}
-              {pluralize(store.me.invitesAvailable, 'code')} available
-            </Text>
-          </TouchableOpacity>
+        </TouchableOpacity>
 
-          <View style={styles.spacer20} />
+        <View style={styles.spacer20} />
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Accessibility
-          </Text>
-          <View style={[pal.view, styles.toggleCard]}>
-            <ToggleButton
-              type="default-light"
-              label="Require alt text before posting"
-              labelType="lg"
-              isSelected={store.preferences.requireAltTextEnabled}
-              onPress={store.preferences.toggleRequireAltTextEnabled}
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Invite a Friend</Trans>
+        </Text>
+
+        <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"
+          disabled={invites?.disabled}>
+          <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}>
+            {invites?.disabled ? (
+              <Trans>
+                Your invite codes are hidden when logged in using an App
+                Password
+              </Trans>
+            ) : invitesAvailable === 1 ? (
+              <Trans>{invitesAvailable} invite code available</Trans>
+            ) : (
+              <Trans>{invitesAvailable} invite codes available</Trans>
+            )}
+          </Text>
+        </TouchableOpacity>
 
-          <View style={styles.spacer20} />
+        <View style={styles.spacer20} />
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Appearance
-          </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>
+        <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>
+
+        <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 style={styles.spacer20} />
+        </View>
+        <View style={styles.spacer20} />
 
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Basics
+        <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>
-          <TouchableOpacity
-            testID="preferencesHomeFeedButton"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={openHomeFeedPreferences}
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel="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}>
-              Home Feed Preferences
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="preferencesThreadsButton"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={openThreadsPreferences}
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel="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}>
-              Thread Preferences
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="savedFeedsBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            accessibilityHint="My Saved Feeds"
-            accessibilityLabel="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}>
-              My Saved Feeds
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="languageSettingsBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressLanguageSettings}
-            accessibilityRole="button"
-            accessibilityHint="Language settings"
-            accessibilityLabel="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}>
-              Languages
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="moderationBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={
-              isSwitching ? undefined : () => navigation.navigate('Moderation')
-            }
-            accessibilityRole="button"
-            accessibilityHint=""
-            accessibilityLabel="Opens moderation settings">
-            <View style={[styles.iconContainer, pal.btn]}>
-              <HandIcon style={pal.text} size={18} strokeWidth={6} />
-            </View>
-            <Text type="lg" style={pal.text}>
-              Moderation
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Advanced
+        </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
-            testID="appPasswordBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={onPressAppPasswords}
-            accessibilityRole="button"
-            accessibilityHint="Open app password settings"
-            accessibilityLabel="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}>
-              App passwords
-            </Text>
-          </TouchableOpacity>
-          <TouchableOpacity
-            testID="changeHandleBtn"
-            style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            onPress={isSwitching ? undefined : onPressChangeHandle}
-            accessibilityRole="button"
-            accessibilityLabel="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}>
-              Change handle
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Danger Zone
+        </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
-            style={[pal.view, styles.linkCard]}
-            onPress={onPressDeleteAccount}
-            accessible={true}
-            accessibilityRole="button"
-            accessibilityLabel="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}>
-              Delete my account…
-            </Text>
-          </TouchableOpacity>
-          <View style={styles.spacer20} />
-          <Text type="xl-bold" style={[pal.text, styles.heading]}>
-            Developer Tools
+        </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.linkCardNoIcon]}
-            onPress={onPressSystemLog}
-            accessibilityRole="button"
-            accessibilityHint="Open system log"
-            accessibilityLabel="Opens the system log page">
-            <Text type="lg" style={pal.text}>
-              System log
-            </Text>
-          </TouchableOpacity>
-          {__DEV__ ? (
-            <ToggleButton
-              type="default-light"
-              label="Experiment: Use AppView Proxy"
-              isSelected={debugHeaderEnabled}
-              onPress={toggleDebugHeader}
+        </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>
+        </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="Opens the storybook page">
-                <Text type="lg" style={pal.text}>
-                  Storybook
-                </Text>
-              </TouchableOpacity>
-              <TouchableOpacity
-                style={[pal.view, styles.linkCardNoIcon]}
-                onPress={onPressResetPreferences}
-                accessibilityRole="button"
-                accessibilityHint="Reset preferences"
-                accessibilityLabel="Resets the preferences state">
-                <Text type="lg" style={pal.text}>
-                  Reset preferences state
-                </Text>
-              </TouchableOpacity>
-              <TouchableOpacity
-                style={[pal.view, styles.linkCardNoIcon]}
-                onPress={onPressResetOnboarding}
-                accessibilityRole="button"
-                accessibilityHint="Reset onboarding"
-                accessibilityLabel="Resets the onboarding state">
-                <Text type="lg" style={pal.text}>
-                  Reset onboarding state
-                </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]}>
-                Build version {AppInfo.appVersion} {AppInfo.updateChannel}
+              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]}>
-                Status page
+              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 store = useStores()
-    const {isMobile} = useWebMediaQueries()
-
-    if (!store.session.emailNeedsConfirmation) {
-      return null
-    }
-
-    return (
-      <View style={{marginBottom: 20}}>
-        <Text type="xl-bold" style={[pal.text, styles.heading]}>
-          Verify email
-        </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="Verify my email"
-              accessibilityHint=""
-              onPress={() => store.shell.openModal({name: 'verify-email'})}>
-              <FontAwesomeIcon
-                icon="envelope"
-                color={palInverted.colors.text}
-                size={16}
-              />
-              <Text type="button" style={palInverted.text}>
-                Verify My Email
+              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}>
-            Protect your account by verifying your email.
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
+              onPress={clearAllLegacyStorage}
+              accessibilityRole="button"
+              accessibilityHint="Clear all legacy storage data"
+              accessibilityLabel={_(msg`Clear all legacy storage data`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>
+                  Clear all legacy storage data (restart after this)
+                </Trans>
+              </Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[pal.view, styles.linkCardNoIcon]}
+              onPress={clearAllStorage}
+              accessibilityRole="button"
+              accessibilityHint="Clear all storage data"
+              accessibilityLabel={_(msg`Clear all storage data`)}>
+              <Text type="lg" style={pal.text}>
+                <Trans>Clear all storage data (restart after this)</Trans>
+              </Text>
+            </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/screens/Support.tsx b/src/view/screens/Support.tsx
index 7106b4136..6856f6759 100644
--- a/src/view/screens/Support.tsx
+++ b/src/view/screens/Support.tsx
@@ -10,11 +10,14 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {HELP_DESK_URL} from 'lib/constants'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Support'>
 export const SupportScreen = (_props: Props) => {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -24,19 +27,21 @@ export const SupportScreen = (_props: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Support" />
+      <ViewHeader title={_(msg`Support`)} />
       <CenteredView>
         <Text type="title-xl" style={[pal.text, s.p20, s.pb5]}>
-          Support
+          <Trans>Support</Trans>
         </Text>
         <Text style={[pal.text, s.p20]}>
-          The support form has been moved. If you need help, please
-          <TextLink
-            href={HELP_DESK_URL}
-            text=" click here"
-            style={pal.link}
-          />{' '}
-          or visit {HELP_DESK_URL} to get in touch with us.
+          <Trans>
+            The support form has been moved. If you need help, please
+            <TextLink
+              href={HELP_DESK_URL}
+              text=" click here"
+              style={pal.link}
+            />{' '}
+            or visit {HELP_DESK_URL} to get in touch with us.
+          </Trans>
         </Text>
       </CenteredView>
     </View>
diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx
index b7a388b65..c20890e29 100644
--- a/src/view/screens/TermsOfService.tsx
+++ b/src/view/screens/TermsOfService.tsx
@@ -9,11 +9,14 @@ import {ScrollView} from 'view/com/util/Views'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'TermsOfService'>
 export const TermsOfServiceScreen = (_props: Props) => {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
+  const {_} = useLingui()
 
   useFocusEffect(
     React.useCallback(() => {
@@ -23,11 +26,11 @@ export const TermsOfServiceScreen = (_props: Props) => {
 
   return (
     <View>
-      <ViewHeader title="Terms of Service" />
+      <ViewHeader title={_(msg`Terms of Service`)} />
       <ScrollView style={[s.hContentRegion, pal.view]}>
         <View style={[s.p20]}>
           <Text style={pal.text}>
-            The Terms of Service have been moved to{' '}
+            <Trans>The Terms of Service have been moved to</Trans>{' '}
             <TextLink
               style={pal.link}
               href="https://blueskyweb.xyz/support/tos"