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.tsx6
-rw-r--r--src/view/screens/Debug.tsx15
-rw-r--r--src/view/screens/Home.tsx58
-rw-r--r--src/view/screens/Log.tsx8
-rw-r--r--src/view/screens/Login.tsx47
-rw-r--r--src/view/screens/Login.web.tsx14
-rw-r--r--src/view/screens/NotFound.tsx2
-rw-r--r--src/view/screens/Notifications.tsx81
-rw-r--r--src/view/screens/Onboard.tsx4
-rw-r--r--src/view/screens/PostDownvotedBy.tsx4
-rw-r--r--src/view/screens/PostRepostedBy.tsx4
-rw-r--r--src/view/screens/PostThread.tsx14
-rw-r--r--src/view/screens/PostUpvotedBy.tsx4
-rw-r--r--src/view/screens/Profile.tsx19
-rw-r--r--src/view/screens/ProfileFollowers.tsx4
-rw-r--r--src/view/screens/ProfileFollows.tsx4
-rw-r--r--src/view/screens/Search.tsx232
-rw-r--r--src/view/screens/Settings.tsx40
18 files changed, 374 insertions, 186 deletions
diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx
index cba17f285..21943a10a 100644
--- a/src/view/screens/Contacts.tsx
+++ b/src/view/screens/Contacts.tsx
@@ -4,10 +4,10 @@ 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 {colors} from 'lib/styles'
 import {ScreenParams} from '../routes'
