about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/Contacts.tsx88
-rw-r--r--src/view/screens/Debug.tsx6
-rw-r--r--src/view/screens/Home.tsx68
-rw-r--r--src/view/screens/Log.tsx22
-rw-r--r--src/view/screens/NotFound.tsx48
-rw-r--r--src/view/screens/Notifications.tsx40
-rw-r--r--src/view/screens/PostDownvotedBy.tsx27
-rw-r--r--src/view/screens/PostRepostedBy.tsx19
-rw-r--r--src/view/screens/PostThread.tsx78
-rw-r--r--src/view/screens/PostUpvotedBy.tsx20
-rw-r--r--src/view/screens/Profile.tsx57
-rw-r--r--src/view/screens/ProfileFollowers.tsx19
-rw-r--r--src/view/screens/ProfileFollows.tsx19
-rw-r--r--src/view/screens/Search.tsx53
-rw-r--r--src/view/screens/Search.web.tsx28
-rw-r--r--src/view/screens/Settings.tsx54
16 files changed, 274 insertions, 372 deletions
diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx
deleted file mode 100644
index 21943a10a..000000000
--- a/src/view/screens/Contacts.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, {useEffect, useState, useRef} from 'react'
-import {StyleSheet, TextInput, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
-import {Selector} from '../com/util/Selector'
-import {Text} from '../com/util/text/Text'
-import {colors} from 'lib/styles'
-import {ScreenParams} from '../routes'
-import {useStores} from 'state/index'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
-
-export const Contacts = ({navIdx, visible}: ScreenParams) => {
-  const store = useStores()
-  const selectorInterp = useAnimatedValue(0)
-
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, 'Contacts')
-    }
-  }, [store, visible, navIdx])
-
-  const [searchText, onChangeSearchText] = useState('')
-  const inputRef = useRef<TextInput | null>(null)
-
-  return (
-    <View>
-      <View style={styles.section}>
-        <Text testID="contactsTitle" style={styles.title}>
-          Contacts
-        </Text>
-      </View>
-      <View style={styles.section}>
-        <View style={styles.searchContainer}>
-          <FontAwesomeIcon
-            icon="magnifying-glass"
-            size={16}
-            style={styles.searchIcon}
-          />
-          <TextInput
-            testID="contactsTextInput"
-            ref={inputRef}
-            value={searchText}
-            style={styles.searchInput}
-            placeholder="Search"
-            placeholderTextColor={colors.gray4}
-            onChangeText={onChangeSearchText}
-          />
-        </View>
-      </View>
-      <Selector
-        items={['All', 'Following', 'Scenes']}
-        selectedIndex={0}
-        panX={selectorInterp}
-      />
-      {!!store.me.handle && <ProfileFollowsComponent name={store.me.handle} />}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  section: {
-    backgroundColor: colors.white,
-  },
-  title: {
-    fontSize: 30,
-    fontWeight: 'bold',
-    paddingHorizontal: 12,
-    paddingVertical: 6,
-  },
-
-  searchContainer: {
-    flexDirection: 'row',
-    backgroundColor: colors.gray1,
-    paddingHorizontal: 8,
-    paddingVertical: 8,
-    marginHorizontal: 10,
-    marginBottom: 6,
-    borderRadius: 4,
-  },
-  searchIcon: {
-    color: colors.gray5,
-    marginRight: 8,
-  },
-  searchInput: {
-    flex: 1,
-    color: colors.black,
-  },
-})
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index eb5ffe20f..852025324 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {ScrollView, View} from 'react-native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -20,7 +21,10 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage'
 
 const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs']
 
