about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx4
-rw-r--r--src/App.web.tsx4
-rw-r--r--src/state/models/navigation.ts251
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/view/com/feed/Feed.tsx15
-rw-r--r--src/view/com/feed/FeedItem.tsx26
-rw-r--r--src/view/com/modals/SharePost.native.tsx89
-rw-r--r--src/view/com/notifications/Feed.tsx5
-rw-r--r--src/view/com/notifications/FeedItem.tsx26
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx24
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx18
-rw-r--r--src/view/com/post-thread/PostThread.tsx29
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx33
-rw-r--r--src/view/com/post/Post.tsx22
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx20
-rw-r--r--src/view/com/profile/ProfileFollows.tsx20
-rw-r--r--src/view/com/profile/ProfileHeader.tsx7
-rw-r--r--src/view/com/util/BottomSheetCustomBackdrop.tsx36
-rw-r--r--src/view/index.ts24
-rw-r--r--src/view/lib/navigation.ts12
-rw-r--r--src/view/routes.ts64
-rw-r--r--src/view/routes/index.tsx208
-rw-r--r--src/view/routes/types.ts47
-rw-r--r--src/view/screens/Composer.tsx43
-rw-r--r--src/view/screens/Home.tsx65
-rw-r--r--src/view/screens/Login.tsx (renamed from src/view/screens/tabroots/Login.tsx)12
-rw-r--r--src/view/screens/NotFound.tsx13
-rw-r--r--src/view/screens/Notifications.tsx65
-rw-r--r--src/view/screens/PostLikedBy.tsx26
-rw-r--r--src/view/screens/PostRepostedBy.tsx26
-rw-r--r--src/view/screens/PostThread.tsx32
-rw-r--r--src/view/screens/Profile.tsx58
-rw-r--r--src/view/screens/ProfileFollowers.tsx24
-rw-r--r--src/view/screens/ProfileFollows.tsx24
-rw-r--r--src/view/screens/Search.tsx11
-rw-r--r--src/view/screens/Signup.tsx (renamed from src/view/screens/tabroots/Signup.tsx)12
-rw-r--r--src/view/screens/stacks/Composer.tsx50
-rw-r--r--src/view/screens/stacks/PostLikedBy.tsx38
-rw-r--r--src/view/screens/stacks/PostRepostedBy.tsx41
-rw-r--r--src/view/screens/stacks/PostThread.tsx38
-rw-r--r--src/view/screens/stacks/Profile.tsx71
-rw-r--r--src/view/screens/stacks/ProfileFollowers.tsx39
-rw-r--r--src/view/screens/stacks/ProfileFollows.tsx39
-rw-r--r--src/view/screens/tabroots/Home.tsx69
-rw-r--r--src/view/screens/tabroots/Menu.tsx16
-rw-r--r--src/view/screens/tabroots/NotFound.tsx15
-rw-r--r--src/view/screens/tabroots/Notifications.tsx71
-rw-r--r--src/view/screens/tabroots/Search.tsx14
-rw-r--r--src/view/shell/desktop-web/index.tsx (renamed from src/view/shell/desktop-web/shell.tsx)0
-rw-r--r--src/view/shell/desktop-web/left-column.tsx6
-rw-r--r--src/view/shell/index.tsx12
-rw-r--r--src/view/shell/mobile/history-menu.tsx99
-rw-r--r--src/view/shell/mobile/index.tsx235
-rw-r--r--src/view/shell/mobile/tabs-selector.tsx158
54 files changed, 1374 insertions, 1038 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 4309fa3c3..a220fab3b 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -5,7 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'
 import {whenWebCrypto} from './platform/polyfills.native'
 import * as view from './view/index'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