-import {useStores} from '../../state'
-import {useAnimatedValue} from '../lib/hooks/useAnimatedValue'
+import {useStores} from 'state/index'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 
 export const Contacts = ({navIdx, visible}: ScreenParams) => {
   const store = useStores()
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index 0223e631d..09e3dd46a 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -1,11 +1,10 @@
 import React from 'react'
 import {ScrollView, View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
-import {ThemeProvider} from '../lib/ThemeContext'
-import {PaletteColorName} from '../lib/ThemeContext'
-import {usePalette} from '../lib/hooks/usePalette'
-import {s} from '../lib/styles'
-import {displayNotification} from '../lib/notifee'
+import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {displayNotification} from 'lib/notifee'
 
 import {Text} from '../com/util/text/Text'
 import {ViewSelector} from '../com/util/ViewSelector'
@@ -284,6 +283,9 @@ function TypographyView() {
         'xs-heavy' lorem ipsum dolor
       </Text>
 
+      <Text type="title-2xl" style={[pal.text]}>
+        'title-2xl' lorem ipsum dolor
+      </Text>
       <Text type="title-xl" style={[pal.text]}>
         'title-xl' lorem ipsum dolor
       </Text>
@@ -296,6 +298,9 @@ function TypographyView() {
       <Text type="button" style={[pal.text]}>
         Button
       </Text>
+      <Text type="button-lg" style={[pal.text]}>
+        Button-lg
+      </Text>
     </View>
   )
 }
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 4222c7513..de7e61ba4 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,23 +1,24 @@
 import React, {useEffect} from 'react'
-import {View} from 'react-native'
+import {FlatList, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/posts/Feed'
 import {FAB} from '../com/util/FAB'
 import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 import {ScreenParams} from '../routes'
-import {s} from '../lib/styles'
-import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
+import {s} from 'lib/styles'
+import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {useAnalytics} from 'lib/analytics'
 
-export const Home = observer(function Home({
-  navIdx,
-  visible,
-  scrollElRef,
-}: ScreenParams) {
+const HEADER_HEIGHT = 42
+
+export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
   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),
@@ -31,22 +32,31 @@ export const Home = observer(function Home({
       if (store.me.mainFeed.isLoading) {
         return
       }
-      store.log.debug('Polling home feed')
-      store.me.mainFeed.checkForLatest().catch(e => {
-        store.log.error('Failed to poll feed', e)
-      })
+      store.log.debug('HomeScreen: Polling for new posts')
+      store.me.mainFeed.checkForLatest()
     },
     [appState, visible, store],
   )
 
+  const scrollToTop = React.useCallback(() => {
+    // NOTE: the feed is offset by the height of the collapsing header,
+    //       so we scroll to the negative of that height -prf
+    scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT})
+  }, [scrollElRef])
+
   useEffect(() => {
+    const softResetSub = store.onScreenSoftReset(scrollToTop)
     const feedCleanup = store.me.mainFeed.registerListeners()
-    const pollInterval = setInterval(() => doPoll(), 15e3)
+    const pollInterval = setInterval(doPoll, 15e3)
     const cleanup = () => {
       clearInterval(pollInterval)
+      softResetSub.remove()
       feedCleanup()
     }
 
+    // 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
@@ -55,17 +65,20 @@ export const Home = observer(function Home({
     }
     setWasVisible(true)
 
+    // just became visible
+    screen('Feed')
     store.nav.setTitle(navIdx, 'Home')
-    store.log.debug('Updating home feed')
+    store.log.debug('HomeScreen: Updating feed')
     if (store.me.mainFeed.hasContent) {
       store.me.mainFeed.update()
     } else {
       store.me.mainFeed.setup()
     }
     return cleanup
-  }, [visible, store, navIdx, doPoll, wasVisible])
+  }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen])
 
   const onPressCompose = (imagesOpen?: boolean) => {
+    track('Home:ComposeButtonPressed')
     store.shell.openComposer({imagesOpen})
   }
   const onPressTryAgain = () => {
@@ -73,26 +86,31 @@ export const Home = observer(function Home({
   }
   const onPressLoadLatest = () => {
     store.me.mainFeed.refresh()
-    scrollElRef?.current?.scrollToOffset({offset: 0})
+    scrollToTop()
   }
 
   return (
     <View style={s.h100pct}>
-      <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} />
       <Feed
         testID="homeFeed"
         key="default"
         feed={store.me.mainFeed}
         scrollElRef={scrollElRef}
         style={s.h100pct}
-        onPressCompose={onPressCompose}
         onPressTryAgain={onPressTryAgain}
+        onPressCompose={onPressCompose}
         onScroll={onMainScroll}
+        headerOffset={HEADER_HEIGHT}
       />
+      <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll />
       {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && (
         <LoadLatestBtn onPress={onPressLoadLatest} />
       )}
-      <FAB icon="pen-nib" onPress={() => onPressCompose(false)} />
+      <FAB
+        testID="composeFAB"
+        icon="plus"
+        onPress={() => onPressCompose(false)}
+      />
     </View>
   )
 })
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index c3e156dcb..c067d3506 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -3,13 +3,13 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {ScrollView} from '../com/util/Views'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 import {ScreenParams} from '../routes'
-import {s} from '../lib/styles'
+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'
+import {usePalette} from 'lib/hooks/usePalette'
+import {ago} from 'lib/strings/time'
 
 export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
   const pal = usePalette('default')
diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx
index 81a2c9e6b..50b2a34c0 100644
--- a/src/view/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -1,19 +1,16 @@
-import React, {useState} from 'react'
-import {
-  Image,
-  SafeAreaView,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import React, {useEffect, useState} from 'react'
+import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import Image, {Source as ImageSource} from 'view/com/util/images/Image'
 import {observer} from 'mobx-react-lite'
 import {Signin} from '../com/login/Signin'
 import {CreateAccount} from '../com/login/CreateAccount'
 import {Text} from '../com/util/text/Text'
 import {ErrorBoundary} from '../com/util/ErrorBoundary'
-import {colors} from '../lib/styles'
-import {usePalette} from '../lib/hooks/usePalette'
-import {CLOUD_SPLASH} from '../lib/assets'
+import {colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {CLOUD_SPLASH} from 'lib/assets'
+import {useAnalytics} from 'lib/analytics'
 
 enum ScreenState {
   S_SigninOrCreateAccount,
@@ -28,6 +25,12 @@ const SigninOrCreateAccount = ({
   onPressSignin: () => void
   onPressCreateAccount: () => void
 }) => {
+  const {screen} = useAnalytics()
+
+  useEffect(() => {
+    screen('Login')
+  }, [screen])
+
   const pal = usePalette('default')
   return (
     <>
@@ -57,22 +60,28 @@ const SigninOrCreateAccount = ({
 
 export const Login = observer(() => {
   const pal = usePalette('default')
+  const store = useStores()
   const [screenState, setScreenState] = useState<ScreenState>(
     ScreenState.S_SigninOrCreateAccount,
   )
 
-  if (screenState === ScreenState.S_SigninOrCreateAccount) {
+  if (
+    store.session.isResumingSession ||
+    screenState === ScreenState.S_SigninOrCreateAccount
+  ) {
     return (
       <View style={styles.container}>
-        <Image source={CLOUD_SPLASH} style={styles.bgImg} />
+        <Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
         <SafeAreaView testID="noSessionView" style={styles.container}>
           <ErrorBoundary>
-            <SigninOrCreateAccount
-              onPressSignin={() => setScreenState(ScreenState.S_Signin)}
-              onPressCreateAccount={() =>
-                setScreenState(ScreenState.S_CreateAccount)
-              }
-            />
+            {!store.session.isResumingSession && (
+              <SigninOrCreateAccount
+                onPressSignin={() => setScreenState(ScreenState.S_Signin)}
+                onPressCreateAccount={() =>
+                  setScreenState(ScreenState.S_CreateAccount)
+                }
+              />
+            )}
           </ErrorBoundary>
         </SafeAreaView>
       </View>
diff --git a/src/view/screens/Login.web.tsx b/src/view/screens/Login.web.tsx
index 77149090c..90effc5d6 100644
--- a/src/view/screens/Login.web.tsx
+++ b/src/view/screens/Login.web.tsx
@@ -1,20 +1,13 @@
 import React, {useState} from 'react'
-import {
-  Image,
-  SafeAreaView,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-} from 'react-native'
+import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {CenteredView} from '../com/util/Views'
 import {Signin} from '../com/login/Signin'
 import {CreateAccount} from '../com/login/CreateAccount'
 import {Text} from '../com/util/text/Text'
 import {ErrorBoundary} from '../com/util/ErrorBoundary'
-import {colors} from '../lib/styles'
-import {usePalette} from '../lib/hooks/usePalette'
-import {CLOUD_SPLASH} from '../lib/assets'
+import {colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
 
 enum ScreenState {
   S_SigninOrCreateAccount,
@@ -125,6 +118,7 @@ const styles = StyleSheet.create({
     width: '100%',
     height: '100%',
   },
+  hero: {},
   heroText: {
     backgroundColor: colors.white,
     paddingTop: 10,
diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index c5c5ff002..77bbdd2aa 100644
--- a/src/view/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {Button, StyleSheet, View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Text} from '../com/util/text/Text'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 
 export const NotFound = () => {
   const stores = useStores()
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 9b5dc5970..548b0d564 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,35 +1,79 @@
 import React, {useEffect} from 'react'
-import {View} from 'react-native'
+import {FlatList, View} from 'react-native'
+import useAppState from 'react-native-appstate-hook'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 import {ScreenParams} from '../routes'
-import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
-import {s} from '../lib/styles'
+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) => {
   const store = useStores()
   const onMainScroll = useOnMainScroll(store)
+  const scrollElRef = React.useRef<FlatList>(null)
+  const {screen} = useAnalytics()
+  const {appState} = useAppState({
+    onForeground: () => doPoll(true),
+  })
 
+  // event handlers
+  // =
+  const onPressTryAgain = () => {
+    store.me.notifications.refresh()
+  }
+  const scrollToTop = React.useCallback(() => {
+    scrollElRef.current?.scrollToOffset({offset: 0})
+  }, [scrollElRef])
+
+  // periodic polling
+  // =
+  const doPoll = React.useCallback(
+    async (isForegrounding = false) => {
+      if (isForegrounding) {
+        // app is foregrounding, refresh optimistically
+        store.log.debug('NotificationsScreen: Refreshing on app foreground')
+        await Promise.all([
+          store.me.notifications.loadUnreadCount(),
+          store.me.notifications.refresh(),
+        ])
+      } else if (appState === 'active') {
+        // periodic poll, refresh if there are new notifs
+        store.log.debug('NotificationsScreen: Polling for new notifications')
+        const didChange = await store.me.notifications.loadUnreadCount()
+        if (didChange) {
+          store.log.debug('NotificationsScreen: Loading new notifications')
+          await store.me.notifications.loadLatest()
+        }
+      }
+    },
+    [appState, store],
+  )
+  useEffect(() => {
+    const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL)
+    return () => clearInterval(pollInterval)
+  }, [doPoll])
+
+  // on-visible setup
+  // =
   useEffect(() => {
     if (!visible) {
       return
     }
-    store.log.debug('Updating notifications feed')
-    store.me.notifications
-      .update()
-      .catch(e => {
-        store.log.error('Error while updating notifications feed', e)
-      })
-      .then(() => {
-        store.me.notifications.updateReadState()
-      })
+    store.log.debug('NotificationsScreen: Updating feed')
+    const softResetSub = store.onScreenSoftReset(scrollToTop)
+    store.me.notifications.update().then(() => {
+      store.me.notifications.markAllRead()
+    })
+    screen('Notifications')
     store.nav.setTitle(navIdx, 'Notifications')
-  }, [visible, store, navIdx])
-
-  const onPressTryAgain = () => {
-    store.me.notifications.refresh()
-  }
+    return () => {
+      softResetSub.remove()
+    }
+  }, [visible, store, navIdx, screen, scrollToTop])
 
   return (
     <View style={s.h100pct}>
@@ -38,6 +82,7 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
         view={store.me.notifications}
         onPressTryAgain={onPressTryAgain}
         onScroll={onMainScroll}
+        scrollElRef={scrollElRef}
       />
     </View>
   )
diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx
index e31b42adc..1485670e7 100644
--- a/src/view/screens/Onboard.tsx
+++ b/src/view/screens/Onboard.tsx
@@ -3,8 +3,8 @@ import {StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FeatureExplainer} from '../com/onboard/FeatureExplainer'
 import {Follows} from '../com/onboard/Follows'
-import {OnboardStage, OnboardStageOrder} from '../../state/models/onboard'
-import {useStores} from '../../state'
+import {OnboardStage, OnboardStageOrder} from 'state/models/onboard'
+import {useStores} from 'state/index'
 
 export const Onboard = observer(() => {
   const store = useStores()
diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx
index 1401868d4..570482598 100644
--- a/src/view/screens/PostDownvotedBy.tsx
+++ b/src/view/screens/PostDownvotedBy.tsx
@@ -3,8 +3,8 @@ 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'
-import {makeRecordUri} from '../../lib/strings'
+import {useStores} from 'state/index'
+import {makeRecordUri} from 'lib/strings/url-helpers'
 
 export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index bf4d6ec91..4be4b4b42 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -3,8 +3,8 @@ import {View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
 import {ScreenParams} from '../routes'
-import {useStores} from '../../state'
-import {makeRecordUri} from '../../lib/strings'
+import {useStores} from 'state/index'
+import {makeRecordUri} from 'lib/strings/url-helpers'
 
 export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index febaddc09..4b799468d 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -1,17 +1,16 @@
-import React, {useEffect, useMemo, useState} from 'react'
+import React, {useEffect, useMemo} from 'react'
 import {View} from 'react-native'
-import {makeRecordUri} from '../../lib/strings'
+import {makeRecordUri} from 'lib/strings/url-helpers'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
-import {PostThreadViewModel} from '../../state/models/post-thread-view'
+import {PostThreadViewModel} from 'state/models/post-thread-view'
 import {ScreenParams} from '../routes'
-import {useStores} from '../../state'
-import {s} from '../lib/styles'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
 
 export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
   const {name, rkey} = params
-  const [viewSubtitle, setViewSubtitle] = useState<string>(`by ${name}`)
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
   const view = useMemo<PostThreadViewModel>(
     () => new PostThreadViewModel(store, {uri}),
@@ -24,7 +23,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
     const setTitle = () => {
       const author = view.thread?.post.author
       const niceName = author?.handle || name
-      setViewSubtitle(`by ${niceName}`)
       store.nav.setTitle(navIdx, `Post by ${niceName}`)
     }
     if (!visible) {
@@ -52,7 +50,7 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
 
   return (
     <View style={s.h100pct}>
-      <ViewHeader title="Post" subtitle={viewSubtitle} />
+      <ViewHeader title="Post" />
       <View style={s.h100pct}>
         <PostThreadComponent uri={uri} view={view} />
       </View>
diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx
index 4bba222ae..4d6ad4114 100644
--- a/src/view/screens/PostUpvotedBy.tsx
+++ b/src/view/screens/PostUpvotedBy.tsx
@@ -3,8 +3,8 @@ 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'
-import {makeRecordUri} from '../../lib/strings'
+import {useStores} from 'state/index'
+import {makeRecordUri} from 'lib/strings/url-helpers'
 
 export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 5c6616985..03d973b96 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -4,8 +4,8 @@ import {observer} from 'mobx-react-lite'
 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'
+import {ProfileUiModel, Sections} from 'state/models/profile-ui'
+import {useStores} from 'state/index'
 import {ProfileHeader} from '../com/profile/ProfileHeader'
 import {FeedItem} from '../com/posts/FeedItem'
 import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
@@ -14,8 +14,9 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage'
 import {EmptyState} from '../com/util/EmptyState'
 import {Text} from '../com/util/text/Text'
 import {FAB} from '../com/util/FAB'
-import {s, colors} from '../lib/styles'
-import {useOnMainScroll} from '../lib/hooks/useOnMainScroll'
+import {s, colors} from 'lib/styles'
+import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {useAnalytics} from 'lib/analytics'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const END_ITEM = {_reactKey: '__end__'}
@@ -23,6 +24,12 @@ const EMPTY_ITEM = {_reactKey: '__empty__'}
 
 export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
+  const {screen} = useAnalytics()
+
+  useEffect(() => {
+    screen('Profile')
+  }, [screen])
+
   const onMainScroll = useOnMainScroll(store)
   const [hasSetup, setHasSetup] = useState<boolean>(false)
   const uiState = React.useMemo(
@@ -128,7 +135,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
           }
           if (!uiState.feed.hasMore) {
             items = items.concat([END_ITEM])
-          } else {
+          } else if (uiState.feed.isLoading) {
             Footer = LoadingMoreFooter
           }
           renderItem = (item: any) => {
@@ -184,7 +191,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
       ) : (
         <CenteredView>{renderHeader()}</CenteredView>
       )}
-      <FAB icon="pen-nib" onPress={onPressCompose} />
+      <FAB icon="plus" onPress={onPressCompose} />
     </View>
   )
 })
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index f7520549e..9f1a9c741 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers'
 import {ScreenParams} from '../routes'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 
 export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
@@ -18,7 +18,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
 
   return (
     <View>
-      <ViewHeader title="Followers" subtitle={`of ${name}`} />
+      <ViewHeader title="Followers" />
       <ProfileFollowersComponent name={name} />
     </View>
   )
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 65e4004e9..1cdb5bccf 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows'
 import {ScreenParams} from '../routes'
-import {useStores} from '../../state'
+import {useStores} from 'state/index'
 
 export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
   const store = useStores()
@@ -18,7 +18,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
 
   return (
     <View>
-      <ViewHeader title="Followed" subtitle={`by ${name}`} />
+      <ViewHeader title="Following" />
       <ProfileFollowsComponent name={name} />
     </View>
   )
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index 2a1caab89..2e176d98f 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -1,41 +1,73 @@
-import React, {useEffect, useState, useMemo, useRef} from 'react'
+import React from 'react'
 import {
   Keyboard,
   StyleSheet,
   TextInput,
   TouchableOpacity,
+  TouchableWithoutFeedback,
   View,
 } from 'react-native'
-import {ViewHeader} from '../com/util/ViewHeader'
-import {CenteredView, ScrollView} from '../com/util/Views'
-import {SuggestedFollows} from '../com/discover/SuggestedFollows'
+import {ScrollView} from '../com/util/Views'
+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'
-import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view'
-import {s} from '../lib/styles'
-import {MagnifyingGlassIcon} from '../lib/icons'
-import {usePalette} from '../lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {s} from 'lib/styles'
+import {MagnifyingGlassIcon} from 'lib/icons'
+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 {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {useAnalytics} from 'lib/analytics'
 
-export const Search = ({navIdx, visible, params}: ScreenParams) => {
+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) => {
   const pal = usePalette('default')
   const store = useStores()
-  const textInput = useRef<TextInput>(null)
-  const [query, setQuery] = useState<string>('')
-  const autocompleteView = useMemo<UserAutocompleteViewModel>(
+  const {track} = useAnalytics()
+  const scrollElRef = React.useRef<ScrollView>(null)
+  const onMainScroll = useOnMainScroll(store)
+  const textInput = React.useRef<TextInput>(null)
+  const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads
+  const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
+  const [query, setQuery] = React.useState<string>('')
+  const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
     () => new UserAutocompleteViewModel(store),
     [store],
   )
   const {name} = params
 
-  useEffect(() => {
+  const onSoftReset = () => {
+    scrollElRef.current?.scrollTo({x: 0, y: 0})
+  }
+
+  React.useEffect(() => {
+    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')
     }
-  }, [store, visible, name, navIdx, autocompleteView])
+    return cleanup
+  }, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
+
+  const onPressMenu = () => {
+    track('ViewHeader:MenuButtonClicked')
+    store.shell.setMainMenuOpen(true)
+  }
 
   const onChangeQuery = (text: string) => {
     setQuery(text)
@@ -46,87 +78,139 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => {
       autocompleteView.setActive(false)
     }
   }
-  const onSelect = (handle: string) => {
-    textInput.current?.blur()
-    store.nav.navigate(`/profile/${handle}`)
+  const onPressCancelSearch = () => {
+    setQuery('')
+    autocompleteView.setActive(false)
   }
 
   return (
-    <View style={[pal.view, styles.container]}>
-      <ViewHeader title="Search" />
-      <CenteredView style={[pal.view, pal.border, styles.inputContainer]}>
-        <MagnifyingGlassIcon style={[pal.text, styles.inputIcon]} />
-        <TextInput
-          testID="searchTextInput"
-          ref={textInput}
-          placeholder="Type your query here..."
-          placeholderTextColor={pal.colors.textLight}
-          selectTextOnFocus
-          returnKeyType="search"
-          style={[pal.text, styles.input]}
-          onChangeText={onChangeQuery}
-        />
-      </CenteredView>
-      <View style={styles.outputContainer}>
-        {query ? (
-          <ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}>
-            {autocompleteView.searchRes.map((item, i) => (
-              <TouchableOpacity
-                key={i}
-                style={[pal.view, pal.border, styles.searchResult]}
-                onPress={() => onSelect(item.handle)}>
-                <UserAvatar
-                  handle={item.handle}
-                  displayName={item.displayName}
-                  avatar={item.avatar}
-                  size={36}
-                />
-                <View style={[s.ml10]}>
-                  <Text type="title-sm" style={pal.text}>
-                    {item.displayName || item.handle}
-                  </Text>
-                  <Text style={pal.textLight}>@{item.handle}</Text>
-                </View>
+    <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
+      <ScrollView
+        ref={scrollElRef}
+        testID="searchScrollView"
+        style={[pal.view, styles.container]}
+        onScroll={onMainScroll}
+        scrollEventThrottle={100}>
+        <View style={[pal.view, pal.border, styles.header]}>
+          <TouchableOpacity
+            testID="viewHeaderBackOrMenuBtn"
+            onPress={onPressMenu}
+            hitSlop={MENU_HITSLOP}
+            style={styles.headerMenuBtn}>
+            <UserAvatar
+              size={30}
+              handle={store.me.handle}
+              displayName={store.me.displayName}
+              avatar={store.me.avatar}
+            />
+          </TouchableOpacity>
+          <View
+            style={[
+              {backgroundColor: pal.colors.backgroundLight},
+              styles.headerSearchContainer,
+            ]}>
+            <MagnifyingGlassIcon
+              style={[pal.icon, styles.headerSearchIcon]}
+              size={21}
+            />
+            <TextInput
+              testID="searchTextInput"
+              ref={textInput}
+              placeholder="Search"
+              placeholderTextColor={pal.colors.textLight}
+              selectTextOnFocus
+              returnKeyType="search"
+              value={query}
+              style={[pal.text, styles.headerSearchInput]}
+              onFocus={() => setIsInputFocused(true)}
+              onBlur={() => setIsInputFocused(false)}
+              onChangeText={onChangeQuery}
+            />
+          </View>
+          {query ? (
+            <View style={styles.headerCancelBtn}>
+              <TouchableOpacity onPress={onPressCancelSearch}>
+                <Text>Cancel</Text>
               </TouchableOpacity>
+            </View>
+          ) : undefined}
+        </View>
+        {query && autocompleteView.searchRes.length ? (
+          <>
+            {autocompleteView.searchRes.map(item => (
+              <ProfileCard
+                key={item.did}
+                handle={item.handle}
+                displayName={item.displayName}
+                avatar={item.avatar}
+              />
             ))}
+          </>
+        ) : query && !autocompleteView.searchRes.length ? (
+          <View>
+            <Text style={[pal.textLight, styles.searchPrompt]}>
+              No results found for {autocompleteView.prefix}
+            </Text>
+          </View>
+        ) : isInputFocused ? (
+          <View>
+            <Text style={[pal.textLight, styles.searchPrompt]}>
+              Search for users on the network
+            </Text>
+          </View>
+        ) : (
+          <ScrollView onScroll={Keyboard.dismiss}>
+            <WhoToFollow key={`wtf-${lastRenderTime}`} />
+            <SuggestedPosts key={`sp-${lastRenderTime}`} />
             <View style={s.footerSpacer} />
           </ScrollView>
-        ) : (
-          <SuggestedFollows asLinks />
         )}
-      </View>
-    </View>
+        <View style={s.footerSpacer} />
+      </ScrollView>
+    </TouchableWithoutFeedback>
   )
-}
+})
 
 const styles = StyleSheet.create({
   container: {
     flex: 1,
   },
 
-  inputContainer: {
+  header: {
     flexDirection: 'row',
-    paddingVertical: 16,
-    paddingHorizontal: 16,
-    borderTopWidth: 1,
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingTop: 4,
+    marginBottom: 14,
   },
-  inputIcon: {
-    marginRight: 10,
-    alignSelf: 'center',
+  headerMenuBtn: {
+    width: 40,
+    height: 30,
+    marginLeft: 6,
   },
-  input: {
+  headerSearchContainer: {
     flex: 1,
-    fontSize: 16,
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 30,
+    paddingHorizontal: 12,
+    paddingVertical: 8,
   },
-
-  outputContainer: {
+  headerSearchIcon: {
+    marginRight: 6,
+    alignSelf: 'center',
+  },
+  headerSearchInput: {
     flex: 1,
+    fontSize: 17,
+  },
+  headerCancelBtn: {
+    width: 60,
+    paddingLeft: 10,
   },
 
-  searchResult: {
-    flexDirection: 'row',
-    borderTopWidth: 1,
-    paddingVertical: 12,
-    paddingHorizontal: 16,
+  searchPrompt: {
+    textAlign: 'center',
+    paddingTop: 10,
   },
 })
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index c2953b59d..94f5acd93 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -7,17 +7,20 @@ import {
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {observer} from 'mobx-react-lite'
-import {useStores} from '../../state'
+import * as AppInfo from 'lib/app-info'
+import {useStores} from 'state/index'
 import {ScreenParams} from '../routes'
-import {s} from '../lib/styles'
+import {s} from 'lib/styles'
 import {ScrollView} from '../com/util/Views'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Link} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
 import * as Toast from '../com/util/Toast'
 import {UserAvatar} from '../com/util/UserAvatar'
-import {usePalette} from '../lib/hooks/usePalette'
-import {AccountData} from '../../state/models/session'
+import {usePalette} from 'lib/hooks/usePalette'
+import {AccountData} from 'state/models/session'
+import {useAnalytics} from 'lib/analytics'
+import {DeleteAccountModal} from 'state/models/shell-ui'
 
 export const Settings = observer(function Settings({
   navIdx,
@@ -25,9 +28,14 @@ export const Settings = observer(function Settings({
 }: ScreenParams) {
   const pal = usePalette('default')
   const store = useStores()
+  const {screen, track} = useAnalytics()
   const [isSwitching, setIsSwitching] = React.useState(false)
 
   useEffect(() => {
+    screen('Settings')
+  }, [screen])
+
+  useEffect(() => {
     if (!visible) {
       return
     }
@@ -36,22 +44,30 @@ export const Settings = observer(function Settings({
   }, [visible, store, navIdx])
 
   const onPressSwitchAccount = async (acct: AccountData) => {
+    track('Settings:SwitchAccountButtonClicked')
     setIsSwitching(true)
     if (await store.session.resumeSession(acct)) {
       setIsSwitching(false)
+      store.nav.tab.fixedTabReset()
       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()
     store.session.clear()
   }
   const onPressAddAccount = () => {
+    track('Settings:AddAccountButtonClicked')
     store.session.clear()
   }
   const onPressSignout = () => {
+    track('Settings:SignOutButtonClicked')
     store.session.logout()
   }
+  const onPressDeleteAccount = () => {
+    store.shell.openModal(new DeleteAccountModal())
+  }
 
   return (
     <View style={[s.h100pct]} testID="settingsScreen">
@@ -143,22 +159,34 @@ export const Settings = observer(function Settings({
               </Text>
             </View>
           </TouchableOpacity>
+
           <View style={styles.spacer} />
           <Text type="sm-medium" style={[s.mb5]}>
+            Danger zone
+          </Text>
+          <TouchableOpacity
+            style={[pal.view, s.p10, s.mb10]}
+            onPress={onPressDeleteAccount}>
+            <Text style={pal.textLight}>Delete my account</Text>
+          </TouchableOpacity>
+          <Text type="sm-medium" style={[s.mt10, s.mb5]}>
             Developer tools
           </Text>
           <Link
             style={[pal.view, s.p10, s.mb2]}
             href="/sys/log"
             title="System log">
-            <Text style={pal.link}>System log</Text>
+            <Text style={pal.textLight}>System log</Text>
           </Link>
           <Link
             style={[pal.view, s.p10, s.mb2]}
             href="/sys/debug"
             title="Debug tools">
-            <Text style={pal.link}>Storybook</Text>
+            <Text style={pal.textLight}>Storybook</Text>
           </Link>
+          <Text type="sm" style={[s.mt10, pal.textLight]}>
+            Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
+          </Text>
           <View style={s.footerSpacer} />
         </View>
       </ScrollView>