-export const Debug = () => {
+export const DebugScreen = ({}: NativeStackScreenProps<
+  CommonNavigatorParams,
+  'Debug'
+>) => {
   const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>(
     'light',
   )
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 42759f7ff..505b1fcfe 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,14 +1,15 @@
 import React from 'react'
 import {FlatList, View} from 'react-native'
+import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
+import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/posts/Feed'
 import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
 import {WelcomeBanner} from '../com/util/WelcomeBanner'
 import {FAB} from '../com/util/FAB'
 import {useStores} from 'state/index'
-import {ScreenParams} from '../routes'
 import {s} from 'lib/styles'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useAnalytics} from 'lib/analytics'
@@ -16,19 +17,20 @@ import {ComposeIcon2} from 'lib/icons'
 
 const HEADER_HEIGHT = 42
 
-export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
+type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
+export const HomeScreen = observer(function Home(_opts: Props) {
   const store = useStores()
   const onMainScroll = useOnMainScroll(store)
   const {screen, track} = useAnalytics()
   const scrollElRef = React.useRef<FlatList>(null)
-  const [wasVisible, setWasVisible] = React.useState<boolean>(false)
   const {appState} = useAppState({
     onForeground: () => doPoll(true),
   })
+  const isFocused = useIsFocused()
 
   const doPoll = React.useCallback(
     (knownActive = false) => {
-      if ((!knownActive && appState !== 'active') || !visible) {
+      if ((!knownActive && appState !== 'active') || !isFocused) {
         return
       }
       if (store.me.mainFeed.isLoading) {
@@ -37,7 +39,7 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
       store.log.debug('HomeScreen: Polling for new posts')
       store.me.mainFeed.checkForLatest()
     },
-    [appState, visible, store],
+    [appState, isFocused, store],
   )
 
   const scrollToTop = React.useCallback(() => {
@@ -46,53 +48,35 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
     scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
   }, [scrollElRef])
 
-  React.useEffect(() => {
-    const softResetSub = store.onScreenSoftReset(scrollToTop)
-    const feedCleanup = store.me.mainFeed.registerListeners()
-    const pollInterval = setInterval(doPoll, 15e3)
-    const cleanup = () => {
-      clearInterval(pollInterval)
-      softResetSub.remove()
-      feedCleanup()
-    }
+  useFocusEffect(
+    React.useCallback(() => {
+      const softResetSub = store.onScreenSoftReset(scrollToTop)
+      const feedCleanup = store.me.mainFeed.registerListeners()
+      const pollInterval = setInterval(doPoll, 15e3)
 
-    // guard to only continue when transitioning from !visible -> visible
-    // TODO is this 100% needed? depends on if useEffect() is getting refired
-    //      for reasons other than `visible` changing -prf
-    if (!visible) {
-      setWasVisible(false)
-      return cleanup
-    } else if (wasVisible) {
-      return cleanup
-    }
-    setWasVisible(true)
+      screen('Feed')
+      store.log.debug('HomeScreen: Updating feed')
+      if (store.me.mainFeed.hasContent) {
+        store.me.mainFeed.update()
+      }
 
-    // just became visible
-    screen('Feed')
-    store.nav.setTitle(navIdx, 'Home')
-    store.log.debug('HomeScreen: Updating feed')
-    if (store.me.mainFeed.hasContent) {
-      store.me.mainFeed.update()
-    }
-    return cleanup
-  }, [
-    visible,
-    store,
-    store.me.mainFeed,
-    navIdx,
-    doPoll,
-    wasVisible,
-    scrollToTop,
-    screen,
-  ])
+      return () => {
+        clearInterval(pollInterval)
+        softResetSub.remove()
+        feedCleanup()
+      }
+    }, [store, doPoll, scrollToTop, screen]),
+  )
 
   const onPressCompose = React.useCallback(() => {
     track('HomeScreen:PressCompose')
     store.shell.openComposer({})
   }, [store, track])
+
   const onPressTryAgain = React.useCallback(() => {
     store.me.mainFeed.refresh()
   }, [store])
+
   const onPressLoadLatest = React.useCallback(() => {
     store.me.mainFeed.refresh()
     scrollToTop()
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index c067d3506..8e0fe8dd3 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -1,28 +1,30 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ScrollView} from '../com/util/Views'
 import {useStores} from 'state/index'
-import {ScreenParams} from '../routes'
 import {s} from 'lib/styles'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Text} from '../com/util/text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {ago} from 'lib/strings/time'
 
-export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
+export const LogScreen = observer(function Log({}: NativeStackScreenProps<
+  CommonNavigatorParams,
+  'Log'
+>) {
   const pal = usePalette('default')
   const store = useStores()
   const [expanded, setExpanded] = React.useState<string[]>([])
 
-  useEffect(() => {
-    if (!visible) {
-      return
-    }
-    store.shell.setMinimalShellMode(false)
-    store.nav.setTitle(navIdx, 'Log')
-  }, [visible, store, navIdx])
+  useFocusEffect(
+    React.useCallback(() => {
+      store.shell.setMinimalShellMode(false)
+    }, [store]),
+  )
 
   const toggler = (id: string) => () => {
     if (expanded.includes(id)) {
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index 77bbdd2aa..6ab37f117 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -1,20 +1,41 @@
 import React from 'react'
-import {Button, StyleSheet, View} from 'react-native'
+import {StyleSheet, View} from 'react-native'
+import {useNavigation, StackActions} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
+import {Button} from 'view/com/util/forms/Button'
+import {NavigationProp} from 'lib/routes/types'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+
+export const NotFoundScreen = () => {
+  const pal = usePalette('default')
+  const navigation = useNavigation<NavigationProp>()
+
+  const canGoBack = navigation.canGoBack()
+  const onPressHome = React.useCallback(() => {
+    if (canGoBack) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('HomeTab')
+      navigation.dispatch(StackActions.popToTop())
+    }
+  }, [navigation, canGoBack])
 
-export const NotFound = () => {
-  const stores = useStores()
   return (
-    <View testID="notFoundView">
+    <View testID="notFoundView" style={pal.view}>
       <ViewHeader title="Page not found" />
       <View style={styles.container}>
-        <Text style={styles.title}>Page not found</Text>
+        <Text type="title-2xl" style={[pal.text, s.mb10]}>
+          Page not found
+        </Text>
+        <Text type="md" style={[pal.text, s.mb10]}>
+          We're sorry! We can't find the page you were looking for.
+        </Text>
         <Button
-          testID="navigateHomeButton"
-          title="Home"
-          onPress={() => stores.nav.navigate('/')}
+          type="primary"
+          label={canGoBack ? 'Go back' : 'Go home'}
+          onPress={onPressHome}
         />
       </View>
     </View>
@@ -23,12 +44,9 @@ export const NotFound = () => {
 
 const styles = StyleSheet.create({
   container: {
-    justifyContent: 'center',
-    alignItems: 'center',
     paddingTop: 100,
-  },
-  title: {
-    fontSize: 40,
-    fontWeight: 'bold',
+    paddingHorizontal: 20,
+    alignItems: 'center',
+    height: '100%',
   },
 })
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index f1a9e8bf0..492177d1f 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,17 +1,25 @@
 import React, {useEffect} from 'react'
 import {FlatList, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
 import useAppState from 'react-native-appstate-hook'
+import {
+  NativeStackScreenProps,
+  NotificationsTabNavigatorParams,
+} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
 import {useStores} from 'state/index'
-import {ScreenParams} from '../routes'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {s} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics'
 
 const NOTIFICATIONS_POLL_INTERVAL = 15e3
 
-export const Notifications = ({navIdx, visible}: ScreenParams) => {
+type Props = NativeStackScreenProps<
+  NotificationsTabNavigatorParams,
+  'Notifications'
+>
+export const NotificationsScreen = ({}: Props) => {
   const store = useStores()
   const onMainScroll = useOnMainScroll(store)
   const scrollElRef = React.useRef<FlatList>(null)
@@ -59,21 +67,19 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
 
   // on-visible setup
   // =
-  useEffect(() => {
-    if (!visible) {
-      // mark read when the user leaves the screen
-      store.me.notifications.markAllRead()
-      return
-    }
-    store.log.debug('NotificationsScreen: Updating feed')
-    const softResetSub = store.onScreenSoftReset(scrollToTop)
-    store.me.notifications.update()
-    screen('Notifications')
-    store.nav.setTitle(navIdx, 'Notifications')
-    return () => {
-      softResetSub.remove()
-    }
-  }, [visible, store, navIdx, screen, scrollToTop])
+  useFocusEffect(
+    React.useCallback(() => {
+      store.log.debug('NotificationsScreen: Updating feed')
+      const softResetSub = store.onScreenSoftReset(scrollToTop)
+      store.me.notifications.update()
+      screen('Notifications')
+
+      return () => {
+        softResetSub.remove()
+        store.me.notifications.markAllRead()
+      }
+    }, [store, screen, scrollToTop]),
+  )
 
   return (
     <View style={s.hContentRegion}>
diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx
deleted file mode 100644
index 570482598..000000000
--- a/src/view/screens/PostDownvotedBy.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, {useEffect} from 'react'
-import {View} from 'react-native'
-import {ViewHeader} from '../com/util/ViewHeader'
-import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
-import {ScreenParams} from '../routes'
-import {useStores} from 'state/index'
-import {makeRecordUri} from 'lib/strings/url-helpers'
-
-export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
-  const store = useStores()
-  const {name, rkey} = params
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, 'Downvoted by')
-      store.shell.setMinimalShellMode(false)
-    }
-  }, [store, visible, navIdx])
-
-  return (
-    <View>
-      <ViewHeader title="Downvoted by" />
-      <PostLikedByComponent uri={uri} direction="down" />
-    </View>
-  )
-}
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index 4be4b4b42..1a63445e5 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -1,22 +1,23 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 
-export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
+export const PostRepostedByScreen = ({route}: Props) => {
   const store = useStores()
-  const {name, rkey} = params
+  const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
 
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, 'Reposted by')
+  useFocusEffect(
+    React.useCallback(() => {
       store.shell.setMinimalShellMode(false)
-    }
-  }, [store, visible, navIdx])
+    }, [store]),
+  )
 
   return (
     <View>
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 0b6829735..0e9feae0b 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -1,58 +1,45 @@
-import React, {useEffect, useMemo} from 'react'
+import React, {useMemo} from 'react'
 import {StyleSheet, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
 import {ComposePrompt} from 'view/com/composer/Prompt'
 import {PostThreadViewModel} from 'state/models/post-thread-view'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {clamp} from 'lodash'
+import {isDesktopWeb} from 'platform/detection'
 
 const SHELL_FOOTER_HEIGHT = 44
 
-export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
+export const PostThreadScreen = ({route}: Props) => {
   const store = useStores()
   const safeAreaInsets = useSafeAreaInsets()
-  const {name, rkey} = params
+  const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
   const view = useMemo<PostThreadViewModel>(
     () => new PostThreadViewModel(store, {uri}),
     [store, uri],
   )
 
-  useEffect(() => {
-    let aborted = false
-    const threadCleanup = view.registerListeners()
-    const setTitle = () => {
-      const author = view.thread?.post.author
-      const niceName = author?.handle || name
-      store.nav.setTitle(navIdx, `Post by ${niceName}`)
-    }
-    if (!visible) {
-      return threadCleanup
-    }
-    setTitle()
-    store.shell.setMinimalShellMode(false)
-    if (!view.hasLoaded && !view.isLoading) {
-      view.setup().then(
-        () => {
-          if (!aborted) {
-            setTitle()
-          }
-        },
-        err => {
+  useFocusEffect(
+    React.useCallback(() => {
+      const threadCleanup = view.registerListeners()
+      store.shell.setMinimalShellMode(false)
+      if (!view.hasLoaded && !view.isLoading) {
+        view.setup().catch(err => {
           store.log.error('Failed to fetch thread', err)
-        },
-      )
-    }
-    return () => {
-      aborted = true
-      threadCleanup()
-    }
-  }, [visible, store.nav, store.log, store.shell, name, navIdx, view])
+        })
+      }
+      return () => {
+        threadCleanup()
+      }
+    }, [store, view]),
+  )
 
   const onPressReply = React.useCallback(() => {
     if (!view.thread) {
@@ -77,15 +64,24 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
     <View style={s.hContentRegion}>
       <ViewHeader title="Post" />
       <View style={s.hContentRegion}>
-        <PostThreadComponent uri={uri} view={view} />
-      </View>
-      <View
-        style={[
-          styles.prompt,
-          {bottom: SHELL_FOOTER_HEIGHT + clamp(safeAreaInsets.bottom, 15, 30)},
-        ]}>
-        <ComposePrompt onPressCompose={onPressReply} />
+        <PostThreadComponent
+          uri={uri}
+          view={view}
+          onPressReply={onPressReply}
+        />
       </View>
+      {!isDesktopWeb && (
+        <View
+          style={[
+            styles.prompt,
+            {
+              bottom:
+                SHELL_FOOTER_HEIGHT + clamp(safeAreaInsets.bottom, 15, 30),
+            },
+          ]}>
+          <ComposePrompt onPressCompose={onPressReply} />
+        </View>
+      )}
     </View>
   )
 }
diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx
index 4d6ad4114..b1690721b 100644
--- a/src/view/screens/PostUpvotedBy.tsx
+++ b/src/view/screens/PostUpvotedBy.tsx
@@ -1,21 +1,23 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 
-export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'>
+export const PostUpvotedByScreen = ({route}: Props) => {
   const store = useStores()
-  const {name, rkey} = params
+  const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
 
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, 'Liked by')
-    }
-  }, [store, visible, navIdx])
+  useFocusEffect(
+    React.useCallback(() => {
+      store.shell.setMinimalShellMode(false)
+    }, [store]),
+  )
 
   return (
     <View>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index fa0c04106..e0d0a5884 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,9 +1,10 @@
 import React, {useEffect, useState} from 'react'
 import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewSelector} from '../com/util/ViewSelector'
 import {CenteredView} from '../com/util/Views'
-import {ScreenParams} from '../routes'
 import {ProfileUiModel, Sections} from 'state/models/profile-ui'
 import {useStores} from 'state/index'
 import {ProfileHeader} from '../com/profile/ProfileHeader'
@@ -23,7 +24,8 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
 const END_ITEM = {_reactKey: '__end__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
 
-export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
+export const ProfileScreen = observer(({route}: Props) => {
   const store = useStores()
   const {screen, track} = useAnalytics()
 
@@ -34,35 +36,30 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const onMainScroll = useOnMainScroll(store)
   const [hasSetup, setHasSetup] = useState<boolean>(false)
   const uiState = React.useMemo(
-    () => new ProfileUiModel(store, {user: params.name}),
-    [params.name, store],
+    () => new ProfileUiModel(store, {user: route.params.name}),
+    [route.params.name, store],
   )
 
-  useEffect(() => {
-    store.nav.setTitle(navIdx, params.name)
-  }, [store, navIdx, params.name])
-
-  useEffect(() => {
-    let aborted = false
-    const feedCleanup = uiState.feed.registerListeners()
-    if (!visible) {
-      return feedCleanup
-    }
-    if (hasSetup) {
-      uiState.update()
-    } else {
-      uiState.setup().then(() => {
-        if (aborted) {
-          return
-        }
-        setHasSetup(true)
-      })
-    }
-    return () => {
-      aborted = true
-      feedCleanup()
-    }
-  }, [visible, store, hasSetup, uiState])
+  useFocusEffect(
+    React.useCallback(() => {
+      let aborted = false
+      const feedCleanup = uiState.feed.registerListeners()
+      if (hasSetup) {
+        uiState.update()
+      } else {
+        uiState.setup().then(() => {
+          if (aborted) {
+            return
+          }
+          setHasSetup(true)
+        })
+      }
+      return () => {
+        aborted = true
+        feedCleanup()
+      }
+    }, [hasSetup, uiState]),
+  )
 
   // events
   // =
@@ -171,7 +168,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
         <ErrorScreen
           testID="profileErrorScreen"
           title="Failed to load profile"
-          message={`There was an issue when attempting to load ${params.name}`}
+          message={`There was an issue when attempting to load ${route.params.name}`}
           details={uiState.profile.error}
           onPressTryAgain={onPressTryAgain}
         />
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 9f1a9c741..b248cdc3a 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -1,20 +1,21 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 
-export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
+export const ProfileFollowersScreen = ({route}: Props) => {
   const store = useStores()
-  const {name} = params
+  const {name} = route.params
 
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, `Followers of ${name}`)
+  useFocusEffect(
+    React.useCallback(() => {
       store.shell.setMinimalShellMode(false)
-    }
-  }, [store, visible, name, navIdx])
+    }, [store]),
+  )
 
   return (
     <View>
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 1cdb5bccf..7edf8edba 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -1,20 +1,21 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 
-export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
+export const ProfileFollowsScreen = ({route}: Props) => {
   const store = useStores()
-  const {name} = params
+  const {name} = route.params
 
-  useEffect(() => {
-    if (visible) {
-      store.nav.setTitle(navIdx, `Followed by ${name}`)
+  useFocusEffect(
+    React.useCallback(() => {
       store.shell.setMinimalShellMode(false)
-    }
-  }, [store, visible, name, navIdx])
+    }, [store]),
+  )
 
   return (
     <View>
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index a87c41e76..a50d5c6a7 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -7,12 +7,19 @@ import {
   TouchableWithoutFeedback,
   View,
 } from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useFocusEffect} from '@react-navigation/native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
 import {ScrollView} from '../com/util/Views'
+import {
+  NativeStackScreenProps,
+  SearchTabNavigatorParams,
+} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
 import {UserAvatar} from '../com/util/UserAvatar'
 import {Text} from '../com/util/text/Text'
-import {ScreenParams} from '../routes'
 import {useStores} from 'state/index'
 import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
 import {s} from 'lib/styles'
@@ -21,14 +28,17 @@ import {WhoToFollow} from '../com/discover/WhoToFollow'
 import {SuggestedPosts} from '../com/discover/SuggestedPosts'
 import {ProfileCard} from '../com/profile/ProfileCard'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useAnalytics} from 'lib/analytics'
 
 const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
 const FIVE_MIN = 5 * 60 * 1e3
 
-export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
+type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
+export const SearchScreen = observer<Props>(({}: Props) => {
   const pal = usePalette('default')
+  const theme = useTheme()
   const store = useStores()
   const {track} = useAnalytics()
   const scrollElRef = React.useRef<ScrollView>(null)
@@ -41,33 +51,32 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
     () => new UserAutocompleteViewModel(store),
     [store],
   )
-  const {name} = params
 
   const onSoftReset = () => {
     scrollElRef.current?.scrollTo({x: 0, y: 0})
   }
 
-  React.useEffect(() => {
-    const softResetSub = store.onScreenSoftReset(onSoftReset)
-    const cleanup = () => {
-      softResetSub.remove()
-    }
+  useFocusEffect(
+    React.useCallback(() => {
+      const softResetSub = store.onScreenSoftReset(onSoftReset)
+      const cleanup = () => {
+        softResetSub.remove()
+      }
 
-    if (visible) {
       const now = Date.now()
       if (now - lastRenderTime > FIVE_MIN) {
         setRenderTime(Date.now()) // trigger reload of suggestions
       }
       store.shell.setMinimalShellMode(false)
       autocompleteView.setup()
-      store.nav.setTitle(navIdx, 'Search')
-    }
-    return cleanup
-  }, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
+
+      return cleanup
+    }, [store, autocompleteView, lastRenderTime, setRenderTime]),
+  )
 
   const onPressMenu = () => {
     track('ViewHeader:MenuButtonClicked')
-    store.shell.setMainMenuOpen(true)
+    store.shell.openDrawer()
   }
 
   const onChangeQuery = (text: string) => {
@@ -102,12 +111,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
             onPress={onPressMenu}
             hitSlop={MENU_HITSLOP}
             style={styles.headerMenuBtn}>
-            <UserAvatar
-              size={30}
-              handle={store.me.handle}
-              displayName={store.me.displayName}
-              avatar={store.me.avatar}
-            />
+            <UserAvatar size={30} avatar={store.me.avatar} />
           </TouchableOpacity>
           <View
             style={[
@@ -127,13 +131,18 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
               returnKeyType="search"
               value={query}
               style={[pal.text, styles.headerSearchInput]}
+              keyboardAppearance={theme.colorScheme}
               onFocus={() => setIsInputFocused(true)}
               onBlur={() => setIsInputFocused(false)}
               onChangeText={onChangeQuery}
             />
             {query ? (
               <TouchableOpacity onPress={onPressClearQuery}>
-                <FontAwesomeIcon icon="xmark" size={16} style={pal.textLight} />
+                <FontAwesomeIcon
+                  icon="xmark"
+                  size={16}
+                  style={pal.textLight as FontAwesomeIconStyle}
+                />
               </TouchableOpacity>
             ) : undefined}
           </View>
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
index 886d49af7..75b5f01ce 100644
--- a/src/view/screens/Search.web.tsx
+++ b/src/view/screens/Search.web.tsx
@@ -1,8 +1,12 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
 import {ScrollView} from '../com/util/Views'
 import {observer} from 'mobx-react-lite'
-import {ScreenParams} from '../routes'
+import {
+  NativeStackScreenProps,
+  SearchTabNavigatorParams,
+} from 'lib/routes/types'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {WhoToFollow} from '../com/discover/WhoToFollow'
@@ -12,7 +16,8 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 
 const FIVE_MIN = 5 * 60 * 1e3
 
-export const Search = observer(({navIdx, visible}: ScreenParams) => {
+type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
+export const SearchScreen = observer(({}: Props) => {
   const pal = usePalette('default')
   const store = useStores()
   const scrollElRef = React.useRef<ScrollView>(null)
@@ -23,22 +28,21 @@ export const Search = observer(({navIdx, visible}: ScreenParams) => {
     scrollElRef.current?.scrollTo({x: 0, y: 0})
   }
 
-  React.useEffect(() => {
-    const softResetSub = store.onScreenSoftReset(onSoftReset)
-    const cleanup = () => {
-      softResetSub.remove()
-    }
+  useFocusEffect(
+    React.useCallback(() => {
+      const softResetSub = store.onScreenSoftReset(onSoftReset)
 
-    if (visible) {
       const now = Date.now()
       if (now - lastRenderTime > FIVE_MIN) {
         setRenderTime(Date.now()) // trigger reload of suggestions
       }
       store.shell.setMinimalShellMode(false)
-      store.nav.setTitle(navIdx, 'Search')
-    }
-    return cleanup
-  }, [store, visible, navIdx, lastRenderTime])
+
+      return () => {
+        softResetSub.remove()
+      }
+    }, [store, lastRenderTime, setRenderTime]),
+  )
 
   return (
     <ScrollView
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 47e76a124..2e5d2c001 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect} from 'react'
+import React from 'react'
 import {
   ActivityIndicator,
   StyleSheet,
@@ -6,13 +6,18 @@ import {
   View,
 } from 'react-native'
 import {
+  useFocusEffect,
+  useNavigation,
+  StackActions,
+} from '@react-navigation/native'
+import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {observer} from 'mobx-react-lite'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import * as AppInfo from 'lib/app-info'
 import {useStores} from 'state/index'
-import {ScreenParams} from '../routes'
 import {s, colors} from 'lib/styles'
 import {ScrollView} from '../com/util/Views'
 import {ViewHeader} from '../com/util/ViewHeader'
@@ -25,41 +30,38 @@ import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {AccountData} from 'state/models/session'
 import {useAnalytics} from 'lib/analytics'
+import {NavigationProp} from 'lib/routes/types'
 
-export const Settings = observer(function Settings({
-  navIdx,
-  visible,
-}: ScreenParams) {
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+export const SettingsScreen = observer(function Settings({}: Props) {
   const theme = useTheme()
   const pal = usePalette('default')
   const store = useStores()
+  const navigation = useNavigation<NavigationProp>()
   const {screen, track} = useAnalytics()
   const [isSwitching, setIsSwitching] = React.useState(false)
 
-  useEffect(() => {
-    screen('Settings')
-  }, [screen])
-
-  useEffect(() => {
-    if (!visible) {
-      return
-    }
-    store.shell.setMinimalShellMode(false)
-    store.nav.setTitle(navIdx, 'Settings')
-  }, [visible, store, navIdx])
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('Settings')
+      store.shell.setMinimalShellMode(false)
+    }, [screen, store]),
+  )
 
   const onPressSwitchAccount = async (acct: AccountData) => {
     track('Settings:SwitchAccountButtonClicked')
     setIsSwitching(true)
     if (await store.session.resumeSession(acct)) {
       setIsSwitching(false)
-      store.nav.tab.fixedTabReset()
+      navigation.navigate('HomeTab')
+      navigation.dispatch(StackActions.popToTop())
       Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
       return
     }
     setIsSwitching(false)
     Toast.show('Sorry! We need you to enter your password.')
-    store.nav.tab.fixedTabReset()
+    navigation.navigate('HomeTab')
+    navigation.dispatch(StackActions.popToTop())
     store.session.clear()
   }
   const onPressAddAccount = () => {
@@ -118,12 +120,7 @@ export const Settings = observer(function Settings({
             noFeedback>
             <View style={[pal.view, styles.linkCard]}>
               <View style={styles.avi}>
-                <UserAvatar
-                  size={40}
-                  displayName={store.me.displayName}
-                  handle={store.me.handle || ''}
-                  avatar={store.me.avatar}
-                />
+                <UserAvatar size={40} avatar={store.me.avatar} />
               </View>
               <View style={[s.flex1]}>
                 <Text type="md-bold" style={pal.text} numberOfLines={1}>
@@ -152,12 +149,7 @@ export const Settings = observer(function Settings({
               isSwitching ? undefined : () => onPressSwitchAccount(account)
             }>
             <View style={styles.avi}>
-              <UserAvatar
-                size={40}
-                displayName={account.displayName}
-                handle={account.handle || ''}
-                avatar={account.aviUrl}
-              />
+              <UserAvatar size={40} avatar={account.aviUrl} />
             </View>
             <View style={[s.flex1]}>
               <Text type="md-bold" style={pal.text}>