-import * as Routes from './view/routes'
+import {MobileShell} from './view/shell/mobile'
 
 function App() {
   const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
@@ -31,7 +31,7 @@ function App() {
     <GestureHandlerRootView style={{flex: 1}}>
       <RootSiblingParent>
         <RootStoreProvider value={rootStore}>
-          <Routes.Root />
+          <MobileShell />
         </RootStoreProvider>
       </RootSiblingParent>
     </GestureHandlerRootView>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 9a6fedd5a..06da5e4e3 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,7 +1,7 @@
 import React, {useState, useEffect} from 'react'
 import * as view from './view/index'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
-import * as Routes from './view/routes'
+import {DesktopWebShell} from './view/shell/desktop-web'
 import Toast from './view/com/util/Toast'
 
 function App() {
@@ -22,7 +22,7 @@ function App() {
 
   return (
     <RootStoreProvider value={rootStore}>
-      <Routes.Root />
+      <DesktopWebShell />
       <Toast.ToastContainer />
     </RootStoreProvider>
   )
diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts
new file mode 100644
index 000000000..d5338ac05
--- /dev/null
+++ b/src/state/models/navigation.ts
@@ -0,0 +1,251 @@
+import {makeAutoObservable} from 'mobx'
+import {isObj, hasProp} from '../lib/type-guards'
+
+let __tabId = 0
+function genTabId() {
+  return ++__tabId
+}
+
+interface HistoryItem {
+  url: string
+  ts: number
+  title?: string
+}
+
+export class NavigationTabModel {
+  id = genTabId()
+  history: HistoryItem[] = [{url: '/', ts: Date.now()}]
+  index = 0
+
+  constructor() {
+    makeAutoObservable(this, {
+      serialize: false,
+      hydrate: false,
+    })
+  }
+
+  // accessors
+  // =
+
+  get current() {
+    return this.history[this.index]
+  }
+
+  get canGoBack() {
+    return this.index > 0
+  }
+
+  get canGoForward() {
+    return this.index < this.history.length - 1
+  }
+
+  getBackList(n: number) {
+    const start = Math.max(this.index - n, 0)
+    const end = Math.min(this.index, n)
+    return this.history.slice(start, end).map((item, i) => ({
+      url: item.url,
+      title: item.title,
+      index: start + i,
+    }))
+  }
+
+  get backTen() {
+    return this.getBackList(10)
+  }
+
+  getForwardList(n: number) {
+    const start = Math.min(this.index + 1, this.history.length)
+    const end = Math.min(this.index + n, this.history.length)
+    return this.history.slice(start, end).map((item, i) => ({
+      url: item.url,
+      title: item.title,
+      index: start + i,
+    }))
+  }
+
+  get forwardTen() {
+    return this.getForwardList(10)
+  }
+
+  // navigation
+  // =
+
+  navigate(url: string, title?: string) {
+    if (this.current?.url === url) {
+      this.refresh()
+    } else {
+      if (this.index < this.history.length - 1) {
+        this.history.length = this.index + 1
+      }
+      this.history.push({url, title, ts: Date.now()})
+      this.index = this.history.length - 1
+    }
+  }
+
+  refresh() {
+    this.history = [
+      ...this.history.slice(0, this.index),
+      {url: this.current.url, title: this.current.title, ts: Date.now()},
+      ...this.history.slice(this.index + 1),
+    ]
+  }
+
+  goBack() {
+    if (this.canGoBack) {
+      this.index--
+    }
+  }
+
+  goForward() {
+    if (this.canGoForward) {
+      this.index++
+    }
+  }
+
+  goToIndex(index: number) {
+    if (index >= 0 && index <= this.history.length - 1) {
+      this.index = index
+    }
+  }
+
+  setTitle(title: string) {
+    this.current.title = title
+  }
+
+  // persistence
+  // =
+
+  serialize(): unknown {
+    return {
+      history: this.history,
+      index: this.index,
+    }
+  }
+
+  hydrate(v: unknown) {
+    this.history = []
+    this.index = 0
+    if (isObj(v)) {
+      if (hasProp(v, 'history') && Array.isArray(v.history)) {
+        for (const item of v.history) {
+          if (
+            isObj(item) &&
+            hasProp(item, 'url') &&
+            typeof item.url === 'string'
+          ) {
+            let copy: HistoryItem = {
+              url: item.url,
+              ts:
+                hasProp(item, 'ts') && typeof item.ts === 'number'
+                  ? item.ts
+                  : Date.now(),
+            }
+            if (hasProp(item, 'title') && typeof item.title === 'string') {
+              copy.title = item.title
+            }
+            this.history.push(copy)
+          }
+        }
+      }
+      if (hasProp(v, 'index') && typeof v.index === 'number') {
+        this.index = v.index
+      }
+      if (this.index >= this.history.length - 1) {
+        this.index = this.history.length - 1
+      }
+    }
+  }
+}
+
+export class NavigationModel {
+  tabs: NavigationTabModel[] = [new NavigationTabModel()]
+  tabIndex = 0
+
+  constructor() {
+    makeAutoObservable(this, {
+      serialize: false,
+      hydrate: false,
+    })
+  }
+
+  // accessors
+  // =
+
+  get tab() {
+    return this.tabs[this.tabIndex]
+  }
+
+  isCurrentScreen(tabId: number, index: number) {
+    return this.tab.id === tabId && this.tab.index === index
+  }
+
+  // navigation
+  // =
+
+  navigate(url: string, title?: string) {
+    this.tab.navigate(url, title)
+  }
+
+  refresh() {
+    this.tab.refresh()
+  }
+
+  setTitle(title: string) {
+    this.tab.setTitle(title)
+  }
+
+  // tab management
+  // =
+
+  newTab(url: string, title?: string) {
+    const tab = new NavigationTabModel()
+    tab.navigate(url, title)
+    this.tabs.push(tab)
+    this.tabIndex = this.tabs.length - 1
+  }
+
+  setActiveTab(tabIndex: number) {
+    this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0)
+  }
+
+  closeTab(tabIndex: number) {
+    this.tabs = [
+      ...this.tabs.slice(0, tabIndex),
+      ...this.tabs.slice(tabIndex + 1),
+    ]
+    if (this.tabs.length === 0) {
+      this.newTab('/')
+    } else if (this.tabIndex >= this.tabs.length) {
+      this.tabIndex = this.tabs.length - 1
+    }
+  }
+
+  // persistence
+  // =
+
+  serialize(): unknown {
+    return {
+      tabs: this.tabs.map(t => t.serialize()),
+      tabIndex: this.tabIndex,
+    }
+  }
+
+  hydrate(v: unknown) {
+    this.tabs.length = 0
+    this.tabIndex = 0
+    if (isObj(v)) {
+      if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) {
+        for (const tab of v.tabs) {
+          const copy = new NavigationTabModel()
+          copy.hydrate(tab)
+          if (copy.history.length) {
+            this.tabs.push(copy)
+          }
+        }
+      }
+      if (hasProp(v, 'tabIndex') && typeof v.tabIndex === 'number') {
+        this.tabIndex = v.tabIndex
+      }
+    }
+  }
+}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index e05c86389..d1e731328 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -7,12 +7,14 @@ import {adx, AdxClient} from '@adxp/mock-api'
 import {createContext, useContext} from 'react'
 import {isObj, hasProp} from '../lib/type-guards'
 import {SessionModel} from './session'
+import {NavigationModel} from './navigation'
 import {MeModel} from './me'
 import {FeedViewModel} from './feed-view'
 import {NotificationsViewModel} from './notifications-view'
 
 export class RootStoreModel {
   session = new SessionModel()
+  nav = new NavigationModel()
   me = new MeModel(this)
   homeFeed = new FeedViewModel(this, {})
   notesFeed = new NotificationsViewModel(this, {})
@@ -35,6 +37,7 @@ export class RootStoreModel {
   serialize(): unknown {
     return {
       session: this.session.serialize(),
+      nav: this.nav.serialize(),
     }
   }
 
@@ -43,6 +46,9 @@ export class RootStoreModel {
       if (hasProp(v, 'session')) {
         this.session.hydrate(v.session)
       }
+      if (hasProp(v, 'nav')) {
+        this.nav.hydrate(v.nav)
+      }
     }
   }
 }
diff --git a/src/view/com/feed/Feed.tsx b/src/view/com/feed/Feed.tsx
index 6787b51ae..7c7fea58a 100644
--- a/src/view/com/feed/Feed.tsx
+++ b/src/view/com/feed/Feed.tsx
@@ -1,18 +1,11 @@
 import React, {useRef} from 'react'
 import {observer} from 'mobx-react-lite'
 import {Text, View, FlatList} from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view'
 import {FeedItem} from './FeedItem'
 import {ShareModal} from '../modals/SharePost'
 
-export const Feed = observer(function Feed({
-  feed,
-  onNavigateContent,
-}: {
-  feed: FeedViewModel
-  onNavigateContent: OnNavigateContent
-}) {
+export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
   const shareSheetRef = useRef<{open: (_uri: string) => void}>()
 
   const onPressShare = (uri: string) => {
@@ -23,11 +16,7 @@ export const Feed = observer(function Feed({
   //   renderItem function renders components that follow React performance best practices
   //   like PureComponent, shouldComponentUpdate, etc
   const renderItem = ({item}: {item: FeedViewItemModel}) => (
-    <FeedItem
-      item={item}
-      onNavigateContent={onNavigateContent}
-      onPressShare={onPressShare}
-    />
+    <FeedItem item={item} onPressShare={onPressShare} />
   )
   const onRefresh = () => {
     feed.refresh().catch(err => console.error('Failed to refresh', err))
diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx
index e79c15326..a63fb7a2c 100644
--- a/src/view/com/feed/FeedItem.tsx
+++ b/src/view/com/feed/FeedItem.tsx
@@ -3,39 +3,31 @@ import {observer} from 'mobx-react-lite'
 import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {bsky, AdxUri} from '@adxp/mock-api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {OnNavigateContent} from '../../routes/types'
 import {FeedViewItemModel} from '../../../state/models/feed-view'
 import {s} from '../../lib/styles'
 import {ago} from '../../lib/strings'
 import {AVIS} from '../../lib/assets'
+import {useStores} from '../../../state'
 
 export const FeedItem = observer(function FeedItem({
   item,
-  onNavigateContent,
   onPressShare,
 }: {
   item: FeedViewItemModel
-  onNavigateContent: OnNavigateContent
   onPressShare: (_uri: string) => void
 }) {
+  const store = useStores()
   const record = item.record as unknown as bsky.Post.Record
 
   const onPressOuter = () => {
     const urip = new AdxUri(item.uri)
-    onNavigateContent('PostThread', {
-      name: item.author.name,
-      recordKey: urip.recordKey,
-    })
+    store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
   }
   const onPressAuthor = () => {
-    onNavigateContent('Profile', {
-      name: item.author.name,
-    })
+    store.nav.navigate(`/profile/${item.author.name}`)
   }
   const onPressReply = () => {
-    onNavigateContent('Composer', {
-      replyTo: item.uri,
-    })
+    store.nav.navigate('/composer')
   }
   const onPressToggleRepost = () => {
     item
@@ -137,8 +129,11 @@ export const FeedItem = observer(function FeedItem({
 
 const styles = StyleSheet.create({
   outer: {
-    borderTopWidth: 1,
-    borderTopColor: '#e8e8e8',
+    // borderWidth: 1,
+    // borderColor: '#e8e8e8',
+    borderRadius: 10,
+    margin: 2,
+    marginBottom: 0,
     backgroundColor: '#fff',
     padding: 10,
   },
@@ -175,6 +170,7 @@ const styles = StyleSheet.create({
   },
   postText: {
     paddingBottom: 5,
+    fontFamily: 'Helvetica Neue',
   },
   ctrls: {
     flexDirection: 'row',
diff --git a/src/view/com/modals/SharePost.native.tsx b/src/view/com/modals/SharePost.native.tsx
index 0e99bd4d1..6fc1d1adf 100644
--- a/src/view/com/modals/SharePost.native.tsx
+++ b/src/view/com/modals/SharePost.native.tsx
@@ -1,27 +1,10 @@
-import React, {
-  forwardRef,
-  useState,
-  useMemo,
-  useImperativeHandle,
-  useRef,
-} from 'react'
-import {
-  Button,
-  StyleSheet,
-  Text,
-  TouchableOpacity,
-  TouchableWithoutFeedback,
-  View,
-} from 'react-native'
-import BottomSheet, {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
-import Animated, {
-  Extrapolate,
-  interpolate,
-  useAnimatedStyle,
-} from 'react-native-reanimated'
+import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react'
+import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
+import BottomSheet from '@gorhom/bottom-sheet'
 import Toast from '../util/Toast'
 import Clipboard from '@react-native-clipboard/clipboard'
 import {s} from '../../lib/styles'
+import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
 
 export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
   const [isOpen, setIsOpen] = useState<boolean>(false)
@@ -33,14 +16,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
       console.log('sharing', uri)
       setUri(uri)
       setIsOpen(true)
+      bottomSheetRef.current?.expand()
     },
   }))
 
   const onPressCopy = () => {
     Clipboard.setString(uri)
     console.log('showing')
-    console.log(Toast)
-    console.log(Toast.show)
     Toast.show('Link copied', {
       position: Toast.positions.TOP,
     })
@@ -55,50 +37,25 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
     bottomSheetRef.current?.close()
   }
 
-  const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
-    // animated variables
-    const opacity = useAnimatedStyle(() => ({
-      opacity: interpolate(
-        animatedIndex.value, // current snap index
-        [-1, 0], // input range
-        [0, 0.5], // output range
-        Extrapolate.CLAMP,
-      ),
-    }))
-
-    const containerStyle = useMemo(
-      () => [style, {backgroundColor: '#000'}, opacity],
-      [style, opacity],
-    )
-
-    return (
-      <TouchableWithoutFeedback onPress={onClose}>
-        <Animated.View style={containerStyle} />
-      </TouchableWithoutFeedback>
-    )
-  }
   return (
-    <>
-      {isOpen && (
-        <BottomSheet
-          ref={bottomSheetRef}
-          snapPoints={['50%']}
-          enablePanDownToClose
-          backdropComponent={CustomBackdrop}
-          onChange={onShareBottomSheetChange}>
-          <View>
-            <Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
-            <Text style={[s.textCenter, s.mb10]}>{uri}</Text>
-            <Button title="Copy to clipboard" onPress={onPressCopy} />
-            <View style={s.p10}>
-              <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
-                <Text style={s.textCenter}>Close</Text>
-              </TouchableOpacity>
-            </View>
-          </View>
-        </BottomSheet>
-      )}
-    </>
+    <BottomSheet
+      ref={bottomSheetRef}
+      index={-1}
+      snapPoints={['50%']}
+      enablePanDownToClose
+      backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined}
+      onChange={onShareBottomSheetChange}>
+      <View>
+        <Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
+        <Text style={[s.textCenter, s.mb10]}>{uri}</Text>
+        <Button title="Copy to clipboard" onPress={onPressCopy} />
+        <View style={s.p10}>
+          <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
+            <Text style={s.textCenter}>Close</Text>
+          </TouchableOpacity>
+        </View>
+      </View>
+    </BottomSheet>
   )
 })
 
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 7c95003c7..493412e7b 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -1,7 +1,6 @@
 import React from 'react'
 import {observer} from 'mobx-react-lite'
 import {Text, View, FlatList} from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   NotificationsViewModel,
   NotificationsViewItemModel,
@@ -10,17 +9,15 @@ import {FeedItem} from './FeedItem'
 
 export const Feed = observer(function Feed({
   view,
-  onNavigateContent,
 }: {
   view: NotificationsViewModel
-  onNavigateContent: OnNavigateContent
 }) {
   // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
   //   VirtualizedList: You have a large list that is slow to update - make sure your
   //   renderItem function renders components that follow React performance best practices
   //   like PureComponent, shouldComponentUpdate, etc
   const renderItem = ({item}: {item: NotificationsViewItemModel}) => (
-    <FeedItem item={item} onNavigateContent={onNavigateContent} />
+    <FeedItem item={item} />
   )
   const onRefresh = () => {
     view.refresh().catch(err => console.error('Failed to refresh', err))
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 1e0e47811..00bf6f48a 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -3,44 +3,34 @@ import {observer} from 'mobx-react-lite'
 import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {AdxUri} from '@adxp/mock-api'
 import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
-import {OnNavigateContent} from '../../routes/types'
 import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
 import {s} from '../../lib/styles'
 import {ago} from '../../lib/strings'
 import {AVIS} from '../../lib/assets'
 import {PostText} from '../post/PostText'
 import {Post} from '../post/Post'
+import {useStores} from '../../../state'
 
 export const FeedItem = observer(function FeedItem({
   item,
-  onNavigateContent,
 }: {
   item: NotificationsViewItemModel
-  onNavigateContent: OnNavigateContent
 }) {
+  const store = useStores()
+
   const onPressOuter = () => {
     if (item.isLike || item.isRepost) {
       const urip = new AdxUri(item.subjectUri)
-      onNavigateContent('PostThread', {
-        name: urip.host,
-        recordKey: urip.recordKey,
-      })
+      store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
     } else if (item.isFollow) {
-      onNavigateContent('Profile', {
-        name: item.author.name,
-      })
+      store.nav.navigate(`/profile/${item.author.name}`)
     } else if (item.isReply) {
       const urip = new AdxUri(item.uri)
-      onNavigateContent('PostThread', {
-        name: urip.host,
-        recordKey: urip.recordKey,
-      })
+      store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`)
     }
   }
   const onPressAuthor = () => {
-    onNavigateContent('Profile', {
-      name: item.author.name,
-    })
+    store.nav.navigate(`/profile/${item.author.name}`)
   }
 
   let action = ''
@@ -92,7 +82,7 @@ export const FeedItem = observer(function FeedItem({
       </View>
       {item.isReply ? (
         <View style={s.pt5}>
-          <Post uri={item.uri} onNavigateContent={onNavigateContent} />
+          <Post uri={item.uri} />
         </View>
       ) : (
         <></>
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 678e069f6..9b5810b3b 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -9,7 +9,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   LikedByViewModel,
   LikedByViewItemModel,
@@ -18,13 +17,7 @@ import {useStores} from '../../../state'
 import {s} from '../../lib/styles'
 import {AVIS} from '../../lib/assets'
 
-export const PostLikedBy = observer(function PostLikedBy({
-  uri,
-  onNavigateContent,
-}: {
-  uri: string
-  onNavigateContent: OnNavigateContent
-}) {
+export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
   const store = useStores()
   const [view, setView] = useState<LikedByViewModel | undefined>()
 
@@ -66,7 +59,7 @@ export const PostLikedBy = observer(function PostLikedBy({
   // loaded
   // =
   const renderItem = ({item}: {item: LikedByViewItemModel}) => (
-    <LikedByItem item={item} onNavigateContent={onNavigateContent} />
+    <LikedByItem item={item} />
   )
   return (
     <View>
@@ -79,17 +72,10 @@ export const PostLikedBy = observer(function PostLikedBy({
   )
 })
 
-const LikedByItem = ({
-  item,
-  onNavigateContent,
-}: {
-  item: LikedByViewItemModel
-  onNavigateContent: OnNavigateContent
-}) => {
+const LikedByItem = ({item}: {item: LikedByViewItemModel}) => {
+  const store = useStores()
   const onPressOuter = () => {
-    onNavigateContent('Profile', {
-      name: item.name,
-    })
+    store.nav.navigate(`/profile/${item.name}`)
   }
   return (
     <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 98c24ef86..967e03940 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -9,7 +9,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   RepostedByViewModel,
   RepostedByViewItemModel,
@@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
 
 export const PostRepostedBy = observer(function PostRepostedBy({
   uri,
-  onNavigateContent,
 }: {
   uri: string
-  onNavigateContent: OnNavigateContent
 }) {
   const store = useStores()
   const [view, setView] = useState<RepostedByViewModel | undefined>()
@@ -68,7 +65,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
   // loaded
   // =
   const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
-    <RepostedByItem item={item} onNavigateContent={onNavigateContent} />
+    <RepostedByItem item={item} />
   )
   return (
     <View>
@@ -81,17 +78,10 @@ export const PostRepostedBy = observer(function PostRepostedBy({
   )
 })
 
-const RepostedByItem = ({
-  item,
-  onNavigateContent,
-}: {
-  item: RepostedByViewItemModel
-  onNavigateContent: OnNavigateContent
-}) => {
+const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => {
+  const store = useStores()
   const onPressOuter = () => {
-    onNavigateContent('Profile', {
-      name: item.name,
-    })
+    store.nav.navigate(`/profile/${item.name}`)
   }
   return (
     <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 6191875c7..f7044b741 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,8 +1,6 @@
 import React, {useState, useEffect, useRef} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, FlatList, Text, View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   PostThreadViewModel,
   PostThreadViewPostModel,
@@ -14,13 +12,7 @@ import {s} from '../../lib/styles'
 
 const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates
 
-export const PostThread = observer(function PostThread({
-  uri,
-  onNavigateContent,
-}: {
-  uri: string
-  onNavigateContent: OnNavigateContent
-}) {
+export const PostThread = observer(function PostThread({uri}: {uri: string}) {
   const store = useStores()
   const [view, setView] = useState<PostThreadViewModel | undefined>()
   const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
@@ -37,12 +29,13 @@ export const PostThread = observer(function PostThread({
     newView.setup().catch(err => console.error('Failed to fetch thread', err))
   }, [uri, view?.params.uri, store])
 
-  useFocusEffect(() => {
-    if (Date.now() - lastUpdate > UPDATE_DELAY) {
-      view?.update()
-      setLastUpdate(Date.now())
-    }
-  })
+  // TODO
+  // useFocusEffect(() => {
+  //   if (Date.now() - lastUpdate > UPDATE_DELAY) {
+  //     view?.update()
+  //     setLastUpdate(Date.now())
+  //   }
+  // })
 
   const onPressShare = (uri: string) => {
     shareSheetRef.current?.open(uri)
@@ -79,11 +72,7 @@ export const PostThread = observer(function PostThread({
   // =
   const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
   const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
-    <PostThreadItem
-      item={item}
-      onNavigateContent={onNavigateContent}
-      onPressShare={onPressShare}
-    />
+    <PostThreadItem item={item} onPressShare={onPressShare} />
   )
   return (
     <View style={s.h100pct}>
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 981aab092..5430c8ef5 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -3,11 +3,11 @@ import {observer} from 'mobx-react-lite'
 import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
 import {bsky, AdxUri} from '@adxp/mock-api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {OnNavigateContent} from '../../routes/types'
 import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
 import {s} from '../../lib/styles'
 import {ago, pluralize} from '../../lib/strings'
 import {AVIS} from '../../lib/assets'
+import {useStores} from '../../../state'
 
 function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
   const arr: T[] = []
@@ -19,46 +19,36 @@ function iter<T>(n: number, fn: (_i: number) => T): Array<T> {
 
 export const PostThreadItem = observer(function PostThreadItem({
   item,
-  onNavigateContent,
   onPressShare,
 }: {
   item: PostThreadViewPostModel
-  onNavigateContent: OnNavigateContent
   onPressShare: (_uri: string) => void
 }) {
+  const store = useStores()
   const record = item.record as unknown as bsky.Post.Record
   const hasEngagement = item.likeCount || item.repostCount
 
   const onPressOuter = () => {
     const urip = new AdxUri(item.uri)
-    onNavigateContent('PostThread', {
-      name: item.author.name,
-      recordKey: urip.recordKey,
-    })
+    store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
   }
   const onPressAuthor = () => {
-    onNavigateContent('Profile', {
-      name: item.author.name,
-    })
+    store.nav.navigate(`/profile/${item.author.name}`)
   }
   const onPressLikes = () => {
     const urip = new AdxUri(item.uri)
-    onNavigateContent('PostLikedBy', {
-      name: item.author.name,
-      recordKey: urip.recordKey,
-    })
+    store.nav.navigate(
+      `/profile/${item.author.name}/post/${urip.recordKey}/liked-by`,
+    )
   }
   const onPressReposts = () => {
     const urip = new AdxUri(item.uri)
-    onNavigateContent('PostRepostedBy', {
-      name: item.author.name,
-      recordKey: urip.recordKey,
-    })
+    store.nav.navigate(
+      `/profile/${item.author.name}/post/${urip.recordKey}/reposted-by`,
+    )
   }
   const onPressReply = () => {
-    onNavigateContent('Composer', {
-      replyTo: item.uri,
-    })
+    store.nav.navigate(`/composer?replyTo=${item.uri}`)
   }
   const onPressToggleRepost = () => {
     item
@@ -227,6 +217,7 @@ const styles = StyleSheet.create({
   },
   postText: {
     paddingBottom: 5,
+    fontFamily: 'Helvetica Neue',
   },
   expandedInfo: {
     flexDirection: 'row',
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 3cfb6a1a1..3369db518 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -10,20 +10,13 @@ import {
   View,
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {OnNavigateContent} from '../../routes/types'
 import {PostThreadViewModel} from '../../../state/models/post-thread-view'
 import {useStores} from '../../../state'
 import {s} from '../../lib/styles'
 import {ago} from '../../lib/strings'
 import {AVIS} from '../../lib/assets'
 
-export const Post = observer(function Post({
-  uri,
-  onNavigateContent,
-}: {
-  uri: string
-  onNavigateContent: OnNavigateContent
-}) {
+export const Post = observer(function Post({uri}: {uri: string}) {
   const store = useStores()
   const [view, setView] = useState<PostThreadViewModel | undefined>()
 
@@ -63,20 +56,13 @@ export const Post = observer(function Post({
 
   const onPressOuter = () => {
     const urip = new AdxUri(item.uri)
-    onNavigateContent('PostThread', {
-      name: item.author.name,
-      recordKey: urip.recordKey,
-    })
+    store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`)
   }
   const onPressAuthor = () => {
-    onNavigateContent('Profile', {
-      name: item.author.name,
-    })
+    store.nav.navigate(`/profile/${item.author.name}`)
   }
   const onPressReply = () => {
-    onNavigateContent('Composer', {
-      replyTo: item.uri,
-    })
+    store.nav.navigate(`/composer?replyTo=${item.uri}`)
   }
   const onPressToggleRepost = () => {
     item
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 06cc0c14d..33d0c8d55 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -9,7 +9,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   UserFollowersViewModel,
   FollowerItem,
@@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
 
 export const ProfileFollowers = observer(function ProfileFollowers({
   name,
-  onNavigateContent,
 }: {
   name: string
-  onNavigateContent: OnNavigateContent
 }) {
   const store = useStores()
   const [view, setView] = useState<UserFollowersViewModel | undefined>()
@@ -67,9 +64,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
 
   // loaded
   // =
-  const renderItem = ({item}: {item: FollowerItem}) => (
-    <User item={item} onNavigateContent={onNavigateContent} />
-  )
+  const renderItem = ({item}: {item: FollowerItem}) => <User item={item} />
   return (
     <View>
       <FlatList
@@ -81,17 +76,10 @@ export const ProfileFollowers = observer(function ProfileFollowers({
   )
 })
 
-const User = ({
-  item,
-  onNavigateContent,
-}: {
-  item: FollowerItem
-  onNavigateContent: OnNavigateContent
-}) => {
+const User = ({item}: {item: FollowerItem}) => {
+  const store = useStores()
   const onPressOuter = () => {
-    onNavigateContent('Profile', {
-      name: item.name,
-    })
+    store.nav.navigate(`/profile/${item.name}`)
   }
   return (
     <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index bb5859852..62ed7f1c3 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -9,7 +9,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {
   UserFollowsViewModel,
   FollowItem,
@@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets'
 
 export const ProfileFollows = observer(function ProfileFollows({
   name,
-  onNavigateContent,
 }: {
   name: string
-  onNavigateContent: OnNavigateContent
 }) {
   const store = useStores()
   const [view, setView] = useState<UserFollowsViewModel | undefined>()
@@ -67,9 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({
 
   // loaded
   // =
-  const renderItem = ({item}: {item: FollowItem}) => (
-    <User item={item} onNavigateContent={onNavigateContent} />
-  )
+  const renderItem = ({item}: {item: FollowItem}) => <User item={item} />
   return (
     <View>
       <FlatList
@@ -81,17 +76,10 @@ export const ProfileFollows = observer(function ProfileFollows({
   )
 })
 
-const User = ({
-  item,
-  onNavigateContent,
-}: {
-  item: FollowItem
-  onNavigateContent: OnNavigateContent
-}) => {
+const User = ({item}: {item: FollowItem}) => {
+  const store = useStores()
   const onPressOuter = () => {
-    onNavigateContent('Profile', {
-      name: item.name,
-    })
+    store.nav.navigate(`/profile/${item.name}`)
   }
   return (
     <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 6a6d04140..0769a0077 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -9,7 +9,6 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {OnNavigateContent} from '../../routes/types'
 import {ProfileViewModel} from '../../../state/models/profile-view'
 import {useStores} from '../../../state'
 import {pluralize} from '../../lib/strings'
@@ -19,10 +18,8 @@ import Toast from '../util/Toast'
 
 export const ProfileHeader = observer(function ProfileHeader({
   user,
-  onNavigateContent,
 }: {
   user: string
-  onNavigateContent: OnNavigateContent
 }) {
   const store = useStores()
   const [view, setView] = useState<ProfileViewModel | undefined>()
@@ -55,10 +52,10 @@ export const ProfileHeader = observer(function ProfileHeader({
     )
   }
   const onPressFollowers = () => {
-    onNavigateContent('ProfileFollowers', {name: user})
+    store.nav.navigate(`/profile/${user}/followers`)
   }
   const onPressFollows = () => {
-    onNavigateContent('ProfileFollows', {name: user})
+    store.nav.navigate(`/profile/${user}/follows`)
   }
 
   // loading
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
new file mode 100644
index 000000000..e175b33a5
--- /dev/null
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -0,0 +1,36 @@
+import React, {useMemo} from 'react'
+import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native'
+import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
+import Animated, {
+  Extrapolate,
+  interpolate,
+  useAnimatedStyle,
+} from 'react-native-reanimated'
+
+export function createCustomBackdrop(
+  onClose?: ((event: GestureResponderEvent) => void) | undefined,
+): React.FC<BottomSheetBackdropProps> {
+  const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
+    // animated variables
+    const opacity = useAnimatedStyle(() => ({
+      opacity: interpolate(
+        animatedIndex.value, // current snap index
+        [-1, 0], // input range
+        [0, 0.5], // output range
+        Extrapolate.CLAMP,
+      ),
+    }))
+
+    const containerStyle = useMemo(
+      () => [style, {backgroundColor: '#000'}, opacity],
+      [style, opacity],
+    )
+
+    return (
+      <TouchableWithoutFeedback onPress={onClose}>
+        <Animated.View style={containerStyle} />
+      </TouchableWithoutFeedback>
+    )
+  }
+  return CustomBackdrop
+}
diff --git a/src/view/index.ts b/src/view/index.ts
index 026bea123..89db506d0 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -1,33 +1,55 @@
 import {library} from '@fortawesome/fontawesome-svg-core'
 
+import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
+import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
 import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
 import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
 import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
+import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
+import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark'
+import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
 import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
+import {faClone} from '@fortawesome/free-regular-svg-icons/faClone'
 import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
+import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
 import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
 import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
 import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
 import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
+import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
+import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
 import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
 import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
 import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
+import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
+import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
 import {faX} from '@fortawesome/free-solid-svg-icons/faX'
 
 export function setup() {
   library.add(
+    faAngleLeft,
+    faAngleRight,
     faArrowLeft,
     faBars,
     faBell,
+    farBell,
+    faBookmark,
+    farBookmark,
     faCheck,
+    faClone,
     faComment,
+    faEllipsis,
     faHeart,
     fasHeart,
     faHouse,
-    faPlus,
     faMagnifyingGlass,
+    faMessage,
+    faPenNib,
+    faPlus,
     faRetweet,
     faShareFromSquare,
+    faUser,
+    faUsers,
     faX,
   )
 }
diff --git a/src/view/lib/navigation.ts b/src/view/lib/navigation.ts
new file mode 100644
index 000000000..2024918e7
--- /dev/null
+++ b/src/view/lib/navigation.ts
@@ -0,0 +1,12 @@
+import {useEffect} from 'react'
+import {useStores} from '../../state'
+
+type CB = () => void
+/**
+ * This custom effect hook will trigger on every "navigation"
+ * Use this in screens to handle any loading behaviors needed
+ */
+export function useLoadEffect(cb: CB, deps: any[] = []) {
+  const store = useStores()
+  useEffect(cb, [store.nav.tab, ...deps])
+}
diff --git a/src/view/routes.ts b/src/view/routes.ts
new file mode 100644
index 000000000..5d8776ddc
--- /dev/null
+++ b/src/view/routes.ts
@@ -0,0 +1,64 @@
+import React from 'react'
+import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {Home} from './screens/Home'
+import {Search} from './screens/Search'
+import {Notifications} from './screens/Notifications'
+import {Login} from './screens/Login'
+import {Signup} from './screens/Signup'
+import {NotFound} from './screens/NotFound'
+import {Composer} from './screens/Composer'
+import {PostThread} from './screens/PostThread'
+import {PostLikedBy} from './screens/PostLikedBy'
+import {PostRepostedBy} from './screens/PostRepostedBy'
+import {Profile} from './screens/Profile'
+import {ProfileFollowers} from './screens/ProfileFollowers'
+import {ProfileFollows} from './screens/ProfileFollows'
+
+export type ScreenParams = {
+  params: Record<string, any>
+}
+export type Route = [React.FC<ScreenParams>, IconProp, RegExp]
+export type MatchResult = {
+  Com: React.FC<ScreenParams>
+  icon: IconProp
+  params: Record<string, any>
+}
+
+const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i')
+export const routes: Route[] = [
+  [Home, 'house', r('/')],
+  [Search, 'magnifying-glass', r('/search')],
+  [Notifications, 'bell', r('/notifications')],
+  [Profile, ['far', 'user'], r('/profile/(?<name>[^/]+)')],
+  [ProfileFollowers, 'users', r('/profile/(?<name>[^/]+)/followers')],
+  [ProfileFollows, 'users', r('/profile/(?<name>[^/]+)/follows')],
+  [
+    PostThread,
+    ['far', 'message'],
+    r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)'),
+  ],
+  [
+    PostLikedBy,
+    'heart',
+    r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/liked-by'),
+  ],
+  [
+    PostRepostedBy,
+    'retweet',
+    r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/reposted-by'),
+  ],
+  [Composer, 'pen-nib', r('/compose')],
+  [Login, ['far', 'user'], r('/login')],
+  [Signup, ['far', 'user'], r('/signup')],
+]
+
+export function match(url: string): MatchResult {
+  for (const [Com, icon, pattern] of routes) {
+    const res = pattern.exec(url)
+    if (res) {
+      // TODO: query params
+      return {Com, icon, params: res.groups || {}}
+    }
+  }
+  return {Com: NotFound, icon: 'magnifying-glass', params: {}}
+}
diff --git a/src/view/routes/index.tsx b/src/view/routes/index.tsx
deleted file mode 100644
index 675edb3e8..000000000
--- a/src/view/routes/index.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import React, {useEffect} from 'react'
-import {Linking, Text} from 'react-native'
-import {
-  NavigationContainer,
-  LinkingOptions,
-  RouteProp,
-  ParamListBase,
-} from '@react-navigation/native'
-import {createNativeStackNavigator} from '@react-navigation/native-stack'
-import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
-import {observer} from 'mobx-react-lite'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import type {RootTabsParamList} from './types'
-import {useStores} from '../../state'
-import * as platform from '../../platform/detection'
-import {Home} from '../screens/tabroots/Home'
-import {Search} from '../screens/tabroots/Search'
-import {Notifications} from '../screens/tabroots/Notifications'
-import {Menu} from '../screens/tabroots/Menu'
-import {Login} from '../screens/tabroots/Login'
-import {Signup} from '../screens/tabroots/Signup'
-import {NotFound} from '../screens/tabroots/NotFound'
-import {Composer} from '../screens/stacks/Composer'
-import {PostThread} from '../screens/stacks/PostThread'
-import {PostLikedBy} from '../screens/stacks/PostLikedBy'
-import {PostRepostedBy} from '../screens/stacks/PostRepostedBy'
-import {Profile} from '../screens/stacks/Profile'
-import {ProfileFollowers} from '../screens/stacks/ProfileFollowers'
-import {ProfileFollows} from '../screens/stacks/ProfileFollows'
-
-const linking: LinkingOptions<RootTabsParamList> = {
-  prefixes: [
-    'http://localhost:3000', // local dev
-    'https://pubsq.pfrazee.com', // test server (universal links only)
-    'pubsqapp://', // custom protocol (ios)
-    'pubsq://app', // custom protocol (android)
-  ],
-  config: {
-    screens: {
-      HomeTab: '',
-      SearchTab: 'search',
-      NotificationsTab: 'notifications',
-      MenuTab: 'menu',
-      Profile: 'profile/:name',
-      ProfileFollowers: 'profile/:name/followers',
-      ProfileFollows: 'profile/:name/follows',
-      PostThread: 'profile/:name/post/:recordKey',
-      PostLikedBy: 'profile/:name/post/:recordKey/liked-by',
-      PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by',
-      Composer: 'compose',
-      Login: 'login',
-      Signup: 'signup',
-      NotFound: '*',
-    },
-  },
-}
-
-export const RootTabs = createBottomTabNavigator<RootTabsParamList>()
-export const HomeTabStack = createNativeStackNavigator()
-export const SearchTabStack = createNativeStackNavigator()
-export const NotificationsTabStack = createNativeStackNavigator()
-
-const tabBarScreenOptions = ({
-  route,
-}: {
-  route: RouteProp<ParamListBase, string>
-}) => ({
-  headerShown: false,
-  tabBarShowLabel: false,
-  tabBarIcon: (state: {focused: boolean; color: string; size: number}) => {
-    switch (route.name) {
-      case 'HomeTab':
-        return <FontAwesomeIcon icon="house" style={{color: state.color}} />
-      case 'SearchTab':
-        return (
-          <FontAwesomeIcon
-            icon="magnifying-glass"
-            style={{color: state.color}}
-          />
-        )
-      case 'NotificationsTab':
-        return <FontAwesomeIcon icon="bell" style={{color: state.color}} />
-      case 'MenuTab':
-        return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
-      default:
-        return <FontAwesomeIcon icon="bars" style={{color: state.color}} />
-    }
-  },
-})
-
-const HIDE_HEADER = {headerShown: false}
-const HIDE_TAB = {tabBarButton: () => null}
-
-function HomeStackCom() {
-  return (
-    <HomeTabStack.Navigator>
-      <HomeTabStack.Screen name="Home" component={Home} />
-      <HomeTabStack.Screen name="Composer" component={Composer} />
-      <HomeTabStack.Screen name="Profile" component={Profile} />
-      <HomeTabStack.Screen
-        name="ProfileFollowers"
-        component={ProfileFollowers}
-      />
-      <HomeTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
-      <HomeTabStack.Screen name="PostThread" component={PostThread} />
-      <HomeTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
-      <HomeTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
-    </HomeTabStack.Navigator>
-  )
-}
-
-function SearchStackCom() {
-  return (
-    <SearchTabStack.Navigator>
-      <SearchTabStack.Screen
-        name="Search"
-        component={Search}
-        options={HIDE_HEADER}
-      />
-      <SearchTabStack.Screen name="Profile" component={Profile} />
-      <SearchTabStack.Screen
-        name="ProfileFollowers"
-        component={ProfileFollowers}
-      />
-      <SearchTabStack.Screen name="ProfileFollows" component={ProfileFollows} />
-      <SearchTabStack.Screen name="PostThread" component={PostThread} />
-      <SearchTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
-      <SearchTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
-    </SearchTabStack.Navigator>
-  )
-}
-
-function NotificationsStackCom() {
-  return (
-    <NotificationsTabStack.Navigator>
-      <NotificationsTabStack.Screen
-        name="Notifications"
-        component={Notifications}
-      />
-      <NotificationsTabStack.Screen name="Profile" component={Profile} />
-      <NotificationsTabStack.Screen
-        name="ProfileFollowers"
-        component={ProfileFollowers}
-      />
-      <NotificationsTabStack.Screen
-        name="ProfileFollows"
-        component={ProfileFollows}
-      />
-      <NotificationsTabStack.Screen name="PostThread" component={PostThread} />
-      <NotificationsTabStack.Screen
-        name="PostLikedBy"
-        component={PostLikedBy}
-      />
-      <NotificationsTabStack.Screen
-        name="PostRepostedBy"
-        component={PostRepostedBy}
-      />
-    </NotificationsTabStack.Navigator>
-  )
-}
-
-export const Root = observer(() => {
-  const store = useStores()
-
-  useEffect(() => {
-    console.log('Initial link setup')
-    Linking.getInitialURL().then((url: string | null) => {
-      console.log('Initial url', url)
-    })
-    Linking.addEventListener('url', ({url}) => {
-      console.log('Deep link opened with', url)
-    })
-  }, [])
-
-  // hide the tabbar on desktop web
-  const tabBar = platform.isDesktopWeb ? () => null : undefined
-
-  return (
-    <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
-      <RootTabs.Navigator
-        initialRouteName={store.session.isAuthed ? 'HomeTab' : 'Login'}
-        screenOptions={tabBarScreenOptions}
-        tabBar={tabBar}>
-        {store.session.isAuthed ? (
-          <>
-            <RootTabs.Screen name="HomeTab" component={HomeStackCom} />
-            <RootTabs.Screen name="SearchTab" component={SearchStackCom} />
-            <RootTabs.Screen
-              name="NotificationsTab"
-              component={NotificationsStackCom}
-            />
-            <RootTabs.Screen name="MenuTab" component={Menu} />
-          </>
-        ) : (
-          <>
-            <RootTabs.Screen name="Login" component={Login} />
-            <RootTabs.Screen name="Signup" component={Signup} />
-          </>
-        )}
-        <RootTabs.Screen
-          name="NotFound"
-          component={NotFound}
-          options={HIDE_TAB}
-        />
-      </RootTabs.Navigator>
-    </NavigationContainer>
-  )
-})
diff --git a/src/view/routes/types.ts b/src/view/routes/types.ts
deleted file mode 100644
index a67f282a6..000000000
--- a/src/view/routes/types.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type {StackScreenProps} from '@react-navigation/stack'
-
-export type RootTabsParamList = {
-  HomeTab: undefined
-  SearchTab: undefined
-  NotificationsTab: undefined
-  MenuTab: undefined
-  Profile: {name: string}
-  ProfileFollowers: {name: string}
-  ProfileFollows: {name: string}
-  PostThread: {name: string; recordKey: string}
-  PostLikedBy: {name: string; recordKey: string}
-  PostRepostedBy: {name: string; recordKey: string}
-  Composer: {replyTo?: string}
-  Login: undefined
-  Signup: undefined
-  NotFound: undefined
-}
-export type RootTabsScreenProps<T extends keyof RootTabsParamList> =
-  StackScreenProps<RootTabsParamList, T>
-
-export type OnNavigateContent = (
-  screen: string,
-  params: Record<string, string>,
-) => void
-
-/*
-NOTE
-this is leftover from a nested nav implementation
-keeping it around for future reference
--prf
-
-import type {NavigatorScreenParams} from '@react-navigation/native'
-import type {CompositeScreenProps} from '@react-navigation/native'
-import type {BottomTabScreenProps} from '@react-navigation/bottom-tabs'
-
-Container: NavigatorScreenParams<PrimaryStacksParamList>
-export type PrimaryStacksParamList = {
-  Home: undefined
-  Profile: {name: string}
-}
-export type PrimaryStacksScreenProps<T extends keyof PrimaryStacksParamList> =
-  CompositeScreenProps<
-    BottomTabScreenProps<PrimaryStacksParamList, T>,
-    RootTabsScreenProps<keyof RootTabsParamList>
-  >
-*/
diff --git a/src/view/screens/Composer.tsx b/src/view/screens/Composer.tsx
new file mode 100644
index 000000000..2de84583f
--- /dev/null
+++ b/src/view/screens/Composer.tsx
@@ -0,0 +1,43 @@
+import React, {useLayoutEffect, useRef} from 'react'
+// import {Text, TouchableOpacity} from 'react-native'
+// import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Composer as ComposerComponent} from '../com/composer/Composer'
+import {ScreenParams} from '../routes'
+
+export const Composer = ({params}: ScreenParams) => {
+  const {replyTo} = params
+  const ref = useRef<{publish: () => Promise<boolean>}>()
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: replyTo ? 'Reply' : 'New Post',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="x" />
+  //       </TouchableOpacity>
+  //     ),
+  //     headerRight: () => (
+  //       <TouchableOpacity
+  //         onPress={() => {
+  //           if (!ref.current) {
+  //             return
+  //           }
+  //           ref.current.publish().then(
+  //             posted => {
+  //               if (posted) {
+  //                 navigation.goBack()
+  //               }
+  //             },
+  //             err => console.error('Failed to create post', err),
+  //           )
+  //         }}>
+  //         <Text>Post</Text>
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation, replyTo, ref])
+
+  return <ComposerComponent ref={ref} replyTo={replyTo} />
+}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
new file mode 100644
index 000000000..a94ffd2f7
--- /dev/null
+++ b/src/view/screens/Home.tsx
@@ -0,0 +1,65 @@
+import React, {useState, useEffect, useLayoutEffect} from 'react'
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Feed} from '../com/feed/Feed'
+import {useStores} from '../../state'
+import {useLoadEffect} from '../lib/navigation'
+import {AVIS} from '../lib/assets'
+import {ScreenParams} from '../routes'
+
+export function Home({params}: ScreenParams) {
+  const [hasSetup, setHasSetup] = useState<boolean>(false)
+  const store = useStores()
+  useLoadEffect(() => {
+    store.nav.setTitle('Home')
+    console.log('Fetching home feed')
+    store.homeFeed.setup().then(() => setHasSetup(true))
+  }, [store.nav, store.homeFeed])
+
+  // TODO
+  // useEffect(() => {
+  //   return navigation.addListener('focus', () => {
+  //     if (hasSetup) {
+  //       console.log('Updating home feed')
+  //       store.homeFeed.update()
+  //     }
+  //   })
+  // }, [navigation, store.homeFeed, hasSetup])
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'V I B E',
+  //     headerLeft: () => (
+  //       <TouchableOpacity
+  //         onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
+  //         <Image source={AVIS['alice.com']} style={styles.avi} />
+  //       </TouchableOpacity>
+  //     ),
+  //     headerRight: () => (
+  //       <TouchableOpacity
+  //         onPress={() => {
+  //           navigation.push('Composer', {})
+  //         }}>
+  //         <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return (
+    <View>
+      <Feed feed={store.homeFeed} />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  avi: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    resizeMode: 'cover',
+  },
+})
diff --git a/src/view/screens/tabroots/Login.tsx b/src/view/screens/Login.tsx
index a5f670bdd..0857687ab 100644
--- a/src/view/screens/tabroots/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -1,18 +1,15 @@
 import React from 'react'
 import {Text, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Shell} from '../../shell'
-// import type {RootTabsScreenProps} from '../routes/types'
 // import {useStores} from '../../state'
 
 export const Login = observer(
   (/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
     // const store = useStores()
     return (
-      <Shell>
-        <View style={{justifyContent: 'center', alignItems: 'center'}}>
-          <Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
-          {/*store.session.uiError && <Text>{store.session.uiError}</Text>}
+      <View style={{justifyContent: 'center', alignItems: 'center'}}>
+        <Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
+        {/*store.session.uiError && <Text>{store.session.uiError}</Text>}
         {!store.session.uiIsProcessing ? (
           <>
             <Button title="Login" onPress={() => store.session.login()} />
@@ -24,8 +21,7 @@ export const Login = observer(
         ) : (
           <ActivityIndicator />
         )*/}
-        </View>
-      </Shell>
+      </View>
     )
   },
 )
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
new file mode 100644
index 000000000..2483da1e6
--- /dev/null
+++ b/src/view/screens/NotFound.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import {Text, Button, View} from 'react-native'
+import {useStores} from '../../state'
+
+export const NotFound = () => {
+  const stores = useStores()
+  return (
+    <View style={{justifyContent: 'center', alignItems: 'center'}}>
+      <Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
+      <Button title="Home" onPress={() => stores.nav.navigate('/')} />
+    </View>
+  )
+}
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
new file mode 100644
index 000000000..7ebc8a7ce
--- /dev/null
+++ b/src/view/screens/Notifications.tsx
@@ -0,0 +1,65 @@
+import React, {useState, useEffect, useLayoutEffect} from 'react'
+import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Feed} from '../com/notifications/Feed'
+import {useStores} from '../../state'
+import {AVIS} from '../lib/assets'
+import {ScreenParams} from '../routes'
+import {useLoadEffect} from '../lib/navigation'
+
+export const Notifications = ({params}: ScreenParams) => {
+  const [hasSetup, setHasSetup] = useState<boolean>(false)
+  const store = useStores()
+  useLoadEffect(() => {
+    store.nav.setTitle('Notifications')
+    console.log('Fetching notifications feed')
+    store.notesFeed.setup().then(() => setHasSetup(true))
+  }, [store.notesFeed])
+
+  // TODO
+  // useEffect(() => {
+  //   return navigation.addListener('focus', () => {
+  //     if (hasSetup) {
+  //       console.log('Updating notifications feed')
+  //       store.notesFeed.update()
+  //     }
+  //   })
+  // }, [navigation, store.notesFeed, hasSetup])
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Notifications',
+  //     headerLeft: () => (
+  //       <TouchableOpacity
+  //         onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
+  //         <Image source={AVIS['alice.com']} style={styles.avi} />
+  //       </TouchableOpacity>
+  //     ),
+  //     headerRight: () => (
+  //       <TouchableOpacity
+  //         onPress={() => {
+  //           navigation.push('Composer', {})
+  //         }}>
+  //         <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return (
+    <View>
+      <Feed view={store.notesFeed} />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  avi: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    resizeMode: 'cover',
+  },
+})
diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx
new file mode 100644
index 000000000..92fae30ad
--- /dev/null
+++ b/src/view/screens/PostLikedBy.tsx
@@ -0,0 +1,26 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {makeRecordUri} from '../lib/strings'
+import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
+import {ScreenParams} from '../routes'
+
+export const PostLikedBy = ({params}: ScreenParams) => {
+  const {name, recordKey} = params
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Liked By',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="arrow-left" />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return <PostLikedByComponent uri={uri} />
+}
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
new file mode 100644
index 000000000..81014a7c7
--- /dev/null
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -0,0 +1,26 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {makeRecordUri} from '../lib/strings'
+import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
+import {ScreenParams} from '../routes'
+
+export const PostRepostedBy = ({params}: ScreenParams) => {
+  const {name, recordKey} = params
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Reposted By',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="arrow-left" />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return <PostRepostedByComponent uri={uri} />
+}
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
new file mode 100644
index 000000000..1003a40e1
--- /dev/null
+++ b/src/view/screens/PostThread.tsx
@@ -0,0 +1,32 @@
+import React, {useEffect, useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {makeRecordUri} from '../lib/strings'
+import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
+import {ScreenParams} from '../routes'
+import {useStores} from '../../state'
+import {useLoadEffect} from '../lib/navigation'
+
+export const PostThread = ({params}: ScreenParams) => {
+  const store = useStores()
+  const {name, recordKey} = params
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
+  useLoadEffect(() => {
+    store.nav.setTitle(`Post by ${name}`)
+  }, [store.nav, name])
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Thread',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="arrow-left" />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return <PostThreadComponent uri={uri} />
+}
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
new file mode 100644
index 000000000..84ff63f5a
--- /dev/null
+++ b/src/view/screens/Profile.tsx
@@ -0,0 +1,58 @@
+import React, {useState, useEffect} from 'react'
+import {View, StyleSheet} from 'react-native'
+import {FeedViewModel} from '../../state/models/feed-view'
+import {useStores} from '../../state'
+import {ProfileHeader} from '../com/profile/ProfileHeader'
+import {Feed} from '../com/feed/Feed'
+import {ScreenParams} from '../routes'
+import {useLoadEffect} from '../lib/navigation'
+
+export const Profile = ({params}: ScreenParams) => {
+  const store = useStores()
+  const [hasSetup, setHasSetup] = useState<string>('')
+  const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
+
+  useLoadEffect(() => {
+    const author = params.name
+    if (feedView?.params.author === author) {
+      return // no change needed? or trigger refresh?
+    }
+    console.log('Fetching profile feed', author)
+    const newFeedView = new FeedViewModel(store, {author})
+    setFeedView(newFeedView)
+    newFeedView
+      .setup()
+      .catch(err => console.error('Failed to fetch feed', err))
+      .then(() => {
+        setHasSetup(author)
+        store.nav.setTitle(author)
+      })
+  }, [params.name, feedView?.params.author, store])
+
+  // TODO
+  // useEffect(() => {
+  //   return navigation.addListener('focus', () => {
+  //     if (hasSetup === feedView?.params.author) {
+  //       console.log('Updating profile feed', hasSetup)
+  //       feedView?.update()
+  //     }
+  //   })
+  // }, [navigation, feedView, hasSetup])
+
+  return (
+    <View style={styles.container}>
+      <ProfileHeader user={params.name} />
+      <View style={styles.feed}>{feedView && <Feed feed={feedView} />}</View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'column',
+    height: '100%',
+  },
+  feed: {
+    flex: 1,
+  },
+})
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
new file mode 100644
index 000000000..c8e752685
--- /dev/null
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -0,0 +1,24 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
+import {ScreenParams} from '../routes'
+
+export const ProfileFollowers = ({params}: ScreenParams) => {
+  const {name} = params
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Followers',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="arrow-left" />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return <ProfileFollowersComponent name={name} />
+}
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
new file mode 100644
index 000000000..96ce60ddd
--- /dev/null
+++ b/src/view/screens/ProfileFollows.tsx
@@ -0,0 +1,24 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
+import {ScreenParams} from '../routes'
+
+export const ProfileFollows = ({params}: ScreenParams) => {
+  const {name} = params
+
+  // TODO
+  // useLayoutEffect(() => {
+  //   navigation.setOptions({
+  //     headerShown: true,
+  //     headerTitle: 'Following',
+  //     headerLeft: () => (
+  //       <TouchableOpacity onPress={() => navigation.goBack()}>
+  //         <FontAwesomeIcon icon="arrow-left" />
+  //       </TouchableOpacity>
+  //     ),
+  //   })
+  // }, [navigation])
+
+  return <ProfileFollowsComponent name={name} />
+}
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
new file mode 100644
index 000000000..aea54051e
--- /dev/null
+++ b/src/view/screens/Search.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import {Text, View} from 'react-native'
+import {ScreenParams} from '../routes'
+
+export const Search = ({params}: ScreenParams) => {
+  return (
+    <View style={{justifyContent: 'center', alignItems: 'center'}}>
+      <Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/tabroots/Signup.tsx b/src/view/screens/Signup.tsx
index dc2af2b1e..a34cd5727 100644
--- a/src/view/screens/tabroots/Signup.tsx
+++ b/src/view/screens/Signup.tsx
@@ -1,18 +1,15 @@
 import React from 'react'
 import {Text, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Shell} from '../../shell'
-// import type {RootTabsScreenProps} from '../routes/types'
 // import {useStores} from '../../state'
 
 export const Signup = observer(
   (/*{navigation}: RootTabsScreenProps<'Signup'>*/) => {
     // const store = useStores()
     return (
-      <Shell>
-        <View style={{justifyContent: 'center', alignItems: 'center'}}>
-          <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
-          {/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
+      <View style={{justifyContent: 'center', alignItems: 'center'}}>
+        <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
+        {/*store.session.uiError ?? <Text>{store.session.uiError}</Text>}
           {!store.session.uiIsProcessing ? (
             <>
               <Button
@@ -27,8 +24,7 @@ export const Signup = observer(
           ) : (
             <ActivityIndicator />
           )*/}
-        </View>
-      </Shell>
+      </View>
     )
   },
 )
diff --git a/src/view/screens/stacks/Composer.tsx b/src/view/screens/stacks/Composer.tsx
deleted file mode 100644
index e1b36567a..000000000
--- a/src/view/screens/stacks/Composer.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, {useLayoutEffect, useRef} from 'react'
-import {Text, TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {Composer as ComposerComponent} from '../../com/composer/Composer'
-
-export const Composer = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'Composer'>) => {
-  const {replyTo} = route.params
-  const ref = useRef<{publish: () => Promise<boolean>}>()
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: replyTo ? 'Reply' : 'New Post',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="x" />
-        </TouchableOpacity>
-      ),
-      headerRight: () => (
-        <TouchableOpacity
-          onPress={() => {
-            if (!ref.current) {
-              return
-            }
-            ref.current.publish().then(
-              posted => {
-                if (posted) {
-                  navigation.goBack()
-                }
-              },
-              err => console.error('Failed to create post', err),
-            )
-          }}>
-          <Text>Post</Text>
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation, replyTo, ref])
-
-  return (
-    <Shell>
-      <ComposerComponent ref={ref} replyTo={replyTo} />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/stacks/PostLikedBy.tsx b/src/view/screens/stacks/PostLikedBy.tsx
deleted file mode 100644
index f12990141..000000000
--- a/src/view/screens/stacks/PostLikedBy.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, {useLayoutEffect} from 'react'
-import {TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {makeRecordUri} from '../../lib/strings'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {PostLikedBy as PostLikedByComponent} from '../../com/post-thread/PostLikedBy'
-
-export const PostLikedBy = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'PostLikedBy'>) => {
-  const {name, recordKey} = route.params
-  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Liked By',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="arrow-left" />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <PostLikedByComponent uri={uri} onNavigateContent={onNavigateContent} />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/stacks/PostRepostedBy.tsx b/src/view/screens/stacks/PostRepostedBy.tsx
deleted file mode 100644
index 000c1a7fc..000000000
--- a/src/view/screens/stacks/PostRepostedBy.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, {useLayoutEffect} from 'react'
-import {TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {makeRecordUri} from '../../lib/strings'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {PostRepostedBy as PostRepostedByComponent} from '../../com/post-thread/PostRepostedBy'
-
-export const PostRepostedBy = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'PostRepostedBy'>) => {
-  const {name, recordKey} = route.params
-  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Reposted By',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="arrow-left" />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <PostRepostedByComponent
-        uri={uri}
-        onNavigateContent={onNavigateContent}
-      />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/stacks/PostThread.tsx b/src/view/screens/stacks/PostThread.tsx
deleted file mode 100644
index 485a2e49a..000000000
--- a/src/view/screens/stacks/PostThread.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, {useLayoutEffect} from 'react'
-import {TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {makeRecordUri} from '../../lib/strings'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {PostThread as PostThreadComponent} from '../../com/post-thread/PostThread'
-
-export const PostThread = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'PostThread'>) => {
-  const {name, recordKey} = route.params
-  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Thread',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="arrow-left" />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <PostThreadComponent uri={uri} onNavigateContent={onNavigateContent} />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/stacks/Profile.tsx b/src/view/screens/stacks/Profile.tsx
deleted file mode 100644
index d8de12436..000000000
--- a/src/view/screens/stacks/Profile.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {View, StyleSheet} from 'react-native'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {FeedViewModel} from '../../../state/models/feed-view'
-import {useStores} from '../../../state'
-import {ProfileHeader} from '../../com/profile/ProfileHeader'
-import {Feed} from '../../com/feed/Feed'
-
-export const Profile = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'Profile'>) => {
-  const store = useStores()
-  const [hasSetup, setHasSetup] = useState<string>('')
-  const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
-
-  useEffect(() => {
-    const author = route.params.name
-    if (feedView?.params.author === author) {
-      return // no change needed? or trigger refresh?
-    }
-    console.log('Fetching profile feed', author)
-    const newFeedView = new FeedViewModel(store, {author})
-    setFeedView(newFeedView)
-    newFeedView
-      .setup()
-      .catch(err => console.error('Failed to fetch feed', err))
-      .then(() => setHasSetup(author))
-  }, [route.params.name, feedView?.params.author, store])
-
-  useEffect(() => {
-    return navigation.addListener('focus', () => {
-      if (hasSetup === feedView?.params.author) {
-        console.log('Updating profile feed', hasSetup)
-        feedView?.update()
-      }
-    })
-  }, [navigation, feedView, hasSetup])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <View style={styles.container}>
-        <ProfileHeader
-          user={route.params.name}
-          onNavigateContent={onNavigateContent}
-        />
-        <View style={styles.feed}>
-          {feedView && (
-            <Feed feed={feedView} onNavigateContent={onNavigateContent} />
-          )}
-        </View>
-      </View>
-    </Shell>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    flexDirection: 'column',
-    height: '100%',
-  },
-  feed: {
-    flex: 1,
-  },
-})
diff --git a/src/view/screens/stacks/ProfileFollowers.tsx b/src/view/screens/stacks/ProfileFollowers.tsx
deleted file mode 100644
index 48fbb4e13..000000000
--- a/src/view/screens/stacks/ProfileFollowers.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, {useLayoutEffect} from 'react'
-import {TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {ProfileFollowers as ProfileFollowersComponent} from '../../com/profile/ProfileFollowers'
-
-export const ProfileFollowers = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'ProfileFollowers'>) => {
-  const {name} = route.params
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Followers',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="arrow-left" />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <ProfileFollowersComponent
-        name={name}
-        onNavigateContent={onNavigateContent}
-      />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/stacks/ProfileFollows.tsx b/src/view/screens/stacks/ProfileFollows.tsx
deleted file mode 100644
index 6fce3d798..000000000
--- a/src/view/screens/stacks/ProfileFollows.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, {useLayoutEffect} from 'react'
-import {TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Shell} from '../../shell'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {ProfileFollows as ProfileFollowsComponent} from '../../com/profile/ProfileFollows'
-
-export const ProfileFollows = ({
-  navigation,
-  route,
-}: RootTabsScreenProps<'ProfileFollows'>) => {
-  const {name} = route.params
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Following',
-      headerLeft: () => (
-        <TouchableOpacity onPress={() => navigation.goBack()}>
-          <FontAwesomeIcon icon="arrow-left" />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.push(screen, props)
-  }
-
-  return (
-    <Shell>
-      <ProfileFollowsComponent
-        name={name}
-        onNavigateContent={onNavigateContent}
-      />
-    </Shell>
-  )
-}
diff --git a/src/view/screens/tabroots/Home.tsx b/src/view/screens/tabroots/Home.tsx
deleted file mode 100644
index a9c952473..000000000
--- a/src/view/screens/tabroots/Home.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, {useState, useEffect, useLayoutEffect} from 'react'
-import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Shell} from '../../shell'
-import {Feed} from '../../com/feed/Feed'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {useStores} from '../../../state'
-import {AVIS} from '../../lib/assets'
-
-export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) {
-  const [hasSetup, setHasSetup] = useState<boolean>(false)
-  const store = useStores()
-  useEffect(() => {
-    console.log('Fetching home feed')
-    store.homeFeed.setup().then(() => setHasSetup(true))
-  }, [store.homeFeed])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.navigate(screen, props)
-  }
-
-  useEffect(() => {
-    return navigation.addListener('focus', () => {
-      if (hasSetup) {
-        console.log('Updating home feed')
-        store.homeFeed.update()
-      }
-    })
-  }, [navigation, store.homeFeed, hasSetup])
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'V I B E',
-      headerLeft: () => (
-        <TouchableOpacity
-          onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
-          <Image source={AVIS['alice.com']} style={styles.avi} />
-        </TouchableOpacity>
-      ),
-      headerRight: () => (
-        <TouchableOpacity
-          onPress={() => {
-            navigation.push('Composer', {})
-          }}>
-          <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  return (
-    <Shell>
-      <View>
-        <Feed feed={store.homeFeed} onNavigateContent={onNavigateContent} />
-      </View>
-    </Shell>
-  )
-}
-
-const styles = StyleSheet.create({
-  avi: {
-    width: 20,
-    height: 20,
-    borderRadius: 10,
-    resizeMode: 'cover',
-  },
-})
diff --git a/src/view/screens/tabroots/Menu.tsx b/src/view/screens/tabroots/Menu.tsx
deleted file mode 100644
index dca5ad33b..000000000
--- a/src/view/screens/tabroots/Menu.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import {Shell} from '../../shell'
-import {ScrollView, Text, View} from 'react-native'
-import type {RootTabsScreenProps} from '../../routes/types'
-
-export const Menu = (_props: RootTabsScreenProps<'MenuTab'>) => {
-  return (
-    <Shell>
-      <ScrollView contentInsetAdjustmentBehavior="automatic">
-        <View style={{justifyContent: 'center', alignItems: 'center'}}>
-          <Text style={{fontSize: 20, fontWeight: 'bold'}}>Menu</Text>
-        </View>
-      </ScrollView>
-    </Shell>
-  )
-}
diff --git a/src/view/screens/tabroots/NotFound.tsx b/src/view/screens/tabroots/NotFound.tsx
deleted file mode 100644
index a35808cbc..000000000
--- a/src/view/screens/tabroots/NotFound.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import {Shell} from '../../shell'
-import {Text, Button, View} from 'react-native'
-import type {RootTabsScreenProps} from '../../routes/types'
-
-export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => {
-  return (
-    <Shell>
-      <View style={{justifyContent: 'center', alignItems: 'center'}}>
-        <Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
-        <Button title="Home" onPress={() => navigation.navigate('HomeTab')} />
-      </View>
-    </Shell>
-  )
-}
diff --git a/src/view/screens/tabroots/Notifications.tsx b/src/view/screens/tabroots/Notifications.tsx
deleted file mode 100644
index ea7576799..000000000
--- a/src/view/screens/tabroots/Notifications.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, {useState, useEffect, useLayoutEffect} from 'react'
-import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Shell} from '../../shell'
-import {Feed} from '../../com/notifications/Feed'
-import type {RootTabsScreenProps} from '../../routes/types'
-import {useStores} from '../../../state'
-import {AVIS} from '../../lib/assets'
-
-export const Notifications = ({
-  navigation,
-}: RootTabsScreenProps<'NotificationsTab'>) => {
-  const [hasSetup, setHasSetup] = useState<boolean>(false)
-  const store = useStores()
-  useEffect(() => {
-    console.log('Fetching home feed')
-    store.notesFeed.setup().then(() => setHasSetup(true))
-  }, [store.notesFeed])
-
-  const onNavigateContent = (screen: string, props: Record<string, string>) => {
-    // @ts-ignore it's up to the callers to supply correct params -prf
-    navigation.navigate(screen, props)
-  }
-
-  useEffect(() => {
-    return navigation.addListener('focus', () => {
-      if (hasSetup) {
-        console.log('Updating home feed')
-        store.notesFeed.update()
-      }
-    })
-  }, [navigation, store.notesFeed, hasSetup])
-
-  useLayoutEffect(() => {
-    navigation.setOptions({
-      headerShown: true,
-      headerTitle: 'Notifications',
-      headerLeft: () => (
-        <TouchableOpacity
-          onPress={() => navigation.push('Profile', {name: 'alice.com'})}>
-          <Image source={AVIS['alice.com']} style={styles.avi} />
-        </TouchableOpacity>
-      ),
-      headerRight: () => (
-        <TouchableOpacity
-          onPress={() => {
-            navigation.push('Composer', {})
-          }}>
-          <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} />
-        </TouchableOpacity>
-      ),
-    })
-  }, [navigation])
-
-  return (
-    <Shell>
-      <View>
-        <Feed view={store.notesFeed} onNavigateContent={onNavigateContent} />
-      </View>
-    </Shell>
-  )
-}
-
-const styles = StyleSheet.create({
-  avi: {
-    width: 20,
-    height: 20,
-    borderRadius: 10,
-    resizeMode: 'cover',
-  },
-})
diff --git a/src/view/screens/tabroots/Search.tsx b/src/view/screens/tabroots/Search.tsx
deleted file mode 100644
index 044ca749c..000000000
--- a/src/view/screens/tabroots/Search.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-import {Shell} from '../../shell'
-import {Text, View} from 'react-native'
-import type {RootTabsScreenProps} from '../../routes/types'
-
-export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => {
-  return (
-    <Shell>
-      <View style={{justifyContent: 'center', alignItems: 'center'}}>
-        <Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text>
-      </View>
-    </Shell>
-  )
-}
diff --git a/src/view/shell/desktop-web/shell.tsx b/src/view/shell/desktop-web/index.tsx
index 13acbbfed..13acbbfed 100644
--- a/src/view/shell/desktop-web/shell.tsx
+++ b/src/view/shell/desktop-web/index.tsx
diff --git a/src/view/shell/desktop-web/left-column.tsx b/src/view/shell/desktop-web/left-column.tsx
index 082231ec9..fabb8bc94 100644
--- a/src/view/shell/desktop-web/left-column.tsx
+++ b/src/view/shell/desktop-web/left-column.tsx
@@ -1,13 +1,11 @@
 import React from 'react'
 import {Pressable, View, StyleSheet} from 'react-native'
-import {Link} from '@react-navigation/native'
-import {useRoute} from '@react-navigation/native'
 
 export const NavItem: React.FC<{label: string; screen: string}> = ({
   label,
   screen,
 }) => {
-  const route = useRoute()
+  const Link = <></> // TODO
   return (
     <View>
       <Pressable
@@ -18,7 +16,7 @@ export const NavItem: React.FC<{label: string; screen: string}> = ({
         <Link
           style={[
             styles.navItemLink,
-            route.name === screen && styles.navItemLinkSelected,
+            false /* TODO route.name === screen*/ && styles.navItemLinkSelected,
           ]}
           to={{screen, params: {}}}>
           {label}
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
deleted file mode 100644
index db60ed149..000000000
--- a/src/view/shell/index.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import {SafeAreaView} from 'react-native'
-import {isDesktopWeb} from '../../platform/detection'
-import {DesktopWebShell} from './desktop-web/shell'
-
-export const Shell: React.FC = ({children}) => {
-  return isDesktopWeb ? (
-    <DesktopWebShell>{children}</DesktopWebShell>
-  ) : (
-    <SafeAreaView>{children}</SafeAreaView>
-  )
-}
diff --git a/src/view/shell/mobile/history-menu.tsx b/src/view/shell/mobile/history-menu.tsx
new file mode 100644
index 000000000..b625162d4
--- /dev/null
+++ b/src/view/shell/mobile/history-menu.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import {
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  TouchableWithoutFeedback,
+  View,
+} from 'react-native'
+import RootSiblings from 'react-native-root-siblings'
+import {NavigationTabModel} from '../../../state/models/navigation'
+
+export function createBackMenu(tab: NavigationTabModel): RootSiblings {
+  const onPressItem = (index: number) => {
+    sibling.destroy()
+    tab.goToIndex(index)
+  }
+  const onOuterPress = () => sibling.destroy()
+  const sibling = new RootSiblings(
+    (
+      <>
+        <TouchableWithoutFeedback onPress={onOuterPress}>
+          <View style={styles.bg} />
+        </TouchableWithoutFeedback>
+        <View style={[styles.menu, styles.back]}>
+          {tab.backTen.map((item, i) => (
+            <TouchableOpacity
+              key={item.index}
+              style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
+              onPress={() => onPressItem(item.index)}>
+              <Text>{item.title || item.url}</Text>
+            </TouchableOpacity>
+          ))}
+        </View>
+      </>
+    ),
+  )
+  return sibling
+}
+export function createForwardMenu(tab: NavigationTabModel): RootSiblings {
+  const onPressItem = (index: number) => {
+    sibling.destroy()
+    tab.goToIndex(index)
+  }
+  const onOuterPress = () => sibling.destroy()
+  const sibling = new RootSiblings(
+    (
+      <>
+        <TouchableWithoutFeedback onPress={onOuterPress}>
+          <View style={styles.bg} />
+        </TouchableWithoutFeedback>
+        <View style={[styles.menu, styles.forward]}>
+          {tab.forwardTen.reverse().map((item, i) => (
+            <TouchableOpacity
+              key={item.index}
+              style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
+              onPress={() => onPressItem(item.index)}>
+              <Text>{item.title || item.url}</Text>
+            </TouchableOpacity>
+          ))}
+        </View>
+      </>
+    ),
+  )
+  return sibling
+}
+
+const styles = StyleSheet.create({
+  bg: {
+    position: 'absolute',
+    top: 0,
+    right: 0,
+    bottom: 0,
+    left: 0,
+    backgroundColor: '#000',
+    opacity: 0.1,
+  },
+  menu: {
+    position: 'absolute',
+    bottom: 80,
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    opacity: 1,
+  },
+  back: {
+    left: 10,
+  },
+  forward: {
+    left: 60,
+  },
+  menuItem: {
+    paddingVertical: 10,
+    paddingLeft: 15,
+    paddingRight: 30,
+  },
+  menuItemBorder: {
+    borderTopWidth: 1,
+    borderTopColor: '#ddd',
+  },
+})
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
new file mode 100644
index 000000000..7b0098c51
--- /dev/null
+++ b/src/view/shell/mobile/index.tsx
@@ -0,0 +1,235 @@
+import React, {useRef} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  GestureResponderEvent,
+  SafeAreaView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {ScreenContainer, Screen} from 'react-native-screens'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {useStores} from '../../../state'
+import {NavigationModel} from '../../../state/models/navigation'
+import {match, MatchResult} from '../../routes'
+import {TabsSelectorModal} from './tabs-selector'
+import {createBackMenu, createForwardMenu} from './history-menu'
+
+const Location = ({icon, title}: {icon: IconProp; title?: string}) => {
+  return (
+    <TouchableOpacity style={styles.location}>
+      {title ? (
+        <FontAwesomeIcon size={16} style={styles.locationIcon} icon={icon} />
+      ) : (
+        <FontAwesomeIcon
+          size={16}
+          style={styles.locationIconLight}
+          icon="magnifying-glass"
+        />
+      )}
+      <Text style={title ? styles.locationText : styles.locationTextLight}>
+        {title || 'Search'}
+      </Text>
+    </TouchableOpacity>
+  )
+}
+
+const Btn = ({
+  icon,
+  inactive,
+  onPress,
+  onLongPress,
+}: {
+  icon: IconProp
+  inactive?: boolean
+  onPress?: (event: GestureResponderEvent) => void
+  onLongPress?: (event: GestureResponderEvent) => void
+}) => {
+  if (inactive) {
+    return (
+      <View style={styles.ctrl}>
+        <FontAwesomeIcon
+          size={18}
+          style={[styles.ctrlIcon, styles.inactive]}
+          icon={icon}
+        />
+      </View>
+    )
+  }
+  return (
+    <TouchableOpacity
+      style={styles.ctrl}
+      onPress={onPress}
+      onLongPress={onLongPress}>
+      <FontAwesomeIcon size={18} style={styles.ctrlIcon} icon={icon} />
+    </TouchableOpacity>
+  )
+}
+
+export const MobileShell: React.FC = observer(() => {
+  const stores = useStores()
+  const tabSelectorRef = useRef<{open: () => void}>()
+  const screenRenderDesc = constructScreenRenderDesc(stores.nav)
+
+  const onPressBack = () => stores.nav.tab.goBack()
+  const onPressForward = () => stores.nav.tab.goForward()
+  const onPressHome = () => stores.nav.navigate('/')
+  const onPressNotifications = () => stores.nav.navigate('/notifications')
+  const onPressTabs = () => tabSelectorRef.current?.open()
+
+  const onLongPressBack = () => createBackMenu(stores.nav.tab)
+  const onLongPressForward = () => createForwardMenu(stores.nav.tab)
+
+  const onNewTab = () => stores.nav.newTab('/')
+  const onChangeTab = (tabIndex: number) => stores.nav.setActiveTab(tabIndex)
+  const onCloseTab = (tabIndex: number) => stores.nav.closeTab(tabIndex)
+
+  return (
+    <View style={styles.outerContainer}>
+      <View style={styles.topBar}>
+        <Location
+          icon={screenRenderDesc.icon}
+          title={stores.nav.tab.current.title}
+        />
+      </View>
+      <SafeAreaView style={styles.innerContainer}>
+        <ScreenContainer>
+          {screenRenderDesc.screens.map(({Com, params, key, activityState}) => (
+            <Screen
+              key={key}
+              style={{backgroundColor: '#fff'}}
+              activityState={activityState}>
+              <Com params={params} />
+            </Screen>
+          ))}
+        </ScreenContainer>
+      </SafeAreaView>
+      <View style={styles.bottomBar}>
+        <Btn
+          icon="angle-left"
+          inactive={!stores.nav.tab.canGoBack}
+          onPress={onPressBack}
+          onLongPress={onLongPressBack}
+        />
+        <Btn
+          icon="angle-right"
+          inactive={!stores.nav.tab.canGoForward}
+          onPress={onPressForward}
+          onLongPress={onLongPressForward}
+        />
+        <Btn icon="house" onPress={onPressHome} />
+        <Btn icon={['far', 'bell']} onPress={onPressNotifications} />
+        <Btn icon={['far', 'clone']} onPress={onPressTabs} />
+      </View>
+      <TabsSelectorModal
+        ref={tabSelectorRef}
+        tabs={stores.nav.tabs}
+        currentTabIndex={stores.nav.tabIndex}
+        onNewTab={onNewTab}
+        onChangeTab={onChangeTab}
+        onCloseTab={onCloseTab}
+      />
+    </View>
+  )
+})
+
+/**
+ * This method produces the information needed by the shell to
+ * render the current screens with screen-caching behaviors.
+ */
+type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2}
+function constructScreenRenderDesc(nav: NavigationModel): {
+  icon: IconProp
+  screens: ScreenRenderDesc[]
+} {
+  let icon: IconProp = 'magnifying-glass'
+  let screens: ScreenRenderDesc[] = []
+  for (const tab of nav.tabs) {
+    const tabScreens = [
+      ...tab.getBackList(5),
+      Object.assign({}, tab.current, {index: tab.index}),
+    ]
+    const parsedTabScreens = tabScreens.map(screen => {
+      const isCurrent = nav.isCurrentScreen(tab.id, screen.index)
+      const matchRes = match(screen.url)
+      if (isCurrent) {
+        icon = matchRes.icon
+      }
+      return Object.assign(matchRes, {
+        key: `t${tab.id}-s${screen.index}`,
+        activityState: isCurrent ? 2 : 0,
+      })
+    })
+    screens = screens.concat(parsedTabScreens)
+  }
+  return {
+    icon,
+    screens,
+  }
+}
+
+const styles = StyleSheet.create({
+  outerContainer: {
+    height: '100%',
+  },
+  innerContainer: {
+    flex: 1,
+  },
+  topBar: {
+    flexDirection: 'row',
+    backgroundColor: '#fff',
+    borderBottomWidth: 1,
+    borderBottomColor: '#ccc',
+    paddingLeft: 10,
+    paddingRight: 10,
+    paddingTop: 40,
+    paddingBottom: 5,
+  },
+  location: {
+    flex: 1,
+    flexDirection: 'row',
+    borderRadius: 4,
+    paddingLeft: 10,
+    paddingRight: 6,
+    paddingTop: 6,
+    paddingBottom: 6,
+    backgroundColor: '#F8F3F3',
+  },
+  locationIcon: {
+    color: '#DB00FF',
+    marginRight: 8,
+  },
+  locationIconLight: {
+    color: '#909090',
+    marginRight: 8,
+  },
+  locationText: {
+    color: '#000',
+  },
+  locationTextLight: {
+    color: '#868788',
+  },
+  bottomBar: {
+    flexDirection: 'row',
+    backgroundColor: '#fff',
+    borderTopWidth: 1,
+    borderTopColor: '#ccc',
+    paddingLeft: 5,
+    paddingRight: 15,
+    paddingBottom: 20,
+  },
+  ctrl: {
+    flex: 1,
+    paddingTop: 15,
+    paddingBottom: 15,
+  },
+  ctrlIcon: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+  },
+  inactive: {
+    color: '#888',
+  },
+})
diff --git a/src/view/shell/mobile/tabs-selector.tsx b/src/view/shell/mobile/tabs-selector.tsx
new file mode 100644
index 000000000..10651ba1f
--- /dev/null
+++ b/src/view/shell/mobile/tabs-selector.tsx
@@ -0,0 +1,158 @@
+import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react'
+import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native'
+import BottomSheet from '@gorhom/bottom-sheet'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {s} from '../../lib/styles'
+import {NavigationTabModel} from '../../../state/models/navigation'
+import {createCustomBackdrop} from '../../com/util/BottomSheetCustomBackdrop'
+import {match} from '../../routes'
+
+const TAB_HEIGHT = 38
+const TAB_SPACING = 5
+const BOTTOM_MARGIN = 70
+
+export const TabsSelectorModal = forwardRef(function TabsSelectorModal(
+  {
+    onNewTab,
+    onChangeTab,
+    onCloseTab,
+    tabs,
+    currentTabIndex,
+  }: {
+    onNewTab: () => void
+    onChangeTab: (tabIndex: number) => void
+    onCloseTab: (tabIndex: number) => void
+    tabs: NavigationTabModel[]
+    currentTabIndex: number
+  },
+  ref,
+) {
+  const [isOpen, setIsOpen] = useState<boolean>(false)
+  const [snapPoints, setSnapPoints] = useState<number[]>([100])
+  const bottomSheetRef = useRef<BottomSheet>(null)
+
+  useImperativeHandle(ref, () => ({
+    open() {
+      setIsOpen(true)
+      setSnapPoints([
+        (tabs.length + 1) * (TAB_HEIGHT + TAB_SPACING) + BOTTOM_MARGIN,
+      ])
+      bottomSheetRef.current?.expand()
+    },
+  }))
+
+  const onShareBottomSheetChange = (snapPoint: number) => {
+    if (snapPoint === -1) {
+      setIsOpen(false)
+    }
+  }
+  const onPressNewTab = () => {
+    onNewTab()
+    onClose()
+  }
+  const onPressChangeTab = (tabIndex: number) => {
+    onChangeTab(tabIndex)
+    onClose()
+  }
+  const onClose = () => {
+    setIsOpen(false)
+    bottomSheetRef.current?.close()
+  }
+  return (
+    <BottomSheet
+      ref={bottomSheetRef}
+      index={-1}
+      snapPoints={snapPoints}
+      enablePanDownToClose
+      backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined}
+      onChange={onShareBottomSheetChange}>
+      <View style={s.p10}>
+        {tabs.map((tab, tabIndex) => {
+          const {icon} = match(tab.current.url)
+          const isActive = tabIndex === currentTabIndex
+          return (
+            <View
+              key={tabIndex}
+              style={[styles.tab, styles.existing, isActive && styles.active]}>
+              <TouchableWithoutFeedback
+                onPress={() => onPressChangeTab(tabIndex)}>
+                <View style={styles.tabIcon}>
+                  <FontAwesomeIcon size={16} icon={icon} />
+                </View>
+              </TouchableWithoutFeedback>
+              <TouchableWithoutFeedback
+                onPress={() => onPressChangeTab(tabIndex)}>
+                <Text
+                  style={[styles.tabText, isActive && styles.tabTextActive]}>
+                  {tab.current.title || tab.current.url}
+                </Text>
+              </TouchableWithoutFeedback>
+              <TouchableWithoutFeedback onPress={() => onCloseTab(tabIndex)}>
+                <View style={styles.tabClose}>
+                  <FontAwesomeIcon
+                    size={16}
+                    icon="x"
+                    style={styles.tabCloseIcon}
+                  />
+                </View>
+              </TouchableWithoutFeedback>
+            </View>
+          )
+        })}
+        <TouchableWithoutFeedback onPress={onPressNewTab}>
+          <View style={[styles.tab, styles.create]}>
+            <View style={styles.tabIcon}>
+              <FontAwesomeIcon size={16} icon="plus" />
+            </View>
+            <Text style={styles.tabText}>New tab</Text>
+          </View>
+        </TouchableWithoutFeedback>
+      </View>
+    </BottomSheet>
+  )
+})
+
+const styles = StyleSheet.create({
+  tab: {
+    flexDirection: 'row',
+    width: '100%',
+    borderRadius: 4,
+    height: TAB_HEIGHT,
+    marginBottom: TAB_SPACING,
+  },
+  existing: {
+    borderColor: '#000',
+    borderWidth: 1,
+  },
+  create: {
+    backgroundColor: '#F8F3F3',
+  },
+  active: {
+    backgroundColor: '#faf0f0',
+    borderColor: '#f00',
+    borderWidth: 1,
+  },
+  tabIcon: {
+    paddingTop: 10,
+    paddingBottom: 10,
+    paddingLeft: 15,
+    paddingRight: 10,
+  },
+  tabText: {
+    flex: 1,
+    paddingTop: 10,
+    paddingBottom: 10,
+  },
+  tabTextActive: {
+    fontWeight: 'bold',
+  },
+  tabClose: {
+    paddingTop: 10,
+    paddingBottom: 10,
+    paddingLeft: 10,
+    paddingRight: 15,
+  },
+  tabCloseIcon: {
+    color: '#655',
+  },
+})