about summary refs log tree commit diff
path: root/src/view/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/shell')
-rw-r--r--src/view/shell/Composer.tsx27
-rw-r--r--src/view/shell/Composer.web.tsx47
-rw-r--r--src/view/shell/Drawer.tsx367
-rw-r--r--src/view/shell/NavSignupCard.tsx61
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx186
-rw-r--r--src/view/shell/bottom-bar/BottomBarStyles.tsx3
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx77
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx150
-rw-r--r--src/view/shell/desktop/Feeds.tsx62
-rw-r--r--src/view/shell/desktop/LeftNav.tsx295
-rw-r--r--src/view/shell/desktop/RightNav.tsx136
-rw-r--r--src/view/shell/desktop/Search.tsx209
-rw-r--r--src/view/shell/index.tsx36
-rw-r--r--src/view/shell/index.web.tsx49
14 files changed, 1072 insertions, 633 deletions
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index 219a594ed..d37ff4fb7 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -2,30 +2,21 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
 import {ComposePost} from '../com/composer/Composer'
-import {ComposerOpts} from 'state/models/ui/shell'
+import {useComposerState} from 'state/shell/composer'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {usePalette} from 'lib/hooks/usePalette'
 
 export const Composer = observer(function ComposerImpl({
-  active,
   winHeight,
-  replyTo,
-  onPost,
-  quote,
-  mention,
 }: {
-  active: boolean
   winHeight: number
-  replyTo?: ComposerOpts['replyTo']
-  onPost?: ComposerOpts['onPost']
-  quote?: ComposerOpts['quote']
-  mention?: ComposerOpts['mention']
 }) {
+  const state = useComposerState()
   const pal = usePalette('default')
   const initInterp = useAnimatedValue(0)
 
   useEffect(() => {
-    if (active) {
+    if (state) {
       Animated.timing(initInterp, {
         toValue: 1,
         duration: 300,
@@ -35,7 +26,7 @@ export const Composer = observer(function ComposerImpl({
     } else {
       initInterp.setValue(0)
     }
-  }, [initInterp, active])
+  }, [initInterp, state])
   const wrapperAnimStyle = {
     transform: [
       {
@@ -50,7 +41,7 @@ export const Composer = observer(function ComposerImpl({
   // rendering
   // =
 
-  if (!active) {
+  if (!state) {
     return <View />
   }
 
@@ -60,10 +51,10 @@ export const Composer = observer(function ComposerImpl({
       aria-modal
       accessibilityViewIsModal>
       <ComposePost
-        replyTo={replyTo}
-        onPost={onPost}
-        quote={quote}
-        mention={mention}
+        replyTo={state.replyTo}
+        onPost={state.onPost}
+        quote={state.quote}
+        mention={state.mention}
       />
     </Animated.View>
   )
diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index c3ec37e57..73f9f540e 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -1,40 +1,35 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, View} from 'react-native'
+import Animated, {FadeIn, FadeInDown, FadeOut} from 'react-native-reanimated'
 import {ComposePost} from '../com/composer/Composer'
-import {ComposerOpts} from 'state/models/ui/shell'
+import {useComposerState} from 'state/shell/composer'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 
 const BOTTOM_BAR_HEIGHT = 61
 
-export const Composer = observer(function ComposerImpl({
-  active,
-  replyTo,
-  quote,
-  onPost,
-  mention,
-}: {
-  active: boolean
-  winHeight: number
-  replyTo?: ComposerOpts['replyTo']
-  quote: ComposerOpts['quote']
-  onPost?: ComposerOpts['onPost']
-  mention?: ComposerOpts['mention']
-}) {
+export function Composer({}: {winHeight: number}) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
+  const state = useComposerState()
 
   // rendering
   // =
 
-  if (!active) {
+  if (!state) {
     return <View />
   }
 
   return (
-    <View style={styles.mask} aria-modal accessibilityViewIsModal>
-      <View
+    <Animated.View
+      style={styles.mask}
+      aria-modal
+      accessibilityViewIsModal
+      entering={FadeIn.duration(100)}
+      exiting={FadeOut}>
+      <Animated.View
+        entering={FadeInDown.duration(150)}
+        exiting={FadeOut}
         style={[
           styles.container,
           isMobile && styles.containerMobile,
@@ -42,15 +37,15 @@ export const Composer = observer(function ComposerImpl({
           pal.border,
         ]}>
         <ComposePost
-          replyTo={replyTo}
-          quote={quote}
-          onPost={onPost}
-          mention={mention}
+          replyTo={state.replyTo}
+          quote={state.quote}
+          onPost={state.onPost}
+          mention={state.mention}
         />
-      </View>
-    </View>
+      </Animated.View>
+    </Animated.View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   mask: {
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 7f5e6c5e5..459a021c4 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -10,14 +10,13 @@ import {
   ViewStyle,
 } from 'react-native'
 import {useNavigation, StackActions} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
+import {useQueryClient} from '@tanstack/react-query'
 import {s, colors} from 'lib/styles'
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
-import {useStores} from 'state/index'
 import {
   HomeIcon,
   HomeIconSolid,
@@ -42,20 +41,81 @@ import {getTabState, TabState} from 'lib/routes/helpers'
 import {NavigationProp} from 'lib/routes/types'
 import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
 import {isWeb} from 'platform/detection'
-import {formatCount, formatCountShortOnly} from 'view/com/util/numeric/format'
+import {formatCountShortOnly} from 'view/com/util/numeric/format'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useSetDrawerOpen} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {useSession, SessionAccount} from '#/state/session'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {emitSoftReset} from '#/state/events'
+import {useInviteCodesQuery} from '#/state/queries/invites'
+import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
+import {NavSignupCard} from '#/view/shell/NavSignupCard'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
-export const DrawerContent = observer(function DrawerContentImpl() {
+export function DrawerProfileCard({
+  account,
+  onPressProfile,
+}: {
+  account: SessionAccount
+  onPressProfile: () => void
+}) {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {data: profile} = useProfileQuery({did: account.did})
+
+  return (
+    <TouchableOpacity
+      testID="profileCardButton"
+      accessibilityLabel={_(msg`Profile`)}
+      accessibilityHint="Navigates to your profile"
+      onPress={onPressProfile}>
+      <UserAvatar
+        size={80}
+        avatar={profile?.avatar}
+        // See https://github.com/bluesky-social/social-app/pull/1801:
+        usePlainRNImage={true}
+      />
+      <Text
+        type="title-lg"
+        style={[pal.text, s.bold, styles.profileCardDisplayName]}
+        numberOfLines={1}>
+        {profile?.displayName || account.handle}
+      </Text>
+      <Text
+        type="2xl"
+        style={[pal.textLight, styles.profileCardHandle]}
+        numberOfLines={1}>
+        @{account.handle}
+      </Text>
+      <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
+        <Text type="xl-medium" style={pal.text}>
+          {formatCountShortOnly(profile?.followersCount ?? 0)}
+        </Text>{' '}
+        {pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '}
+        <Text type="xl-medium" style={pal.text}>
+          {formatCountShortOnly(profile?.followsCount ?? 0)}
+        </Text>{' '}
+        following
+      </Text>
+    </TouchableOpacity>
+  )
+}
+
+export function DrawerContent() {
   const theme = useTheme()
   const pal = usePalette('default')
-  const store = useStores()
+  const {_} = useLingui()
+  const queryClient = useQueryClient()
   const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
   const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
     useNavigationTabState()
-
-  const {notifications} = store.me
+  const {hasSession, currentAccount} = useSession()
+  const numUnreadNotifications = useUnreadNotifications()
 
   // events
   // =
@@ -68,7 +128,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       if (isWeb) {
         // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
         if (tab === 'MyProfile') {
-          navigation.navigate('Profile', {name: store.me.handle})
+          navigation.navigate('Profile', {name: currentAccount!.handle})
         } else {
           // @ts-ignore must be Home, Search, Notifications, or MyProfile
           navigation.navigate(tab)
@@ -76,16 +136,20 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       } else {
         const tabState = getTabState(state, tab)
         if (tabState === TabState.InsideAtRoot) {
-          store.emitScreenSoftReset()
+          emitSoftReset()
         } else if (tabState === TabState.Inside) {
           navigation.dispatch(StackActions.popToTop())
         } else {
+          if (tab === 'Notifications') {
+            // fetch new notifs on view
+            truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
+          }
           // @ts-ignore must be Home, Search, Notifications, or MyProfile
           navigation.navigate(`${tab}Tab`)
         }
       }
     },
-    [store, track, navigation, setDrawerOpen],
+    [track, navigation, setDrawerOpen, currentAccount, queryClient],
   )
 
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
@@ -131,11 +195,11 @@ export const DrawerContent = observer(function DrawerContentImpl() {
     track('Menu:FeedbackClicked')
     Linking.openURL(
       FEEDBACK_FORM_URL({
-        email: store.session.currentSession?.email,
-        handle: store.session.currentSession?.handle,
+        email: currentAccount?.email,
+        handle: currentAccount?.handle,
       }),
     )
-  }, [track, store.session.currentSession])
+  }, [track, currentAccount])
 
   const onPressHelp = React.useCallback(() => {
     track('Menu:HelpClicked')
@@ -154,48 +218,20 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       ]}>
       <SafeAreaView style={s.flex1}>
         <ScrollView style={styles.main}>
-          <View style={{}}>
-            <TouchableOpacity
-              testID="profileCardButton"
-              accessibilityLabel="Profile"
-              accessibilityHint="Navigates to your profile"
-              onPress={onPressProfile}>
-              <UserAvatar
-                size={80}
-                avatar={store.me.avatar}
-                // See https://github.com/bluesky-social/social-app/pull/1801:
-                usePlainRNImage={true}
+          {hasSession && currentAccount ? (
+            <View style={{}}>
+              <DrawerProfileCard
+                account={currentAccount}
+                onPressProfile={onPressProfile}
               />
-              <Text
-                type="title-lg"
-                style={[pal.text, s.bold, styles.profileCardDisplayName]}
-                numberOfLines={1}>
-                {store.me.displayName || store.me.handle}
-              </Text>
-              <Text
-                type="2xl"
-                style={[pal.textLight, styles.profileCardHandle]}
-                numberOfLines={1}>
-                @{store.me.handle}
-              </Text>
-              <Text
-                type="xl"
-                style={[pal.textLight, styles.profileCardFollowers]}>
-                <Text type="xl-medium" style={pal.text}>
-                  {formatCountShortOnly(store.me.followersCount ?? 0)}
-                </Text>{' '}
-                {pluralize(store.me.followersCount || 0, 'follower')} &middot;{' '}
-                <Text type="xl-medium" style={pal.text}>
-                  {formatCountShortOnly(store.me.followsCount ?? 0)}
-                </Text>{' '}
-                following
-              </Text>
-            </TouchableOpacity>
-          </View>
+            </View>
+          ) : (
+            <NavSignupCard />
+          )}
 
-          <InviteCodes style={{paddingLeft: 0}} />
+          {hasSession && <InviteCodes style={{paddingLeft: 0}} />}
 
-          <View style={{height: 10}} />
+          {hasSession && <View style={{height: 10}} />}
 
           <MenuItem
             icon={
@@ -213,8 +249,8 @@ export const DrawerContent = observer(function DrawerContentImpl() {
                 />
               )
             }
-            label="Search"
-            accessibilityLabel="Search"
+            label={_(msg`Search`)}
+            accessibilityLabel={_(msg`Search`)}
             accessibilityHint=""
             bold={isAtSearch}
             onPress={onPressSearch}
@@ -235,39 +271,43 @@ export const DrawerContent = observer(function DrawerContentImpl() {
                 />
               )
             }
-            label="Home"
-            accessibilityLabel="Home"
+            label={_(msg`Home`)}
+            accessibilityLabel={_(msg`Home`)}
             accessibilityHint=""
             bold={isAtHome}
             onPress={onPressHome}
           />
-          <MenuItem
-            icon={
-              isAtNotifications ? (
-                <BellIconSolid
-                  style={pal.text as StyleProp<ViewStyle>}
-                  size="24"
-                  strokeWidth={1.7}
-                />
-              ) : (
-                <BellIcon
-                  style={pal.text as StyleProp<ViewStyle>}
-                  size="24"
-                  strokeWidth={1.7}
-                />
-              )
-            }
-            label="Notifications"
-            accessibilityLabel="Notifications"
-            accessibilityHint={
-              notifications.unreadCountLabel === ''
-                ? ''
-                : `${notifications.unreadCountLabel} unread`
-            }
-            count={notifications.unreadCountLabel}
-            bold={isAtNotifications}
-            onPress={onPressNotifications}
-          />
+
+          {hasSession && (
+            <MenuItem
+              icon={
+                isAtNotifications ? (
+                  <BellIconSolid
+                    style={pal.text as StyleProp<ViewStyle>}
+                    size="24"
+                    strokeWidth={1.7}
+                  />
+                ) : (
+                  <BellIcon
+                    style={pal.text as StyleProp<ViewStyle>}
+                    size="24"
+                    strokeWidth={1.7}
+                  />
+                )
+              }
+              label={_(msg`Notifications`)}
+              accessibilityLabel={_(msg`Notifications`)}
+              accessibilityHint={
+                numUnreadNotifications === ''
+                  ? ''
+                  : `${numUnreadNotifications} unread`
+              }
+              count={numUnreadNotifications}
+              bold={isAtNotifications}
+              onPress={onPressNotifications}
+            />
+          )}
+
           <MenuItem
             icon={
               isAtFeeds ? (
@@ -284,68 +324,74 @@ export const DrawerContent = observer(function DrawerContentImpl() {
                 />
               )
             }
-            label="Feeds"
-            accessibilityLabel="Feeds"
+            label={_(msg`Feeds`)}
+            accessibilityLabel={_(msg`Feeds`)}
             accessibilityHint=""
             bold={isAtFeeds}
             onPress={onPressMyFeeds}
           />
-          <MenuItem
-            icon={<ListIcon strokeWidth={2} style={pal.text} size={26} />}
-            label="Lists"
-            accessibilityLabel="Lists"
-            accessibilityHint=""
-            onPress={onPressLists}
-          />
-          <MenuItem
-            icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />}
-            label="Moderation"
-            accessibilityLabel="Moderation"
-            accessibilityHint=""
-            onPress={onPressModeration}
-          />
-          <MenuItem
-            icon={
-              isAtMyProfile ? (
-                <UserIconSolid
-                  style={pal.text as StyleProp<ViewStyle>}
-                  size="26"
-                  strokeWidth={1.5}
-                />
-              ) : (
-                <UserIcon
-                  style={pal.text as StyleProp<ViewStyle>}
-                  size="26"
-                  strokeWidth={1.5}
-                />
-              )
-            }
-            label="Profile"
-            accessibilityLabel="Profile"
-            accessibilityHint=""
-            onPress={onPressProfile}
-          />
-          <MenuItem
-            icon={
-              <CogIcon
-                style={pal.text as StyleProp<ViewStyle>}
-                size="26"
-                strokeWidth={1.75}
+
+          {hasSession && (
+            <>
+              <MenuItem
+                icon={<ListIcon strokeWidth={2} style={pal.text} size={26} />}
+                label={_(msg`Lists`)}
+                accessibilityLabel={_(msg`Lists`)}
+                accessibilityHint=""
+                onPress={onPressLists}
               />
-            }
-            label="Settings"
-            accessibilityLabel="Settings"
-            accessibilityHint=""
-            onPress={onPressSettings}
-          />
+              <MenuItem
+                icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />}
+                label={_(msg`Moderation`)}
+                accessibilityLabel={_(msg`Moderation`)}
+                accessibilityHint=""
+                onPress={onPressModeration}
+              />
+              <MenuItem
+                icon={
+                  isAtMyProfile ? (
+                    <UserIconSolid
+                      style={pal.text as StyleProp<ViewStyle>}
+                      size="26"
+                      strokeWidth={1.5}
+                    />
+                  ) : (
+                    <UserIcon
+                      style={pal.text as StyleProp<ViewStyle>}
+                      size="26"
+                      strokeWidth={1.5}
+                    />
+                  )
+                }
+                label={_(msg`Profile`)}
+                accessibilityLabel={_(msg`Profile`)}
+                accessibilityHint=""
+                onPress={onPressProfile}
+              />
+              <MenuItem
+                icon={
+                  <CogIcon
+                    style={pal.text as StyleProp<ViewStyle>}
+                    size="26"
+                    strokeWidth={1.75}
+                  />
+                }
+                label={_(msg`Settings`)}
+                accessibilityLabel={_(msg`Settings`)}
+                accessibilityHint=""
+                onPress={onPressSettings}
+              />
+            </>
+          )}
 
           <View style={styles.smallSpacer} />
           <View style={styles.smallSpacer} />
         </ScrollView>
+
         <View style={styles.footer}>
           <TouchableOpacity
             accessibilityRole="link"
-            accessibilityLabel="Send feedback"
+            accessibilityLabel={_(msg`Send feedback`)}
             accessibilityHint=""
             onPress={onPressFeedback}
             style={[
@@ -361,24 +407,24 @@ export const DrawerContent = observer(function DrawerContentImpl() {
               icon={['far', 'message']}
             />
             <Text type="lg-medium" style={[pal.link, s.pl10]}>
-              Feedback
+              <Trans>Feedback</Trans>
             </Text>
           </TouchableOpacity>
           <TouchableOpacity
             accessibilityRole="link"
-            accessibilityLabel="Send feedback"
+            accessibilityLabel={_(msg`Send feedback`)}
             accessibilityHint=""
             onPress={onPressHelp}
             style={[styles.footerBtn]}>
             <Text type="lg-medium" style={[pal.link, s.pl10]}>
-              Help
+              <Trans>Help</Trans>
             </Text>
           </TouchableOpacity>
         </View>
       </SafeAreaView>
     </View>
   )
-})
+}
 
 interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> {
   icon: JSX.Element
@@ -432,50 +478,54 @@ function MenuItem({
   )
 }
 
-const InviteCodes = observer(function InviteCodesImpl({
-  style,
-}: {
-  style?: StyleProp<ViewStyle>
-}) {
+function InviteCodes({style}: {style?: StyleProp<ViewStyle>}) {
   const {track} = useAnalytics()
-  const store = useStores()
   const setDrawerOpen = useSetDrawerOpen()
   const pal = usePalette('default')
-  const {invitesAvailable} = store.me
+  const {data: invites} = useInviteCodesQuery()
+  const invitesAvailable = invites?.available?.length ?? 0
+  const {openModal} = useModalControls()
+  const {_} = useLingui()
+
   const onPress = React.useCallback(() => {
     track('Menu:ItemClicked', {url: '#invite-codes'})
     setDrawerOpen(false)
-    store.shell.openModal({name: 'invite-codes'})
-  }, [store, track, setDrawerOpen])
+    openModal({name: 'invite-codes'})
+  }, [openModal, track, setDrawerOpen])
+
   return (
     <TouchableOpacity
       testID="menuItemInviteCodes"
       style={[styles.inviteCodes, style]}
       onPress={onPress}
       accessibilityRole="button"
-      accessibilityLabel={
-        invitesAvailable === 1
-          ? 'Invite codes: 1 available'
-          : `Invite codes: ${invitesAvailable} available`
-      }
-      accessibilityHint="Opens list of invite codes">
+      accessibilityLabel={_(msg`Invite codes: ${invitesAvailable} available`)}
+      accessibilityHint={_(msg`Opens list of invite codes`)}
+      disabled={invites?.disabled}>
       <FontAwesomeIcon
         icon="ticket"
         style={[
           styles.inviteCodesIcon,
-          store.me.invitesAvailable > 0 ? pal.link : pal.textLight,
+          invitesAvailable > 0 ? pal.link : pal.textLight,
         ]}
         size={18}
       />
       <Text
         type="lg-medium"
-        style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}>
-        {formatCount(store.me.invitesAvailable)} invite{' '}
-        {pluralize(store.me.invitesAvailable, 'code')}
+        style={invitesAvailable > 0 ? pal.link : pal.textLight}>
+        {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>
   )
-})
+}
 
 const styles = StyleSheet.create({
   view: {
@@ -548,10 +598,11 @@ const styles = StyleSheet.create({
     paddingLeft: 22,
     paddingVertical: 8,
     flexDirection: 'row',
-    alignItems: 'center',
   },
   inviteCodesIcon: {
     marginRight: 6,
+    flexShrink: 0,
+    marginTop: 2,
   },
 
   footer: {
diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx
new file mode 100644
index 000000000..7026dd2a6
--- /dev/null
+++ b/src/view/shell/NavSignupCard.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {DefaultAvatar} from '#/view/com/util/UserAvatar'
+import {Text} from '#/view/com/util/text/Text'
+import {Button} from '#/view/com/util/forms/Button'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+
+export function NavSignupCard() {
+  const {_} = useLingui()
+  const pal = usePalette('default')
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+  const closeAllActiveElements = useCloseAllActiveElements()
+
+  const showLoggedOut = React.useCallback(() => {
+    closeAllActiveElements()
+    setShowLoggedOut(true)
+  }, [setShowLoggedOut, closeAllActiveElements])
+
+  return (
+    <View
+      style={{
+        alignItems: 'flex-start',
+        paddingTop: 6,
+        marginBottom: 24,
+      }}>
+      <DefaultAvatar type="user" size={48} />
+
+      <View style={{paddingTop: 12}}>
+        <Text type="md" style={[pal.text, s.bold]}>
+          <Trans>Sign up or sign in to join the conversation</Trans>
+        </Text>
+      </View>
+
+      <View style={{flexDirection: 'row', paddingTop: 12, gap: 8}}>
+        <Button
+          onPress={showLoggedOut}
+          accessibilityHint={_(msg`Sign up`)}
+          accessibilityLabel={_(msg`Sign up`)}>
+          <Text type="md" style={[{color: 'white'}, s.bold]}>
+            <Trans>Sign up</Trans>
+          </Text>
+        </Button>
+        <Button
+          type="default"
+          onPress={showLoggedOut}
+          accessibilityHint={_(msg`Sign in`)}
+          accessibilityLabel={_(msg`Sign in`)}>
+          <Text type="md" style={[pal.text, s.bold]}>
+            Sign in
+          </Text>
+        </Button>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index d360ceead..746b4d123 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -1,12 +1,11 @@
 import React, {ComponentProps} from 'react'
 import {GestureResponderEvent, TouchableOpacity, View} from 'react-native'
 import Animated from 'react-native-reanimated'
+import {useQueryClient} from '@tanstack/react-query'
 import {StackActions} from '@react-navigation/native'
 import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {observer} from 'mobx-react-lite'
 import {Text} from 'view/com/util/text/Text'
-import {useStores} from 'state/index'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {clamp} from 'lib/numbers'
 import {
@@ -24,21 +23,33 @@ import {styles} from './BottomBarStyles'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
 import {UserAvatar} from 'view/com/util/UserAvatar'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {useModalControls} from '#/state/modals'
+import {useShellLayout} from '#/state/shell/shell-layout'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {emitSoftReset} from '#/state/events'
+import {useSession} from '#/state/session'
+import {useProfileQuery} from '#/state/queries/profile'
+import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
-export const BottomBar = observer(function BottomBarImpl({
-  navigation,
-}: BottomTabBarProps) {
-  const store = useStores()
+export function BottomBar({navigation}: BottomTabBarProps) {
+  const {openModal} = useModalControls()
+  const {hasSession, currentAccount} = useSession()
   const pal = usePalette('default')
+  const {_} = useLingui()
+  const queryClient = useQueryClient()
   const safeAreaInsets = useSafeAreaInsets()
   const {track} = useAnalytics()
+  const {footerHeight} = useShellLayout()
   const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
     useNavigationTabState()
-
-  const {minimalShellMode, footerMinimalShellTransform} = useMinimalShellMode()
-  const {notifications} = store.me
+  const numUnreadNotifications = useUnreadNotifications()
+  const {footerMinimalShellTransform} = useMinimalShellMode()
+  const {data: profile} = useProfileQuery({did: currentAccount?.did})
 
   const onPressTab = React.useCallback(
     (tab: TabOptions) => {
@@ -46,14 +57,18 @@ export const BottomBar = observer(function BottomBarImpl({
       const state = navigation.getState()
       const tabState = getTabState(state, tab)
       if (tabState === TabState.InsideAtRoot) {
-        store.emitScreenSoftReset()
+        emitSoftReset()
       } else if (tabState === TabState.Inside) {
         navigation.dispatch(StackActions.popToTop())
       } else {
+        if (tab === 'Notifications') {
+          // fetch new notifs on view
+          truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
+        }
         navigation.navigate(`${tab}Tab`)
       }
     },
-    [store, track, navigation],
+    [track, navigation, queryClient],
   )
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
   const onPressSearch = React.useCallback(
@@ -72,8 +87,8 @@ export const BottomBar = observer(function BottomBarImpl({
     onPressTab('MyProfile')
   }, [onPressTab])
   const onLongPressProfile = React.useCallback(() => {
-    store.shell.openModal({name: 'switch-account'})
-  }, [store])
+    openModal({name: 'switch-account'})
+  }, [openModal])
 
   return (
     <Animated.View
@@ -83,8 +98,10 @@ export const BottomBar = observer(function BottomBarImpl({
         pal.border,
         {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
         footerMinimalShellTransform,
-        minimalShellMode && styles.disabled,
-      ]}>
+      ]}
+      onLayout={e => {
+        footerHeight.value = e.nativeEvent.layout.height
+      }}>
       <Btn
         testID="bottomBarHomeBtn"
         icon={
@@ -104,7 +121,7 @@ export const BottomBar = observer(function BottomBarImpl({
         }
         onPress={onPressHome}
         accessibilityRole="tab"
-        accessibilityLabel="Home"
+        accessibilityLabel={_(msg`Home`)}
         accessibilityHint=""
       />
       <Btn
@@ -126,7 +143,7 @@ export const BottomBar = observer(function BottomBarImpl({
         }
         onPress={onPressSearch}
         accessibilityRole="search"
-        accessibilityLabel="Search"
+        accessibilityLabel={_(msg`Search`)}
         accessibilityHint=""
       />
       <Btn
@@ -148,78 +165,83 @@ export const BottomBar = observer(function BottomBarImpl({
         }
         onPress={onPressFeeds}
         accessibilityRole="tab"
-        accessibilityLabel="Feeds"
+        accessibilityLabel={_(msg`Feeds`)}
         accessibilityHint=""
       />
-      <Btn
-        testID="bottomBarNotificationsBtn"
-        icon={
-          isAtNotifications ? (
-            <BellIconSolid
-              size={24}
-              strokeWidth={1.9}
-              style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
-            />
-          ) : (
-            <BellIcon
-              size={24}
-              strokeWidth={1.9}
-              style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
-            />
-          )
-        }
-        onPress={onPressNotifications}
-        notificationCount={notifications.unreadCountLabel}
-        accessible={true}
-        accessibilityRole="tab"
-        accessibilityLabel="Notifications"
-        accessibilityHint={
-          notifications.unreadCountLabel === ''
-            ? ''
-            : `${notifications.unreadCountLabel} unread`
-        }
-      />
-      <Btn
-        testID="bottomBarProfileBtn"
-        icon={
-          <View style={styles.ctrlIconSizingWrapper}>
-            {isAtMyProfile ? (
-              <View
-                style={[
-                  styles.ctrlIcon,
-                  pal.text,
-                  styles.profileIcon,
-                  styles.onProfile,
-                  {borderColor: pal.text.color},
-                ]}>
-                <UserAvatar
-                  avatar={store.me.avatar}
-                  size={27}
-                  // See https://github.com/bluesky-social/social-app/pull/1801:
-                  usePlainRNImage={true}
+
+      {hasSession && (
+        <>
+          <Btn
+            testID="bottomBarNotificationsBtn"
+            icon={
+              isAtNotifications ? (
+                <BellIconSolid
+                  size={24}
+                  strokeWidth={1.9}
+                  style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
                 />
-              </View>
-            ) : (
-              <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}>
-                <UserAvatar
-                  avatar={store.me.avatar}
-                  size={28}
-                  // See https://github.com/bluesky-social/social-app/pull/1801:
-                  usePlainRNImage={true}
+              ) : (
+                <BellIcon
+                  size={24}
+                  strokeWidth={1.9}
+                  style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
                 />
+              )
+            }
+            onPress={onPressNotifications}
+            notificationCount={numUnreadNotifications}
+            accessible={true}
+            accessibilityRole="tab"
+            accessibilityLabel={_(msg`Notifications`)}
+            accessibilityHint={
+              numUnreadNotifications === ''
+                ? ''
+                : `${numUnreadNotifications} unread`
+            }
+          />
+          <Btn
+            testID="bottomBarProfileBtn"
+            icon={
+              <View style={styles.ctrlIconSizingWrapper}>
+                {isAtMyProfile ? (
+                  <View
+                    style={[
+                      styles.ctrlIcon,
+                      pal.text,
+                      styles.profileIcon,
+                      styles.onProfile,
+                      {borderColor: pal.text.color},
+                    ]}>
+                    <UserAvatar
+                      avatar={profile?.avatar}
+                      size={27}
+                      // See https://github.com/bluesky-social/social-app/pull/1801:
+                      usePlainRNImage={true}
+                    />
+                  </View>
+                ) : (
+                  <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}>
+                    <UserAvatar
+                      avatar={profile?.avatar}
+                      size={28}
+                      // See https://github.com/bluesky-social/social-app/pull/1801:
+                      usePlainRNImage={true}
+                    />
+                  </View>
+                )}
               </View>
-            )}
-          </View>
-        }
-        onPress={onPressProfile}
-        onLongPress={onLongPressProfile}
-        accessibilityRole="tab"
-        accessibilityLabel="Profile"
-        accessibilityHint=""
-      />
+            }
+            onPress={onPressProfile}
+            onLongPress={onLongPressProfile}
+            accessibilityRole="tab"
+            accessibilityLabel={_(msg`Profile`)}
+            accessibilityHint=""
+          />
+        </>
+      )}
     </Animated.View>
   )
-})
+}
 
 interface BtnProps
   extends Pick<
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index c175ed848..ae9381440 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -65,7 +65,4 @@ export const styles = StyleSheet.create({
     borderWidth: 1,
     borderRadius: 100,
   },
-  disabled: {
-    pointerEvents: 'none',
-  },
 })
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index ebcc527a1..3a60bd3b1 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -1,6 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useNavigationState} from '@react-navigation/native'
 import Animated from 'react-native-reanimated'
@@ -23,9 +21,10 @@ import {Link} from 'view/com/util/Link'
 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
 import {makeProfileLink} from 'lib/routes/links'
 import {CommonNavigatorParams} from 'lib/routes/types'
+import {useSession} from '#/state/session'
 
-export const BottomBarWeb = observer(function BottomBarWebImpl() {
-  const store = useStores()
+export function BottomBarWeb() {
+  const {hasSession, currentAccount} = useSession()
   const pal = usePalette('default')
   const safeAreaInsets = useSafeAreaInsets()
   const {footerMinimalShellTransform} = useMinimalShellMode()
@@ -76,55 +75,69 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() {
           )
         }}
       </NavItem>
-      <NavItem routeName="Notifications" href="/notifications">
-        {({isActive}) => {
-          const Icon = isActive ? BellIconSolid : BellIcon
-          return (
-            <Icon
-              size={24}
-              strokeWidth={1.9}
-              style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
-            />
-          )
-        }}
-      </NavItem>
-      <NavItem routeName="Profile" href={makeProfileLink(store.me)}>
-        {({isActive}) => {
-          const Icon = isActive ? UserIconSolid : UserIcon
-          return (
-            <Icon
-              size={28}
-              strokeWidth={1.5}
-              style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
-            />
-          )
-        }}
-      </NavItem>
+
+      {hasSession && (
+        <>
+          <NavItem routeName="Notifications" href="/notifications">
+            {({isActive}) => {
+              const Icon = isActive ? BellIconSolid : BellIcon
+              return (
+                <Icon
+                  size={24}
+                  strokeWidth={1.9}
+                  style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
+                />
+              )
+            }}
+          </NavItem>
+          <NavItem
+            routeName="Profile"
+            href={
+              currentAccount
+                ? makeProfileLink({
+                    did: currentAccount.did,
+                    handle: currentAccount.handle,
+                  })
+                : '/'
+            }>
+            {({isActive}) => {
+              const Icon = isActive ? UserIconSolid : UserIcon
+              return (
+                <Icon
+                  size={28}
+                  strokeWidth={1.5}
+                  style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
+                />
+              )
+            }}
+          </NavItem>
+        </>
+      )}
     </Animated.View>
   )
-})
+}
 
 const NavItem: React.FC<{
   children: (props: {isActive: boolean}) => React.ReactChild
   href: string
   routeName: string
 }> = ({children, href, routeName}) => {
+  const {currentAccount} = useSession()
   const currentRoute = useNavigationState(state => {
     if (!state) {
       return {name: 'Home'}
     }
     return getCurrentRoute(state)
   })
-  const store = useStores()
   const isActive =
     currentRoute.name === 'Profile'
       ? isTab(currentRoute.name, routeName) &&
         (currentRoute.params as CommonNavigatorParams['Profile']).name ===
-          store.me.handle
+          currentAccount?.handle
       : isTab(currentRoute.name, routeName)
 
   return (
-    <Link href={href} style={styles.ctrl}>
+    <Link href={href} style={styles.ctrl} navigationAction="navigate">
       {children({isActive})}
     </Link>
   )
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
new file mode 100644
index 000000000..c7b5d1d2e
--- /dev/null
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -0,0 +1,150 @@
+import * as React from 'react'
+import {View} from 'react-native'
+
+// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
+// MIT License
+// Copyright (c) 2017 React Navigation Contributors
+
+import {
+  createNavigatorFactory,
+  EventArg,
+  ParamListBase,
+  StackActionHelpers,
+  StackActions,
+  StackNavigationState,
+  StackRouter,
+  StackRouterOptions,
+  useNavigationBuilder,
+} from '@react-navigation/native'
+import type {
+  NativeStackNavigationEventMap,
+  NativeStackNavigationOptions,
+} from '@react-navigation/native-stack'
+import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
+import {NativeStackView} from '@react-navigation/native-stack'
+
+import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
+import {DesktopLeftNav} from './desktop/LeftNav'
+import {DesktopRightNav} from './desktop/RightNav'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {useOnboardingState} from '#/state/shell'
+import {
+  useLoggedOutView,
+  useLoggedOutViewControls,
+} from '#/state/shell/logged-out'
+import {useSession} from '#/state/session'
+import {isWeb} from 'platform/detection'
+import {LoggedOut} from '../com/auth/LoggedOut'
+import {Onboarding} from '../com/auth/Onboarding'
+
+type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
+  requireAuth?: boolean
+}
+
+function NativeStackNavigator({
+  id,
+  initialRouteName,
+  children,
+  screenListeners,
+  screenOptions,
+  ...rest
+}: NativeStackNavigatorProps) {
+  // --- this is copy and pasted from the original native stack navigator ---
+  const {state, descriptors, navigation, NavigationContent} =
+    useNavigationBuilder<
+      StackNavigationState<ParamListBase>,
+      StackRouterOptions,
+      StackActionHelpers<ParamListBase>,
+      NativeStackNavigationOptionsWithAuth,
+      NativeStackNavigationEventMap
+    >(StackRouter, {
+      id,
+      initialRouteName,
+      children,
+      screenListeners,
+      screenOptions,
+    })
+  React.useEffect(
+    () =>
+      // @ts-expect-error: there may not be a tab navigator in parent
+      navigation?.addListener?.('tabPress', (e: any) => {
+        const isFocused = navigation.isFocused()
+
+        // Run the operation in the next frame so we're sure all listeners have been run
+        // This is necessary to know if preventDefault() has been called
+        requestAnimationFrame(() => {
+          if (
+            state.index > 0 &&
+            isFocused &&
+            !(e as EventArg<'tabPress', true>).defaultPrevented
+          ) {
+            // When user taps on already focused tab and we're inside the tab,
+            // reset the stack to replicate native behaviour
+            navigation.dispatch({
+              ...StackActions.popToTop(),
+              target: state.key,
+            })
+          }
+        })
+      }),
+    [navigation, state.index, state.key],
+  )
+
+  // --- our custom logic starts here ---
+  const {hasSession} = useSession()
+  const activeRoute = state.routes[state.index]
+  const activeDescriptor = descriptors[activeRoute.key]
+  const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false
+  const onboardingState = useOnboardingState()
+  const {showLoggedOut} = useLoggedOutView()
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+  const {isMobile} = useWebMediaQueries()
+  if (activeRouteRequiresAuth && !hasSession) {
+    return <LoggedOut />
+  }
+  if (showLoggedOut) {
+    return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
+  }
+  if (onboardingState.isActive) {
+    return <Onboarding />
+  }
+  const newDescriptors: typeof descriptors = {}
+  for (let key in descriptors) {
+    const descriptor = descriptors[key]
+    const requireAuth = descriptor.options.requireAuth ?? false
+    newDescriptors[key] = {
+      ...descriptor,
+      render() {
+        if (requireAuth && !hasSession) {
+          return <View />
+        } else {
+          return descriptor.render()
+        }
+      },
+    }
+  }
+  return (
+    <NavigationContent>
+      <NativeStackView
+        {...rest}
+        state={state}
+        navigation={navigation}
+        descriptors={newDescriptors}
+      />
+      {isWeb && isMobile && <BottomBarWeb />}
+      {isWeb && !isMobile && (
+        <>
+          <DesktopLeftNav />
+          <DesktopRightNav />
+        </>
+      )}
+    </NavigationContent>
+  )
+}
+
+export const createNativeStackNavigatorWithAuth = createNavigatorFactory<
+  StackNavigationState<ParamListBase>,
+  NativeStackNavigationOptionsWithAuth,
+  NativeStackNavigationEventMap,
+  typeof NativeStackNavigator
+>(NativeStackNavigator)
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 3237d2cdd..ff51ffe22 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -1,17 +1,17 @@
 import React from 'react'
 import {View, StyleSheet} from 'react-native'
 import {useNavigationState} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useDesktopRightNavItems} from 'lib/hooks/useDesktopRightNavItems'
 import {TextLink} from 'view/com/util/Link'
 import {getCurrentRoute} from 'lib/routes/helpers'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
+import {usePinnedFeedsInfos} from '#/state/queries/feed'
 
-export const DesktopFeeds = observer(function DesktopFeeds() {
-  const store = useStores()
+export function DesktopFeeds() {
   const pal = usePalette('default')
-  const items = useDesktopRightNavItems(store.preferences.pinnedFeeds)
+  const {_} = useLingui()
+  const {feeds} = usePinnedFeedsInfos()
 
   const route = useNavigationState(state => {
     if (!state) {
@@ -23,40 +23,40 @@ export const DesktopFeeds = observer(function DesktopFeeds() {
   return (
     <View style={[styles.container, pal.view, pal.border]}>
       <FeedItem href="/" title="Following" current={route.name === 'Home'} />
-      {items.map(item => {
-        try {
-          const params = route.params as Record<string, string>
-          const routeName =
-            item.collection === 'app.bsky.feed.generator'
-              ? 'ProfileFeed'
-              : 'ProfileList'
-          return (
-            <FeedItem
-              key={item.uri}
-              href={item.href}
-              title={item.displayName}
-              current={
-                route.name === routeName &&
-                params.name === item.hostname &&
-                params.rkey === item.rkey
-              }
-            />
-          )
-        } catch {
-          return null
-        }
-      })}
+      {feeds
+        .filter(f => f.displayName !== 'Following')
+        .map(feed => {
+          try {
+            const params = route.params as Record<string, string>
+            const routeName =
+              feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList'
+            return (
+              <FeedItem
+                key={feed.uri}
+                href={feed.route.href}
+                title={feed.displayName}
+                current={
+                  route.name === routeName &&
+                  params.name === feed.route.params.name &&
+                  params.rkey === feed.route.params.rkey
+                }
+              />
+            )
+          } catch {
+            return null
+          }
+        })}
       <View style={{paddingTop: 8, paddingBottom: 6}}>
         <TextLink
           type="lg"
           href="/feeds"
-          text="More feeds"
+          text={_(msg`More feeds`)}
           style={[pal.link]}
         />
       </View>
     </View>
   )
-})
+}
 
 function FeedItem({
   title,
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 39271605c..2ed294501 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {PressableWithHover} from 'view/com/util/PressableWithHover'
 import {
@@ -16,7 +15,6 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
 import {Link} from 'view/com/util/Link'
 import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s, colors} from 'lib/styles'
 import {
@@ -39,18 +37,36 @@ import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
 import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
 import {router} from '../../../routes'
 import {makeProfileLink} from 'lib/routes/links'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {useComposerControls} from '#/state/shell/composer'
+import {useFetchHandle} from '#/state/queries/handle'
+import {emitSoftReset} from '#/state/events'
+import {useQueryClient} from '@tanstack/react-query'
+import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
+import {NavSignupCard} from '#/view/shell/NavSignupCard'
+import {truncateAndInvalidate} from '#/state/queries/util'
 
-const ProfileCard = observer(function ProfileCardImpl() {
-  const store = useStores()
+function ProfileCard() {
+  const {currentAccount} = useSession()
+  const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did})
   const {isDesktop} = useWebMediaQueries()
+  const {_} = useLingui()
   const size = 48
-  return store.me.handle ? (
+
+  return !isLoading && profile ? (
     <Link
-      href={makeProfileLink(store.me)}
+      href={makeProfileLink({
+        did: currentAccount!.did,
+        handle: currentAccount!.handle,
+      })}
       style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
-      title="My Profile"
+      title={_(msg`My Profile`)}
       asAnchor>
-      <UserAvatar avatar={store.me.avatar} size={size} />
+      <UserAvatar avatar={profile.avatar} size={size} />
     </Link>
   ) : (
     <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>
@@ -61,12 +77,13 @@ const ProfileCard = observer(function ProfileCardImpl() {
       />
     </View>
   )
-})
+}
 
 function BackBtn() {
   const {isTablet} = useWebMediaQueries()
   const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
+  const {_} = useLingui()
   const shouldShow = useNavigationState(state => !isStateAtTabRoot(state))
 
   const onPressBack = React.useCallback(() => {
@@ -86,7 +103,7 @@ function BackBtn() {
       onPress={onPressBack}
       style={styles.backBtn}
       accessibilityRole="button"
-      accessibilityLabel="Go back"
+      accessibilityLabel={_(msg`Go back`)}
       accessibilityHint="">
       <FontAwesomeIcon
         size={24}
@@ -104,15 +121,10 @@ interface NavItemProps {
   iconFilled: JSX.Element
   label: string
 }
-const NavItem = observer(function NavItemImpl({
-  count,
-  href,
-  icon,
-  iconFilled,
-  label,
-}: NavItemProps) {
+function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
   const pal = usePalette('default')
-  const store = useStores()
+  const queryClient = useQueryClient()
+  const {currentAccount} = useSession()
   const {isDesktop, isTablet} = useWebMediaQueries()
   const [pathName] = React.useMemo(() => router.matchPath(href), [href])
   const currentRouteInfo = useNavigationState(state => {
@@ -125,7 +137,7 @@ const NavItem = observer(function NavItemImpl({
     currentRouteInfo.name === 'Profile'
       ? isTab(currentRouteInfo.name, pathName) &&
         (currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
-          store.me.handle
+          currentAccount?.handle
       : isTab(currentRouteInfo.name, pathName)
   const {onPress} = useLinkProps({to: href})
   const onPressWrapped = React.useCallback(
@@ -135,12 +147,16 @@ const NavItem = observer(function NavItemImpl({
       }
       e.preventDefault()
       if (isCurrent) {
-        store.emitScreenSoftReset()
+        emitSoftReset()
       } else {
+        if (href === '/notifications') {
+          // fetch new notifs on view
+          truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
+        }
         onPress()
       }
     },
-    [onPress, isCurrent, store],
+    [onPress, isCurrent, queryClient, href],
   )
 
   return (
@@ -179,12 +195,16 @@ const NavItem = observer(function NavItemImpl({
       )}
     </PressableWithHover>
   )
-})
+}
 
 function ComposeBtn() {
-  const store = useStores()
+  const {currentAccount} = useSession()
   const {getState} = useNavigation()
+  const {openComposer} = useComposerControls()
+  const {_} = useLingui()
   const {isTablet} = useWebMediaQueries()
+  const [isFetchingHandle, setIsFetchingHandle] = React.useState(false)
+  const fetchHandle = useFetchHandle()
 
   const getProfileHandle = async () => {
     const {routes} = getState()
@@ -196,13 +216,21 @@ function ComposeBtn() {
       ).name
 
       if (handle.startsWith('did:')) {
-        const cached = await store.profiles.cache.get(handle)
-        const profile = cached ? cached.data : undefined
-        // if we can't resolve handle, set to undefined
-        handle = profile?.handle || undefined
+        try {
+          setIsFetchingHandle(true)
+          handle = await fetchHandle(handle)
+        } catch (e) {
+          handle = undefined
+        } finally {
+          setIsFetchingHandle(false)
+        }
       }
 
-      if (!handle || handle === store.me.handle || handle === 'handle.invalid')
+      if (
+        !handle ||
+        handle === currentAccount?.handle ||
+        handle === 'handle.invalid'
+      )
         return undefined
 
       return handle
@@ -212,17 +240,18 @@ function ComposeBtn() {
   }
 
   const onPressCompose = async () =>
-    store.shell.openComposer({mention: await getProfileHandle()})
+    openComposer({mention: await getProfileHandle()})
 
   if (isTablet) {
     return null
   }
   return (
     <TouchableOpacity
+      disabled={isFetchingHandle}
       style={[styles.newPostBtn]}
       onPress={onPressCompose}
       accessibilityRole="button"
-      accessibilityLabel="New post"
+      accessibilityLabel={_(msg`New post`)}
       accessibilityHint="">
       <View style={styles.newPostBtnIconWrapper}>
         <ComposeIcon2
@@ -232,16 +261,18 @@ function ComposeBtn() {
         />
       </View>
       <Text type="button" style={styles.newPostBtnLabel}>
-        New Post
+        <Trans>New Post</Trans>
       </Text>
     </TouchableOpacity>
   )
 }
 
-export const DesktopLeftNav = observer(function DesktopLeftNav() {
-  const store = useStores()
+export function DesktopLeftNav() {
+  const {hasSession, currentAccount} = useSession()
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isDesktop, isTablet} = useWebMediaQueries()
+  const numUnread = useUnreadNotifications()
 
   return (
     <View
@@ -251,8 +282,16 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
         pal.view,
         pal.border,
       ]}>
-      {store.session.hasSession && <ProfileCard />}
+      {hasSession ? (
+        <ProfileCard />
+      ) : isDesktop ? (
+        <View style={{paddingHorizontal: 12}}>
+          <NavSignupCard />
+        </View>
+      ) : null}
+
       <BackBtn />
+
       <NavItem
         href="/"
         icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />}
@@ -263,7 +302,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
             style={pal.text}
           />
         }
-        label="Home"
+        label={_(msg`Home`)}
       />
       <NavItem
         href="/search"
@@ -281,7 +320,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
             style={pal.text}
           />
         }
-        label="Search"
+        label={_(msg`Search`)}
       />
       <NavItem
         href="/feeds"
@@ -299,105 +338,109 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
             size={isDesktop ? 24 : 28}
           />
         }
-        label="Feeds"
+        label={_(msg`Feeds`)}
       />
-      <NavItem
-        href="/notifications"
-        count={store.me.notifications.unreadCountLabel}
-        icon={
-          <BellIcon
-            strokeWidth={2}
-            size={isDesktop ? 24 : 26}
-            style={pal.text}
+
+      {hasSession && (
+        <>
+          <NavItem
+            href="/notifications"
+            count={numUnread}
+            icon={
+              <BellIcon
+                strokeWidth={2}
+                size={isDesktop ? 24 : 26}
+                style={pal.text}
+              />
+            }
+            iconFilled={
+              <BellIconSolid
+                strokeWidth={1.5}
+                size={isDesktop ? 24 : 26}
+                style={pal.text}
+              />
+            }
+            label={_(msg`Notifications`)}
           />
-        }
-        iconFilled={
-          <BellIconSolid
-            strokeWidth={1.5}
-            size={isDesktop ? 24 : 26}
-            style={pal.text}
+          <NavItem
+            href="/lists"
+            icon={
+              <ListIcon
+                style={pal.text}
+                size={isDesktop ? 26 : 30}
+                strokeWidth={2}
+              />
+            }
+            iconFilled={
+              <ListIcon
+                style={pal.text}
+                size={isDesktop ? 26 : 30}
+                strokeWidth={3}
+              />
+            }
+            label={_(msg`Lists`)}
           />
-        }
-        label="Notifications"
-      />
-      <NavItem
-        href="/lists"
-        icon={
-          <ListIcon
-            style={pal.text}
-            size={isDesktop ? 26 : 30}
-            strokeWidth={2}
+          <NavItem
+            href="/moderation"
+            icon={
+              <HandIcon
+                style={pal.text}
+                size={isDesktop ? 24 : 27}
+                strokeWidth={5.5}
+              />
+            }
+            iconFilled={
+              <FontAwesomeIcon
+                icon="hand"
+                style={pal.text as FontAwesomeIconStyle}
+                size={isDesktop ? 20 : 26}
+              />
+            }
+            label={_(msg`Moderation`)}
           />
-        }
-        iconFilled={
-          <ListIcon
-            style={pal.text}
-            size={isDesktop ? 26 : 30}
-            strokeWidth={3}
+          <NavItem
+            href={currentAccount ? makeProfileLink(currentAccount) : '/'}
+            icon={
+              <UserIcon
+                strokeWidth={1.75}
+                size={isDesktop ? 28 : 30}
+                style={pal.text}
+              />
+            }
+            iconFilled={
+              <UserIconSolid
+                strokeWidth={1.75}
+                size={isDesktop ? 28 : 30}
+                style={pal.text}
+              />
+            }
+            label="Profile"
           />
-        }
-        label="Lists"
-      />
-      <NavItem
-        href="/moderation"
-        icon={
-          <HandIcon
-            style={pal.text}
-            size={isDesktop ? 24 : 27}
-            strokeWidth={5.5}
+          <NavItem
+            href="/settings"
+            icon={
+              <CogIcon
+                strokeWidth={1.75}
+                size={isDesktop ? 28 : 32}
+                style={pal.text}
+              />
+            }
+            iconFilled={
+              <CogIconSolid
+                strokeWidth={1.5}
+                size={isDesktop ? 28 : 32}
+                style={pal.text}
+              />
+            }
+            label={_(msg`Settings`)}
           />
-        }
-        iconFilled={
-          <FontAwesomeIcon
-            icon="hand"
-            style={pal.text as FontAwesomeIconStyle}
-            size={isDesktop ? 20 : 26}
-          />
-        }
-        label="Moderation"
-      />
-      {store.session.hasSession && (
-        <NavItem
-          href={makeProfileLink(store.me)}
-          icon={
-            <UserIcon
-              strokeWidth={1.75}
-              size={isDesktop ? 28 : 30}
-              style={pal.text}
-            />
-          }
-          iconFilled={
-            <UserIconSolid
-              strokeWidth={1.75}
-              size={isDesktop ? 28 : 30}
-              style={pal.text}
-            />
-          }
-          label="Profile"
-        />
+
+          <ComposeBtn />
+        </>
       )}
-      <NavItem
-        href="/settings"
-        icon={
-          <CogIcon
-            strokeWidth={1.75}
-            size={isDesktop ? 28 : 32}
-            style={pal.text}
-          />
-        }
-        iconFilled={
-          <CogIconSolid
-            strokeWidth={1.5}
-            size={isDesktop ? 28 : 32}
-            style={pal.text}
-          />
-        }
-        label="Settings"
-      />
-      {store.session.hasSession && <ComposeBtn />}
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   leftNav: {
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 84d7d7854..9a5186549 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -9,15 +8,19 @@ import {Text} from 'view/com/util/text/Text'
 import {TextLink} from 'view/com/util/Link'
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
 import {s} from 'lib/styles'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {pluralize} from 'lib/strings/helpers'
 import {formatCount} from 'view/com/util/numeric/format'
+import {useModalControls} from '#/state/modals'
+import {useLingui} from '@lingui/react'
+import {Plural, Trans, msg, plural} from '@lingui/macro'
+import {useSession} from '#/state/session'
+import {useInviteCodesQuery} from '#/state/queries/invites'
 
-export const DesktopRightNav = observer(function DesktopRightNavImpl() {
-  const store = useStores()
+export function DesktopRightNav() {
   const pal = usePalette('default')
   const palError = usePalette('error')
+  const {_} = useLingui()
+  const {isSandbox, hasSession, currentAccount} = useSession()
 
   const {isTablet} = useWebMediaQueries()
   if (isTablet) {
@@ -26,10 +29,22 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
 
   return (
     <View style={[styles.rightNav, pal.view]}>
-      {store.session.hasSession && <DesktopSearch />}
-      {store.session.hasSession && <DesktopFeeds />}
-      <View style={styles.message}>
-        {store.session.isSandbox ? (
+      <DesktopSearch />
+
+      {hasSession && (
+        <View style={{paddingTop: 18, marginBottom: 18}}>
+          <DesktopFeeds />
+        </View>
+      )}
+
+      <View
+        style={[
+          styles.message,
+          {
+            paddingTop: hasSession ? 0 : 18,
+          },
+        ]}>
+        {isSandbox ? (
           <View style={[palError.view, styles.messageLine, s.p10]}>
             <Text type="md" style={[palError.text, s.bold]}>
               SANDBOX. Posts and accounts are not permanent.
@@ -37,23 +52,27 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
           </View>
         ) : undefined}
         <View style={[s.flexRow]}>
-          <TextLink
-            type="md"
-            style={pal.link}
-            href={FEEDBACK_FORM_URL({
-              email: store.session.currentSession?.email,
-              handle: store.session.currentSession?.handle,
-            })}
-            text="Send feedback"
-          />
-          <Text type="md" style={pal.textLight}>
-            &nbsp;&middot;&nbsp;
-          </Text>
+          {hasSession && (
+            <>
+              <TextLink
+                type="md"
+                style={pal.link}
+                href={FEEDBACK_FORM_URL({
+                  email: currentAccount!.email,
+                  handle: currentAccount!.handle,
+                })}
+                text={_(msg`Feedback`)}
+              />
+              <Text type="md" style={pal.textLight}>
+                &nbsp;&middot;&nbsp;
+              </Text>
+            </>
+          )}
           <TextLink
             type="md"
             style={pal.link}
             href="https://blueskyweb.xyz/support/privacy-policy"
-            text="Privacy"
+            text={_(msg`Privacy`)}
           />
           <Text type="md" style={pal.textLight}>
             &nbsp;&middot;&nbsp;
@@ -62,7 +81,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
             type="md"
             style={pal.link}
             href="https://blueskyweb.xyz/support/tos"
-            text="Terms"
+            text={_(msg`Terms`)}
           />
           <Text type="md" style={pal.textLight}>
             &nbsp;&middot;&nbsp;
@@ -71,52 +90,80 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
             type="md"
             style={pal.link}
             href={HELP_DESK_URL}
-            text="Help"
+            text={_(msg`Help`)}
           />
         </View>
       </View>
-      <InviteCodes />
+
+      {hasSession && <InviteCodes />}
     </View>
   )
-})
+}
 
-const InviteCodes = observer(function InviteCodesImpl() {
-  const store = useStores()
+function InviteCodes() {
   const pal = usePalette('default')
-
-  const {invitesAvailable} = store.me
+  const {openModal} = useModalControls()
+  const {data: invites} = useInviteCodesQuery()
+  const invitesAvailable = invites?.available?.length ?? 0
+  const {_} = useLingui()
 
   const onPress = React.useCallback(() => {
-    store.shell.openModal({name: 'invite-codes'})
-  }, [store])
+    openModal({name: 'invite-codes'})
+  }, [openModal])
+
+  if (!invites) {
+    return null
+  }
+
+  if (invites?.disabled) {
+    return (
+      <View style={[styles.inviteCodes, pal.border]}>
+        <FontAwesomeIcon
+          icon="ticket"
+          style={[styles.inviteCodesIcon, pal.textLight]}
+          size={16}
+        />
+        <Text type="md-medium" style={pal.textLight}>
+          <Trans>
+            Your invite codes are hidden when logged in using an App Password
+          </Trans>
+        </Text>
+      </View>
+    )
+  }
+
   return (
     <TouchableOpacity
       style={[styles.inviteCodes, pal.border]}
       onPress={onPress}
       accessibilityRole="button"
-      accessibilityLabel={
-        invitesAvailable === 1
-          ? 'Invite codes: 1 available'
-          : `Invite codes: ${invitesAvailable} available`
-      }
-      accessibilityHint="Opens list of invite codes">
+      accessibilityLabel={_(
+        plural(invitesAvailable, {
+          one: 'Invite codes: # available',
+          other: 'Invite codes: # available',
+        }),
+      )}
+      accessibilityHint={_(msg`Opens list of invite codes`)}>
       <FontAwesomeIcon
         icon="ticket"
         style={[
           styles.inviteCodesIcon,
-          store.me.invitesAvailable > 0 ? pal.link : pal.textLight,
+          invitesAvailable > 0 ? pal.link : pal.textLight,
         ]}
         size={16}
       />
       <Text
         type="md-medium"
-        style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}>
-        {formatCount(store.me.invitesAvailable)} invite{' '}
-        {pluralize(store.me.invitesAvailable, 'code')} available
+        style={invitesAvailable > 0 ? pal.link : pal.textLight}>
+        <Plural
+          value={formatCount(invitesAvailable)}
+          one="# invite code available"
+          other="# invite codes available"
+        />
       </Text>
     </TouchableOpacity>
   )
-})
+}
 
 const styles = StyleSheet.create({
   rightNav: {
@@ -142,9 +189,10 @@ const styles = StyleSheet.create({
     paddingHorizontal: 16,
     paddingVertical: 12,
     flexDirection: 'row',
-    alignItems: 'center',
   },
   inviteCodesIcon: {
+    marginTop: 2,
     marginRight: 6,
+    flexShrink: 0,
   },
 })
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index caecea4a8..f899431b6 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -1,56 +1,150 @@
 import React from 'react'
-import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native'
+import {
+  ViewStyle,
+  TextInput,
+  View,
+  StyleSheet,
+  TouchableOpacity,
+  ActivityIndicator,
+} from 'react-native'
 import {useNavigation, StackActions} from '@react-navigation/native'
-import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
+import {
+  AppBskyActorDefs,
+  moderateProfile,
+  ProfileModeration,
+} from '@atproto/api'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {s} from '#/lib/styles'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {makeProfileLink} from '#/lib/routes/links'
+import {Link} from '#/view/com/util/Link'
 import {usePalette} from 'lib/hooks/usePalette'
 import {MagnifyingGlassIcon2} from 'lib/icons'
 import {NavigationProp} from 'lib/routes/types'
-import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
+import {useModerationOpts} from '#/state/queries/preferences'
 
-export const DesktopSearch = observer(function DesktopSearch() {
-  const store = useStores()
+export function SearchResultCard({
+  profile,
+  style,
+  moderation,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  style: ViewStyle
+  moderation: ProfileModeration
+}) {
   const pal = usePalette('default')
-  const textInput = React.useRef<TextInput>(null)
-  const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
-  const [query, setQuery] = React.useState<string>('')
-  const autocompleteView = React.useMemo<UserAutocompleteModel>(
-    () => new UserAutocompleteModel(store),
-    [store],
+
+  return (
+    <Link
+      href={makeProfileLink(profile)}
+      title={profile.handle}
+      asAnchor
+      anchorNoUnderline>
+      <View
+        style={[
+          pal.border,
+          style,
+          {
+            borderTopWidth: 1,
+            flexDirection: 'row',
+            alignItems: 'center',
+            gap: 12,
+            paddingVertical: 8,
+            paddingHorizontal: 12,
+          },
+        ]}>
+        <UserAvatar
+          size={40}
+          avatar={profile.avatar}
+          moderation={moderation.avatar}
+        />
+        <View style={{flex: 1}}>
+          <Text
+            type="lg"
+            style={[s.bold, pal.text]}
+            numberOfLines={1}
+            lineHeight={1.2}>
+            {sanitizeDisplayName(
+              profile.displayName || sanitizeHandle(profile.handle),
+              moderation.profile,
+            )}
+          </Text>
+          <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+            {sanitizeHandle(profile.handle, '@')}
+          </Text>
+        </View>
+      </View>
+    </Link>
   )
+}
+
+export function DesktopSearch() {
+  const {_} = useLingui()
+  const pal = usePalette('default')
   const navigation = useNavigation<NavigationProp>()
+  const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>(
+    undefined,
+  )
+  const [isActive, setIsActive] = React.useState<boolean>(false)
+  const [isFetching, setIsFetching] = React.useState<boolean>(false)
+  const [query, setQuery] = React.useState<string>('')
+  const [searchResults, setSearchResults] = React.useState<
+    AppBskyActorDefs.ProfileViewBasic[]
+  >([])
 
-  // initial setup
-  React.useEffect(() => {
-    if (store.me.did) {
-      autocompleteView.setup()
-    }
-  }, [autocompleteView, store.me.did])
+  const moderationOpts = useModerationOpts()
+  const search = useActorAutocompleteFn()
 
-  const onChangeQuery = React.useCallback(
-    (text: string) => {
+  const onChangeText = React.useCallback(
+    async (text: string) => {
       setQuery(text)
-      if (text.length > 0 && isInputFocused) {
-        autocompleteView.setActive(true)
-        autocompleteView.setPrefix(text)
+
+      if (text.length > 0) {
+        setIsFetching(true)
+        setIsActive(true)
+
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+
+        searchDebounceTimeout.current = setTimeout(async () => {
+          const results = await search({query: text})
+
+          if (results) {
+            setSearchResults(results)
+            setIsFetching(false)
+          }
+        }, 300)
       } else {
-        autocompleteView.setActive(false)
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+        setSearchResults([])
+        setIsFetching(false)
+        setIsActive(false)
       }
     },
-    [setQuery, autocompleteView, isInputFocused],
+    [setQuery, search, setSearchResults],
   )
 
   const onPressCancelSearch = React.useCallback(() => {
     setQuery('')
-    autocompleteView.setActive(false)
-  }, [setQuery, autocompleteView])
-
+    setIsActive(false)
+    if (searchDebounceTimeout.current)
+      clearTimeout(searchDebounceTimeout.current)
+  }, [setQuery])
   const onSubmit = React.useCallback(() => {
+    setIsActive(false)
+    if (!query.length) return
+    setSearchResults([])
+    if (searchDebounceTimeout.current)
+      clearTimeout(searchDebounceTimeout.current)
     navigation.dispatch(StackActions.push('Search', {q: query}))
-    autocompleteView.setActive(false)
-  }, [query, navigation, autocompleteView])
+  }, [query, navigation, setSearchResults])
 
   return (
     <View style={[styles.container, pal.view]}>
@@ -63,19 +157,16 @@ export const DesktopSearch = observer(function DesktopSearch() {
           />
           <TextInput
             testID="searchTextInput"
-            ref={textInput}
-            placeholder="Search"
+            placeholder={_(msg`Search`)}
             placeholderTextColor={pal.colors.textLight}
             selectTextOnFocus
             returnKeyType="search"
             value={query}
             style={[pal.textLight, styles.input]}
-            onFocus={() => setIsInputFocused(true)}
-            onBlur={() => setIsInputFocused(false)}
-            onChangeText={onChangeQuery}
+            onChangeText={onChangeText}
             onSubmitEditing={onSubmit}
             accessibilityRole="search"
-            accessibilityLabel="Search"
+            accessibilityLabel={_(msg`Search`)}
             accessibilityHint=""
           />
           {query ? (
@@ -83,11 +174,11 @@ export const DesktopSearch = observer(function DesktopSearch() {
               <TouchableOpacity
                 onPress={onPressCancelSearch}
                 accessibilityRole="button"
-                accessibilityLabel="Cancel search"
+                accessibilityLabel={_(msg`Cancel search`)}
                 accessibilityHint="Exits inputting search query"
                 onAccessibilityEscape={onPressCancelSearch}>
                 <Text type="lg" style={[pal.link]}>
-                  Cancel
+                  <Trans>Cancel</Trans>
                 </Text>
               </TouchableOpacity>
             </View>
@@ -95,32 +186,42 @@ export const DesktopSearch = observer(function DesktopSearch() {
         </View>
       </View>
 
-      {query !== '' && (
+      {query !== '' && isActive && moderationOpts && (
         <View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
-          {autocompleteView.suggestions.length ? (
+          {isFetching ? (
+            <View style={{padding: 8}}>
+              <ActivityIndicator />
+            </View>
+          ) : (
             <>
-              {autocompleteView.suggestions.map((item, i) => (
-                <ProfileCard key={item.did} profile={item} noBorder={i === 0} />
-              ))}
+              {searchResults.length ? (
+                searchResults.map((item, i) => (
+                  <SearchResultCard
+                    key={item.did}
+                    profile={item}
+                    moderation={moderateProfile(item, moderationOpts)}
+                    style={i === 0 ? {borderTopWidth: 0} : {}}
+                  />
+                ))
+              ) : (
+                <View>
+                  <Text style={[pal.textLight, styles.noResults]}>
+                    <Trans>No results found for {query}</Trans>
+                  </Text>
+                </View>
+              )}
             </>
-          ) : (
-            <View>
-              <Text style={[pal.textLight, styles.noResults]}>
-                No results found for {autocompleteView.prefix}
-              </Text>
-            </View>
           )}
         </View>
       )}
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   container: {
     position: 'relative',
     width: 300,
-    paddingBottom: 18,
   },
   search: {
     paddingHorizontal: 16,
@@ -150,15 +251,11 @@ const styles = StyleSheet.create({
     paddingVertical: 7,
   },
   resultsContainer: {
-    // @ts-ignore supported by web
-    // position: 'fixed',
     marginTop: 10,
-
     flexDirection: 'column',
     width: 300,
     borderWidth: 1,
     borderRadius: 6,
-    paddingVertical: 4,
   },
   noResults: {
     textAlign: 'center',
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 703edf27a..5562af9ac 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {StatusBar} from 'expo-status-bar'
 import {
   DimensionValue,
@@ -11,7 +10,6 @@ import {
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {Drawer} from 'react-native-drawer-layout'
 import {useNavigationState} from '@react-navigation/native'
-import {useStores} from 'state/index'
 import {ModalsContainer} from 'view/com/modals/Modal'
 import {Lightbox} from 'view/com/lightbox/Lightbox'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -25,20 +23,19 @@ import {
   SafeAreaProvider,
   initialWindowMetrics,
 } from 'react-native-safe-area-context'
-import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
 import {
   useIsDrawerOpen,
   useSetDrawerOpen,
   useIsDrawerSwipeDisabled,
 } from '#/state/shell'
 import {isAndroid} from 'platform/detection'
+import {useSession} from '#/state/session'
+import {useCloseAnyActiveElement} from '#/state/util'
 
-const ShellInner = observer(function ShellInnerImpl() {
-  const store = useStores()
+function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
   const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
   const setIsDrawerOpen = useSetDrawerOpen()
-  useOTAUpdate() // this hook polls for OTA updates every few seconds
   const winDim = useWindowDimensions()
   const safeAreaInsets = useSafeAreaInsets()
   const containerPadding = React.useMemo(
@@ -55,18 +52,20 @@ const ShellInner = observer(function ShellInnerImpl() {
     [setIsDrawerOpen],
   )
   const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
+  const {hasSession} = useSession()
+  const closeAnyActiveElement = useCloseAnyActiveElement()
+
   React.useEffect(() => {
     let listener = {remove() {}}
     if (isAndroid) {
       listener = BackHandler.addEventListener('hardwareBackPress', () => {
-        setIsDrawerOpen(false)
-        return store.shell.closeAnyActiveElement()
+        return closeAnyActiveElement()
       })
     }
     return () => {
       listener.remove()
     }
-  }, [store, setIsDrawerOpen])
+  }, [closeAnyActiveElement])
 
   return (
     <>
@@ -78,28 +77,19 @@ const ShellInner = observer(function ShellInnerImpl() {
             onOpen={onOpenDrawer}
             onClose={onCloseDrawer}
             swipeEdgeWidth={winDim.width / 2}
-            swipeEnabled={
-              !canGoBack && store.session.hasSession && !isDrawerSwipeDisabled
-            }>
+            swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}>
             <TabsNavigator />
           </Drawer>
         </ErrorBoundary>
       </View>
-      <Composer
-        active={store.shell.isComposerActive}
-        winHeight={winDim.height}
-        replyTo={store.shell.composerOpts?.replyTo}
-        onPost={store.shell.composerOpts?.onPost}
-        quote={store.shell.composerOpts?.quote}
-        mention={store.shell.composerOpts?.mention}
-      />
+      <Composer winHeight={winDim.height} />
       <ModalsContainer />
       <Lightbox />
     </>
   )
-})
+}
 
-export const Shell: React.FC = observer(function ShellImpl() {
+export const Shell: React.FC = function ShellImpl() {
   const pal = usePalette('default')
   const theme = useTheme()
   return (
@@ -112,7 +102,7 @@ export const Shell: React.FC = observer(function ShellImpl() {
       </View>
     </SafeAreaProvider>
   )
-})
+}
 
 const styles = StyleSheet.create({
   outerContainer: {
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 843d0b284..38da860bd 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -1,9 +1,5 @@
 import React, {useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
 import {View, StyleSheet, TouchableOpacity} from 'react-native'
-import {useStores} from 'state/index'
-import {DesktopLeftNav} from './desktop/LeftNav'
-import {DesktopRightNav} from './desktop/RightNav'
 import {ErrorBoundary} from '../com/util/ErrorBoundary'
 import {Lightbox} from '../com/lightbox/Lightbox'
 import {ModalsContainer} from '../com/modals/Modal'
@@ -13,30 +9,29 @@ import {s, colors} from 'lib/styles'
 import {RoutesContainer, FlatNavigator} from '../../Navigation'
 import {DrawerContent} from './Drawer'
 import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
-import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {useAuxClick} from 'lib/hooks/useAuxClick'
+import {t} from '@lingui/macro'
 import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
+import {useCloseAllActiveElements} from '#/state/util'
 
-const ShellInner = observer(function ShellInnerImpl() {
-  const store = useStores()
+function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
   const setDrawerOpen = useSetDrawerOpen()
-  const {isDesktop, isMobile} = useWebMediaQueries()
+  const {isDesktop} = useWebMediaQueries()
   const navigator = useNavigation<NavigationProp>()
+  const closeAllActiveElements = useCloseAllActiveElements()
+
   useAuxClick()
 
   useEffect(() => {
-    navigator.addListener('state', () => {
-      setDrawerOpen(false)
-      store.shell.closeAnyActiveElement()
+    const unsubscribe = navigator.addListener('state', () => {
+      closeAllActiveElements()
     })
-  }, [navigator, store.shell, setDrawerOpen])
+    return unsubscribe
+  }, [navigator, closeAllActiveElements])
 
-  const showBottomBar = isMobile && !store.onboarding.isActive
-  const showSideNavs =
-    !isMobile && store.session.hasSession && !store.onboarding.isActive
   return (
     <View style={[s.hContentRegion, {overflow: 'hidden'}]}>
       <View style={s.hContentRegion}>
@@ -44,28 +39,14 @@ const ShellInner = observer(function ShellInnerImpl() {
           <FlatNavigator />
         </ErrorBoundary>
       </View>
-      {showSideNavs && (
-        <>
-          <DesktopLeftNav />
-          <DesktopRightNav />
-        </>
-      )}
-      <Composer
-        active={store.shell.isComposerActive}
-        winHeight={0}
-        replyTo={store.shell.composerOpts?.replyTo}
-        quote={store.shell.composerOpts?.quote}
-        onPost={store.shell.composerOpts?.onPost}
-        mention={store.shell.composerOpts?.mention}
-      />
-      {showBottomBar && <BottomBarWeb />}
+      <Composer winHeight={0} />
       <ModalsContainer />
       <Lightbox />
       {!isDesktop && isDrawerOpen && (
         <TouchableOpacity
           onPress={() => setDrawerOpen(false)}
           style={styles.drawerMask}
-          accessibilityLabel="Close navigation footer"
+          accessibilityLabel={t`Close navigation footer`}
           accessibilityHint="Closes bottom navigation bar">
           <View style={styles.drawerContainer}>
             <DrawerContent />
@@ -74,9 +55,9 @@ const ShellInner = observer(function ShellInnerImpl() {
       )}
     </View>
   )
-})
+}
 
-export const Shell: React.FC = observer(function ShellImpl() {
+export const Shell: React.FC = function ShellImpl() {
   const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
   return (
     <View style={[s.hContentRegion, pageBg]}>
@@ -85,7 +66,7 @@ export const Shell: React.FC = observer(function ShellImpl() {
       </RoutesContainer>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   bgLight: {