about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-09-18 11:44:29 -0700
committerGitHub <noreply@github.com>2023-09-18 11:44:29 -0700
commitea885339cf3a5cba4aa82fbe5e0176052c3b68e1 (patch)
treea02b0581c42a1a0aae4442a75391c99a1719ec3e /src/view
parent3118e3e93338c62d2466699b9f339544d3273823 (diff)
downloadvoidsky-ea885339cf3a5cba4aa82fbe5e0176052c3b68e1.tar.zst
Feed UI update working branch [WIP] (#1420)
* Feeds navigation on right side of desktop (#1403)

* Remove home feed header on desktop

* Add feeds to right sidebar

* Add simple non-moving header to desktop

* Improve loading state of custom feed header

* Remove log

Co-authored-by: Eric Bailey <git@esb.lol>

* Remove dead comment

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Redesign feeds tab (#1439)

* consolidate saved feeds and discover into one screen

* Add hoverStyle behavior to <Link>

* More UI work on SavedFeeds

* Replace satellite icon with a hashtag

* Tune My Feeds mobile ui

* Handle no results in my feeds

* Remove old DiscoverFeeds screen

* Remove multifeed

* Remove DiscoverFeeds from router

* Improve loading placeholders

* Small fixes

* Fix types

* Fix overflow issue on firefox

* Add icons prompting to open feeds

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Merge feed prototype [WIP] (#1398)

* POC WIP for the mergefeed

* Add feed API wrapper and move mergefeed into it

* Show feed source in mergefeed

* Add lodash.random dep

* Improve mergefeed sampling and reliability

* Tune source ui element

* Improve mergefeed edge condition handling

* Remove in-place update of feeds for performance

* Fix link on native

* Fix bad ref

* Improve variety in mergefeed sampling

* Fix types

* Fix rebase error

* Add missing source field (got dropped in merge)

* Update find more link

* Simplify the right hand feeds nav

* Bring back load latest button on desktop & unify impl

* Add 'From' to source

* Add simple headers to desktop home & notifications

* Fix thread view jumping around horizontally

* Add unread indicators to desktop headers

* Add home feed preference for enabling the mergefeed

* Add a preference for showing replies among followed users only (#1448)

* Add a preference for showing replies among followed users only

* Simplify the reply filter UI

* Fix typo

* Simplified custom feed header

* Add soft reset to custom feed screen

* Drop all the in-post translate links except when expanded (#1455)

* Update mobile feed settings links to match desktop

* Fixes to feeds screen loading states

* Bolder active state of feeds tab on mobile web

* Fix dark mode issue

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Ansh <anshnanda10@gmail.com>
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/notifications/Feed.tsx5
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx8
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx13
-rw-r--r--src/view/com/post-thread/PostThread.tsx2
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx9
-rw-r--r--src/view/com/post/Post.tsx22
-rw-r--r--src/view/com/posts/FeedItem.tsx57
-rw-r--r--src/view/com/posts/FeedSlice.tsx2
-rw-r--r--src/view/com/posts/FollowingEmptyState.tsx2
-rw-r--r--src/view/com/posts/MultiFeed.tsx256
-rw-r--r--src/view/com/util/Link.tsx15
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx54
-rw-r--r--src/view/com/util/SimpleViewHeader.tsx105
-rw-r--r--src/view/com/util/UserAvatar.tsx2
-rw-r--r--src/view/com/util/forms/SearchInput.tsx104
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.tsx87
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.web.tsx109
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtnMobile.tsx69
-rw-r--r--src/view/index.ts24
-rw-r--r--src/view/screens/CustomFeed.tsx391
-rw-r--r--src/view/screens/DiscoverFeeds.tsx157
-rw-r--r--src/view/screens/Feeds.tsx320
-rw-r--r--src/view/screens/Home.tsx87
-rw-r--r--src/view/screens/Notifications.tsx57
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx66
-rw-r--r--src/view/screens/Profile.tsx4
-rw-r--r--src/view/screens/SavedFeeds.tsx2
-rw-r--r--src/view/screens/Settings.tsx11
-rw-r--r--src/view/shell/Drawer.tsx15
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx19
-rw-r--r--src/view/shell/bottom-bar/BottomBarStyles.tsx3
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx12
-rw-r--r--src/view/shell/desktop/Feeds.tsx92
-rw-r--r--src/view/shell/desktop/LeftNav.tsx13
-rw-r--r--src/view/shell/desktop/RightNav.tsx5
-rw-r--r--src/view/shell/desktop/Search.tsx1
36 files changed, 1127 insertions, 1073 deletions
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index d457d7136..4ca22282d 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -21,11 +21,13 @@ export const Feed = observer(function Feed({
   scrollElRef,
   onPressTryAgain,
   onScroll,
+  ListHeaderComponent,
 }: {
   view: NotificationsFeedModel
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
   onScroll?: OnScrollCb
+  ListHeaderComponent?: () => JSX.Element
 }) {
   const pal = usePalette('default')
   const [isPTRing, setIsPTRing] = React.useState(false)
@@ -142,6 +144,7 @@ export const Feed = observer(function Feed({
           data={data}
           keyExtractor={item => item._reactKey}
           renderItem={renderItem}
+          ListHeaderComponent={ListHeaderComponent}
           ListFooterComponent={FeedFooter}
           refreshControl={
             <RefreshControl
@@ -156,6 +159,8 @@ export const Feed = observer(function Feed({
           onScroll={onScroll}
           scrollEventThrottle={100}
           contentContainerStyle={s.contentContainer}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
         />
       ) : null}
     </View>
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index 0083e953b..02aa623cc 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -12,15 +12,17 @@ import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
 export const FeedsTabBar = observer(function FeedsTabBarImpl(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
-  const {isMobile} = useWebMediaQueries()
+  const {isMobile, isTablet} = useWebMediaQueries()
   if (isMobile) {
     return <FeedsTabBarMobile {...props} />
+  } else if (isTablet) {
+    return <FeedsTabBarTablet {...props} />
   } else {
-    return <FeedsTabBarDesktop {...props} />
+    return null
   }
 })
 
-const FeedsTabBarDesktop = observer(function FeedsTabBarDesktopImpl(
+const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
   const store = useStores()
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 5ce2906b3..30a712541 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -9,8 +9,8 @@ import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
-import {CogIcon} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {s} from 'lib/styles'
 import {HITSLOP_10} from 'lib/constants'
 
@@ -67,12 +67,15 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
         </Text>
         <View style={[pal.view]}>
           <Link
-            href="/settings/saved-feeds"
+            href="/settings/home-feed"
             hitSlop={HITSLOP_10}
             accessibilityRole="button"
-            accessibilityLabel="Edit Saved Feeds"
-            accessibilityHint="Opens screen to edit Saved Feeds">
-            <CogIcon size={21} strokeWidth={2} style={pal.textLight} />
+            accessibilityLabel="Home Feed Preferences"
+            accessibilityHint="">
+            <FontAwesomeIcon
+              icon="sliders"
+              style={pal.textLight as FontAwesomeIconStyle}
+            />
           </Link>
         </View>
       </View>
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 7a5a45771..1cc177d17 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -357,6 +357,8 @@ export const PostThread = observer(function PostThread({
       }
       onScrollToIndexFailed={onScrollToIndexFailed}
       style={s.hContentRegion}
+      // @ts-ignore our .web version only -prf
+      desktopFixedHeight
     />
   )
 })
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 5b5fee0ca..37c7ece47 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -483,15 +483,6 @@ export const PostThreadItem = observer(function PostThreadItem({
                   />
                 </ContentHider>
               )}
-              {needsTranslation && (
-                <View style={[pal.borderDark, styles.translateLink]}>
-                  <Link href={translatorUrl} title="Translate">
-                    <Text type="sm" style={pal.link}>
-                      Translate this post
-                    </Text>
-                  </Link>
-                </View>
-              )}
               <PostCtrls
                 itemUri={itemUri}
                 itemCid={itemCid}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 0855f25bf..d7559e3c4 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useMemo} from 'react'
+import React, {useState} from 'react'
 import {
   ActivityIndicator,
   Linking,
@@ -28,7 +28,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
+import {getTranslatorLink} from '../../../locale/helpers'
 import {makeProfileLink} from 'lib/routes/links'
 
 export const Post = observer(function PostImpl({
@@ -116,12 +116,6 @@ const PostLoaded = observer(function PostLoadedImpl({
   }
 
   const translatorUrl = getTranslatorLink(record?.text || '')
-  const needsTranslation = useMemo(
-    () =>
-      store.preferences.contentLanguages.length > 0 &&
-      !isPostInLanguage(item.post, store.preferences.contentLanguages),
-    [item.post, store.preferences.contentLanguages],
-  )
 
   const onPressReply = React.useCallback(() => {
     store.shell.openComposer({
@@ -256,15 +250,6 @@ const PostLoaded = observer(function PostLoadedImpl({
                 />
               </ContentHider>
             ) : null}
-            {needsTranslation && (
-              <View style={[pal.borderDark, styles.translateLink]}>
-                <Link href={translatorUrl} title="Translate">
-                  <Text type="sm" style={pal.link}>
-                    Translate this post
-                  </Text>
-                </Link>
-              </View>
-            )}
           </ContentHider>
           <PostCtrls
             itemUri={itemUri}
@@ -322,9 +307,6 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     flexWrap: 'wrap',
   },
-  translateLink: {
-    marginBottom: 12,
-  },
   replyLine: {
     position: 'absolute',
     left: 36,
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index bc7b7a7e6..59ab28d72 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -8,6 +8,7 @@ import {
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
 import {PostsFeedItemModel} from 'state/models/feeds/post'
+import {FeedSourceInfo} from 'lib/api/feed/types'
 import {Link, DesktopWebTextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
@@ -26,17 +27,19 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
+import {getTranslatorLink} from '../../../locale/helpers'
 import {makeProfileLink} from 'lib/routes/links'
 import {isEmbedByEmbedder} from 'lib/embeds'
 
 export const FeedItem = observer(function FeedItemImpl({
   item,
+  source,
   isThreadChild,
   isThreadLastChild,
   isThreadParent,
 }: {
   item: PostsFeedItemModel
+  source?: FeedSourceInfo
   isThreadChild?: boolean
   isThreadLastChild?: boolean
   isThreadParent?: boolean
@@ -62,12 +65,6 @@ export const FeedItem = observer(function FeedItemImpl({
     return urip.hostname
   }, [record?.reply])
   const translatorUrl = getTranslatorLink(record?.text || '')
-  const needsTranslation = useMemo(
-    () =>
-      store.preferences.contentLanguages.length > 0 &&
-      !isPostInLanguage(item.post, store.preferences.contentLanguages),
-    [item.post, store.preferences.contentLanguages],
-  )
 
   const onPressReply = React.useCallback(() => {
     track('FeedItem:PostReply')
@@ -179,7 +176,27 @@ export const FeedItem = observer(function FeedItemImpl({
         </View>
 
         <View style={{paddingTop: 12}}>
-          {item.reasonRepost && (
+          {source ? (
+            <Link
+              title={sanitizeDisplayName(source.displayName)}
+              href={source.uri}>
+              <Text
+                type="sm-bold"
+                style={pal.textLight}
+                lineHeight={1.2}
+                numberOfLines={1}>
+                From{' '}
+                <DesktopWebTextLink
+                  type="sm-bold"
+                  style={pal.textLight}
+                  lineHeight={1.2}
+                  numberOfLines={1}
+                  text={sanitizeDisplayName(source.displayName)}
+                  href={source.uri}
+                />
+              </Text>
+            </Link>
+          ) : item.reasonRepost ? (
             <Link
               style={styles.includeReason}
               href={makeProfileLink(item.reasonRepost.by)}
@@ -188,10 +205,10 @@ export const FeedItem = observer(function FeedItemImpl({
               )}>
               <FontAwesomeIcon
                 icon="retweet"
-                style={[
-                  styles.includeReasonIcon,
-                  {color: pal.colors.textLight} as FontAwesomeIconStyle,
-                ]}
+                style={{
+                  marginRight: 4,
+                  color: pal.colors.textLight,
+                }}
               />
               <Text
                 type="sm-bold"
@@ -212,7 +229,7 @@ export const FeedItem = observer(function FeedItemImpl({
                 />
               </Text>
             </Link>
-          )}
+          ) : null}
         </View>
       </View>
 
@@ -304,15 +321,6 @@ export const FeedItem = observer(function FeedItemImpl({
                 />
               </ContentHider>
             ) : null}
-            {needsTranslation && (
-              <View style={[pal.borderDark, styles.translateLink]}>
-                <Link href={translatorUrl} title="Translate">
-                  <Text type="sm" style={pal.link}>
-                    Translate this post
-                  </Text>
-                </Link>
-              </View>
-            )}
           </ContentHider>
           <PostCtrls
             itemUri={itemUri}
@@ -362,12 +370,9 @@ const styles = StyleSheet.create({
   includeReason: {
     flexDirection: 'row',
     marginTop: 2,
-    marginBottom: 4,
+    marginBottom: 2,
     marginLeft: -20,
   },
-  includeReasonIcon: {
-    marginRight: 4,
-  },
   layout: {
     flexDirection: 'row',
     marginTop: 1,
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 47313ee27..1d26f6cbd 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -28,6 +28,7 @@ export const FeedSlice = observer(function FeedSliceImpl({
         <FeedItem
           key={slice.items[0]._reactKey}
           item={slice.items[0]}
+          source={slice.source}
           isThreadParent={slice.isThreadParentAt(0)}
           isThreadChild={slice.isThreadChildAt(0)}
         />
@@ -55,6 +56,7 @@ export const FeedSlice = observer(function FeedSliceImpl({
         <FeedItem
           key={item._reactKey}
           item={item}
+          source={i === 0 ? slice.source : undefined}
           isThreadParent={slice.isThreadParentAt(i)}
           isThreadChild={slice.isThreadChildAt(i)}
           isThreadLastChild={
diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx
index 4491b2526..a73ffb68b 100644
--- a/src/view/com/posts/FollowingEmptyState.tsx
+++ b/src/view/com/posts/FollowingEmptyState.tsx
@@ -28,7 +28,7 @@ export function FollowingEmptyState() {
   }, [navigation])
 
   const onPressDiscoverFeeds = React.useCallback(() => {
-    navigation.navigate('DiscoverFeeds')
+    navigation.navigate('Feeds')
   }, [navigation])
 
   return (
diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
deleted file mode 100644
index 9c8f4f246..000000000
--- a/src/view/com/posts/MultiFeed.tsx
+++ /dev/null
@@ -1,256 +0,0 @@
-import React, {MutableRefObject} from 'react'
-import {observer} from 'mobx-react-lite'
-import {
-  ActivityIndicator,
-  RefreshControl,
-  StyleProp,
-  StyleSheet,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {FlatList} from '../util/Views'
-import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed'
-import {FeedSlice} from './FeedSlice'
-import {Text} from '../util/text/Text'
-import {Link} from '../util/Link'
-import {UserAvatar} from '../util/UserAvatar'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
-import {s} from 'lib/styles'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useTheme} from 'lib/ThemeContext'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {CogIcon} from 'lib/icons'
-
-export const MultiFeed = observer(function Feed({
-  multifeed,
-  style,
-  scrollElRef,
-  onScroll,
-  scrollEventThrottle,
-  testID,
-  headerOffset = 0,
-  extraData,
-}: {
-  multifeed: PostsMultiFeedModel
-  style?: StyleProp<ViewStyle>
-  scrollElRef?: MutableRefObject<FlatList<any> | null>
-  onPressTryAgain?: () => void
-  onScroll?: OnScrollCb
-  scrollEventThrottle?: number
-  renderEmptyState?: () => JSX.Element
-  testID?: string
-  headerOffset?: number
-  extraData?: any
-}) {
-  const pal = usePalette('default')
-  const theme = useTheme()
-  const {isMobile} = useWebMediaQueries()
-  const {track} = useAnalytics()
-  const [isRefreshing, setIsRefreshing] = React.useState(false)
-
-  // events
-  // =
-
-  const onRefresh = React.useCallback(async () => {
-    track('MultiFeed:onRefresh')
-    setIsRefreshing(true)
-    try {
-      await multifeed.refresh()
-    } catch (err) {
-      multifeed.rootStore.log.error('Failed to refresh posts feed', err)
-    }
-    setIsRefreshing(false)
-  }, [multifeed, track, setIsRefreshing])
-
-  const onEndReached = React.useCallback(async () => {
-    track('MultiFeed:onEndReached')
-    try {
-      await multifeed.loadMore()
-    } catch (err) {
-      multifeed.rootStore.log.error('Failed to load more posts', err)
-    }
-  }, [multifeed, track])
-
-  // rendering
-  // =
-
-  const renderItem = React.useCallback(
-    ({item}: {item: MultiFeedItem}) => {
-      if (item.type === 'header') {
-        if (!isMobile) {
-          return (
-            <>
-              <View style={[pal.view, pal.border, styles.headerDesktop]}>
-                <Text type="2xl-bold" style={pal.text}>
-                  My Feeds
-                </Text>
-                <Link href="/settings/saved-feeds">
-                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
-                </Link>
-              </View>
-              <DiscoverLink />
-            </>
-          )
-        }
-        return (
-          <>
-            <View style={[styles.header, pal.border]} />
-            <DiscoverLink />
-          </>
-        )
-      } else if (item.type === 'feed-header') {
-        return (
-          <View style={styles.feedHeader}>
-            <UserAvatar type="algo" avatar={item.avatar} size={28} />
-            <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}>
-              {item.title}
-            </Text>
-          </View>
-        )
-      } else if (item.type === 'feed-slice') {
-        return <FeedSlice slice={item.slice} />
-      } else if (item.type === 'feed-loading') {
-        return <PostFeedLoadingPlaceholder />
-      } else if (item.type === 'feed-error') {
-        return <ErrorMessage message={item.error} />
-      } else if (item.type === 'feed-footer') {
-        return (
-          <Link
-            href={item.uri}
-            style={[styles.feedFooter, pal.border, pal.view]}>
-            <Text type="lg" style={pal.link}>
-              See more from {item.title}
-            </Text>
-            <FontAwesomeIcon
-              icon="angle-right"
-              size={18}
-              color={pal.colors.link}
-            />
-          </Link>
-        )
-      } else if (item.type === 'footer') {
-        return <DiscoverLink />
-      }
-      return null
-    },
-    [pal, isMobile],
-  )
-
-  const ListFooter = React.useCallback(
-    () =>
-      multifeed.isLoading && !isRefreshing ? (
-        <View style={styles.loadMore}>
-          <ActivityIndicator color={pal.colors.text} />
-        </View>
-      ) : (
-        <View />
-      ),
-    [multifeed.isLoading, isRefreshing, pal],
-  )
-
-  return (
-    <View testID={testID} style={style}>
-      {multifeed.items.length > 0 && (
-        <FlatList
-          testID={testID ? `${testID}-flatlist` : undefined}
-          ref={scrollElRef}
-          data={multifeed.items}
-          keyExtractor={item => item._reactKey}
-          renderItem={renderItem}
-          ListFooterComponent={ListFooter}
-          refreshControl={
-            <RefreshControl
-              refreshing={isRefreshing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-              progressViewOffset={headerOffset}
-            />
-          }
-          contentContainerStyle={s.contentContainer}
-          style={[{paddingTop: headerOffset}, pal.view, styles.container]}
-          onScroll={onScroll}
-          scrollEventThrottle={scrollEventThrottle}
-          indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
-          onEndReached={onEndReached}
-          onEndReachedThreshold={0.6}
-          removeClippedSubviews={true}
-          contentOffset={{x: 0, y: headerOffset * -1}}
-          extraData={extraData}
-          // @ts-ignore our .web version only -prf
-          desktopFixedHeight
-        />
-      )}
-    </View>
-  )
-})
-
-function DiscoverLink() {
-  const pal = usePalette('default')
-  return (
-    <Link style={[styles.discoverLink, pal.viewLight]} href="/search/feeds">
-      <FontAwesomeIcon icon="search" size={18} color={pal.colors.text} />
-      <Text type="xl-medium" style={pal.text}>
-        Discover new feeds
-      </Text>
-    </Link>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    height: '100%',
-  },
-  header: {
-    borderTopWidth: 1,
-    marginBottom: 4,
-  },
-  headerDesktop: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'space-between',
-    borderBottomWidth: 1,
-    marginBottom: 4,
-    paddingHorizontal: 16,
-    paddingVertical: 8,
-  },
-  feedHeader: {
-    flexDirection: 'row',
-    gap: 8,
-    alignItems: 'center',
-    paddingHorizontal: 16,
-    paddingBottom: 8,
-    marginTop: 12,
-  },
-  feedHeaderTitle: {
-    fontWeight: 'bold',
-  },
-  feedFooter: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    paddingHorizontal: 16,
-    paddingVertical: 16,
-    marginBottom: 12,
-    borderTopWidth: 1,
-    borderBottomWidth: 1,
-  },
-  discoverLink: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    borderRadius: 8,
-    paddingHorizontal: 14,
-    paddingVertical: 12,
-    marginHorizontal: 8,
-    marginVertical: 8,
-    gap: 8,
-  },
-  loadMore: {
-    paddingTop: 10,
-  },
-})
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 321b6ab63..d4df2bec4 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -26,6 +26,7 @@ import {useStores, RootStoreModel} from 'state/index'
 import {convertBskyAppUrlIfNeeded, isExternalUrl} from 'lib/strings/url-helpers'
 import {isAndroid, isDesktopWeb} from 'platform/detection'
 import {sanitizeUrl} from '@braintree/sanitize-url'
+import {PressableWithHover} from './PressableWithHover'
 import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
 
 type Event =
@@ -38,6 +39,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
   href?: string
   title?: string
   children?: React.ReactNode
+  hoverStyle?: StyleProp<ViewStyle>
   noFeedback?: boolean
   asAnchor?: boolean
   anchorNoUnderline?: boolean
@@ -112,8 +114,9 @@ export const Link = observer(function Link({
     props.accessibilityLabel = title
   }
 
+  const Com = props.hoverStyle ? PressableWithHover : Pressable
   return (
-    <Pressable
+    <Com
       testID={testID}
       style={style}
       onPress={onPress}
@@ -123,7 +126,7 @@ export const Link = observer(function Link({
       href={asAnchor ? sanitizeUrl(href) : undefined}
       {...props}>
       {children ? children : <Text>{title || 'link'}</Text>}
-    </Pressable>
+    </Com>
   )
 })
 
@@ -137,6 +140,7 @@ export const TextLink = observer(function TextLink({
   lineHeight,
   dataSet,
   title,
+  onPress,
 }: {
   testID?: string
   type?: TypographyVariant
@@ -154,9 +158,14 @@ export const TextLink = observer(function TextLink({
 
   props.onPress = React.useCallback(
     (e?: Event) => {
+      if (onPress) {
+        e?.preventDefault?.()
+        // @ts-ignore function signature differs by platform -prf
+        return onPress()
+      }
       return onPressInner(store, navigation, sanitizeUrl(href), e)
     },
-    [store, navigation, href],
+    [onPress, store, navigation, href],
   )
   const hrefAttrs = useMemo(() => {
     const isExternal = isExternalUrl(href)
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index bf39fd50c..d7ab1be54 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -174,6 +174,60 @@ export function ProfileCardFeedLoadingPlaceholder() {
   )
 }
 
+export function FeedLoadingPlaceholder({
+  style,
+}: {
+  style?: StyleProp<ViewStyle>
+}) {
+  const pal = usePalette('default')
+  return (
+    <View
+      style={[
+        {paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
+        pal.border,
+        style,
+      ]}>
+      <View style={[pal.view, {flexDirection: 'row', marginBottom: 10}]}>
+        <LoadingPlaceholder
+          width={36}
+          height={36}
+          style={[styles.avatar, {borderRadius: 6}]}
+        />
+        <View style={[s.flex1]}>
+          <LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
+          <LoadingPlaceholder width={120} height={8} />
+        </View>
+      </View>
+      <View style={{paddingHorizontal: 5}}>
+        <LoadingPlaceholder
+          width={260}
+          height={8}
+          style={{marginVertical: 12}}
+        />
+        <LoadingPlaceholder width={120} height={8} />
+      </View>
+    </View>
+  )
+}
+
+export function FeedFeedLoadingPlaceholder() {
+  return (
+    <>
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+      <FeedLoadingPlaceholder />
+    </>
+  )
+}
+
 const styles = StyleSheet.create({
   loadingPlaceholder: {
     borderRadius: 6,
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
new file mode 100644
index 000000000..4eff38a31
--- /dev/null
+++ b/src/view/com/util/SimpleViewHeader.tsx
@@ -0,0 +1,105 @@
+import React from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  StyleProp,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useNavigation} from '@react-navigation/native'
+import {CenteredView} from './Views'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {NavigationProp} from 'lib/routes/types'
+
+const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
+
+export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({
+  showBackButton = true,
+  style,
+  children,
+}: React.PropsWithChildren<{
+  showBackButton?: boolean
+  style?: StyleProp<ViewStyle>
+}>) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const navigation = useNavigation<NavigationProp>()
+  const {track} = useAnalytics()
+  const {isMobile} = useWebMediaQueries()
+  const canGoBack = navigation.canGoBack()
+
+  const onPressBack = React.useCallback(() => {
+    if (navigation.canGoBack()) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('Home')
+    }
+  }, [navigation])
+
+  const onPressMenu = React.useCallback(() => {
+    track('ViewHeader:MenuButtonClicked')
+    store.shell.openDrawer()
+  }, [track, store])
+
+  const Container = isMobile ? View : CenteredView
+  return (
+    <Container style={[styles.header, isMobile && styles.headerMobile, style]}>
+      {showBackButton ? (
+        <TouchableOpacity
+          testID="viewHeaderDrawerBtn"
+          onPress={canGoBack ? onPressBack : onPressMenu}
+          hitSlop={BACK_HITSLOP}
+          style={canGoBack ? styles.backBtn : styles.backBtnWide}
+          accessibilityRole="button"
+          accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
+          accessibilityHint="">
+          {canGoBack ? (
+            <FontAwesomeIcon
+              size={18}
+              icon="angle-left"
+              style={[styles.backIcon, pal.text]}
+            />
+          ) : (
+            <FontAwesomeIcon
+              size={18}
+              icon="bars"
+              style={[styles.backIcon, pal.textLight]}
+            />
+          )}
+        </TouchableOpacity>
+      ) : null}
+      {children}
+    </Container>
+  )
+})
+
+const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 18,
+    paddingVertical: 12,
+    width: '100%',
+  },
+  headerMobile: {
+    paddingHorizontal: 12,
+    paddingVertical: 10,
+  },
+  backBtn: {
+    width: 30,
+    height: 30,
+  },
+  backBtnWide: {
+    width: 30,
+    height: 30,
+    paddingHorizontal: 6,
+  },
+  backIcon: {
+    marginTop: 6,
+  },
+})
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 0f34f75aa..7a42ab4d3 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -118,7 +118,7 @@ export function UserAvatar({
       return {
         width: size,
         height: size,
-        borderRadius: 8,
+        borderRadius: size > 32 ? 8 : 3,
       }
     }
     return {
diff --git a/src/view/com/util/forms/SearchInput.tsx b/src/view/com/util/forms/SearchInput.tsx
new file mode 100644
index 000000000..c1eb82bd4
--- /dev/null
+++ b/src/view/com/util/forms/SearchInput.tsx
@@ -0,0 +1,104 @@
+import React from 'react'
+import {
+  StyleProp,
+  StyleSheet,
+  TextInput,
+  TouchableOpacity,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {MagnifyingGlassIcon} from 'lib/icons'
+import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface Props {
+  query: string
+  setIsInputFocused?: (v: boolean) => void
+  onChangeQuery: (v: string) => void
+  onPressCancelSearch: () => void
+  onSubmitQuery: () => void
+  style?: StyleProp<ViewStyle>
+}
+export function SearchInput({
+  query,
+  setIsInputFocused,
+  onChangeQuery,
+  onPressCancelSearch,
+  onSubmitQuery,
+  style,
+}: Props) {
+  const theme = useTheme()
+  const pal = usePalette('default')
+  const textInput = React.useRef<TextInput>(null)
+
+  const onPressCancelSearchInner = React.useCallback(() => {
+    onPressCancelSearch()
+    textInput.current?.blur()
+  }, [onPressCancelSearch, textInput])
+
+  return (
+    <View style={[pal.viewLight, styles.container, style]}>
+      <MagnifyingGlassIcon style={[pal.icon, styles.icon]} size={21} />
+      <TextInput
+        testID="searchTextInput"
+        ref={textInput}
+        placeholder="Search"
+        placeholderTextColor={pal.colors.textLight}
+        selectTextOnFocus
+        returnKeyType="search"
+        value={query}
+        style={[pal.text, styles.input]}
+        keyboardAppearance={theme.colorScheme}
+        onFocus={() => setIsInputFocused?.(true)}
+        onBlur={() => setIsInputFocused?.(false)}
+        onChangeText={onChangeQuery}
+        onSubmitEditing={onSubmitQuery}
+        accessibilityRole="search"
+        accessibilityLabel="Search"
+        accessibilityHint=""
+        autoCorrect={false}
+        autoCapitalize="none"
+      />
+      {query ? (
+        <TouchableOpacity
+          onPress={onPressCancelSearchInner}
+          accessibilityRole="button"
+          accessibilityLabel="Clear search query"
+          accessibilityHint="">
+          <FontAwesomeIcon
+            icon="xmark"
+            size={16}
+            style={pal.textLight as FontAwesomeIconStyle}
+          />
+        </TouchableOpacity>
+      ) : undefined}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 30,
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+  },
+  icon: {
+    marginRight: 6,
+    alignSelf: 'center',
+  },
+  input: {
+    flex: 1,
+    fontSize: 17,
+    minWidth: 0, // overflow mitigation for firefox
+  },
+  cancelBtn: {
+    paddingLeft: 10,
+  },
+})
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index ae9cb9361..6b73edd4b 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -1 +1,86 @@
-export * from './LoadLatestBtnMobile'
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {clamp} from 'lodash'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {colors} from 'lib/styles'
+import {HITSLOP_20} from 'lib/constants'
+
+export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
+  onPress,
+  label,
+  showIndicator,
+}: {
+  onPress: () => void
+  label: string
+  showIndicator: boolean
+  minimalShellMode?: boolean // NOTE not used on mobile -prf
+}) {
+  const store = useStores()
+  const pal = usePalette('default')
+  const {isDesktop, isTablet, isMobile} = useWebMediaQueries()
+  const safeAreaInsets = useSafeAreaInsets()
+  return (
+    <TouchableOpacity
+      style={[
+        styles.loadLatest,
+        isDesktop && styles.loadLatestDesktop,
+        isTablet && styles.loadLatestTablet,
+        pal.borderDark,
+        pal.view,
+        isMobile &&
+          !store.shell.minimalShellMode && {
+            bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
+          },
+      ]}
+      onPress={onPress}
+      hitSlop={HITSLOP_20}
+      accessibilityRole="button"
+      accessibilityLabel={label}
+      accessibilityHint="">
+      <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
+      {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
+    </TouchableOpacity>
+  )
+})
+
+const styles = StyleSheet.create({
+  loadLatest: {
+    position: 'absolute',
+    left: 18,
+    bottom: 35,
+    borderWidth: 1,
+    width: 52,
+    height: 52,
+    borderRadius: 26,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  loadLatestTablet: {
+    // @ts-ignore web only
+    left: '50vw',
+    // @ts-ignore web only -prf
+    transform: 'translateX(-282px)',
+  },
+  loadLatestDesktop: {
+    // @ts-ignore web only
+    left: '50vw',
+    // @ts-ignore web only -prf
+    transform: 'translateX(-382px)',
+  },
+  indicator: {
+    position: 'absolute',
+    top: 3,
+    right: 3,
+    backgroundColor: colors.blue3,
+    width: 12,
+    height: 12,
+    borderRadius: 6,
+    borderWidth: 1,
+  },
+})
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
deleted file mode 100644
index 83c696f7e..000000000
--- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile'
-import {HITSLOP_20} from 'lib/constants'
-
-export const LoadLatestBtn = ({
-  onPress,
-  label,
-  showIndicator,
-  minimalShellMode,
-}: {
-  onPress: () => void
-  label: string
-  showIndicator: boolean
-  minimalShellMode?: boolean
-}) => {
-  const pal = usePalette('default')
-  const {isMobile} = useWebMediaQueries()
-  if (isMobile) {
-    return (
-      <LoadLatestBtnMobile
-        onPress={onPress}
-        label={label}
-        showIndicator={showIndicator}
-      />
-    )
-  }
-  return (
-    <>
-      {showIndicator && (
-        <TouchableOpacity
-          style={[
-            pal.view,
-            pal.borderDark,
-            styles.loadLatestCentered,
-            minimalShellMode && styles.loadLatestCenteredMinimal,
-          ]}
-          onPress={onPress}
-          hitSlop={HITSLOP_20}
-          accessibilityRole="button"
-          accessibilityLabel={label}
-          accessibilityHint="">
-          <Text type="md-bold" style={pal.text}>
-            {label}
-          </Text>
-        </TouchableOpacity>
-      )}
-      <TouchableOpacity
-        style={[pal.view, pal.borderDark, styles.loadLatest]}
-        onPress={onPress}
-        hitSlop={HITSLOP_20}
-        accessibilityRole="button"
-        accessibilityLabel={label}
-        accessibilityHint="">
-        <Text type="md-bold" style={pal.text}>
-          <FontAwesomeIcon
-            icon="angle-up"
-            size={21}
-            style={[pal.text, styles.icon]}
-          />
-        </Text>
-      </TouchableOpacity>
-    </>
-  )
-}
-
-const styles = StyleSheet.create({
-  loadLatest: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    position: 'absolute',
-    // @ts-ignore web only
-    left: '50vw',
-    // @ts-ignore web only -prf
-    transform: 'translateX(-282px)',
-    bottom: 40,
-    width: 54,
-    height: 54,
-    borderRadius: 30,
-    borderWidth: 1,
-  },
-  icon: {
-    position: 'relative',
-    top: 2,
-  },
-  loadLatestCentered: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-    position: 'absolute',
-    // @ts-ignore web only
-    left: '50vw',
-    // @ts-ignore web only -prf
-    transform: 'translateX(-50%)',
-    top: 60,
-    paddingHorizontal: 24,
-    paddingVertical: 14,
-    borderRadius: 30,
-    borderWidth: 1,
-  },
-  loadLatestCenteredMinimal: {
-    top: 20,
-  },
-})
diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
deleted file mode 100644
index 3e8add5e9..000000000
--- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {clamp} from 'lodash'
-import {useStores} from 'state/index'
-import {usePalette} from 'lib/hooks/usePalette'
-import {colors} from 'lib/styles'
-import {HITSLOP_20} from 'lib/constants'
-
-export const LoadLatestBtn = observer(function LoadLatestBtnImpl({
-  onPress,
-  label,
-  showIndicator,
-}: {
-  onPress: () => void
-  label: string
-  showIndicator: boolean
-  minimalShellMode?: boolean // NOTE not used on mobile -prf
-}) {
-  const store = useStores()
-  const pal = usePalette('default')
-  const safeAreaInsets = useSafeAreaInsets()
-  return (
-    <TouchableOpacity
-      style={[
-        styles.loadLatest,
-        pal.borderDark,
-        pal.view,
-        !store.shell.minimalShellMode && {
-          bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30),
-        },
-      ]}
-      onPress={onPress}
-      hitSlop={HITSLOP_20}
-      accessibilityRole="button"
-      accessibilityLabel={label}
-      accessibilityHint="">
-      <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
-      {showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
-    </TouchableOpacity>
-  )
-})
-
-const styles = StyleSheet.create({
-  loadLatest: {
-    position: 'absolute',
-    left: 18,
-    bottom: 35,
-    borderWidth: 1,
-    width: 52,
-    height: 52,
-    borderRadius: 26,
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-  indicator: {
-    position: 'absolute',
-    top: 3,
-    right: 3,
-    backgroundColor: colors.blue3,
-    width: 12,
-    height: 12,
-    borderRadius: 6,
-    borderWidth: 1,
-  },
-})
diff --git a/src/view/index.ts b/src/view/index.ts
index 2e4c08ec7..2fdc34e7b 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -13,6 +13,7 @@ import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrow
 import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
 import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
 import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft'
+import {faArrowTrendUp} from '@fortawesome/free-solid-svg-icons/faArrowTrendUp'
 import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
 import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
 import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
@@ -24,6 +25,7 @@ import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faB
 import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar'
 import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera'
 import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
+import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
 import {faCircle} from '@fortawesome/free-regular-svg-icons/faCircle'
 import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
 import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck'
@@ -41,6 +43,7 @@ import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation'
 import {faEye} from '@fortawesome/free-solid-svg-icons/faEye'
 import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash'
 import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile'
+import {faFire} from '@fortawesome/free-solid-svg-icons/faFire'
 import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk'
 import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
 import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
@@ -54,15 +57,18 @@ import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
 import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo'
 import {faLanguage} from '@fortawesome/free-solid-svg-icons/faLanguage'
 import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
+import {faList} from '@fortawesome/free-solid-svg-icons/faList'
 import {faListUl} from '@fortawesome/free-solid-svg-icons/faListUl'
 import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
 import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
 import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
 import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky'
+import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
 import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste'
 import {faPen} from '@fortawesome/free-solid-svg-icons/faPen'
 import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
 import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare'
+import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
 import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
 import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft'
 import {faReply} from '@fortawesome/free-solid-svg-icons/faReply'
@@ -77,6 +83,7 @@ import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders'
 import {faSquare} from '@fortawesome/free-regular-svg-icons/faSquare'
 import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck'
 import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
+import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
 import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
 import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
 import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
@@ -88,11 +95,6 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
 import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash'
 import {faX} from '@fortawesome/free-solid-svg-icons/faX'
 import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
-import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
-import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
-import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
-import {faList} from '@fortawesome/free-solid-svg-icons/faList'
-import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
 
 export function setup() {
   library.add(
@@ -109,6 +111,7 @@ export function setup() {
     faArrowUpFromBracket,
     faArrowUpRightFromSquare,
     faArrowRotateLeft,
+    faArrowTrendUp,
     faArrowsRotate,
     faAt,
     faBan,
@@ -120,6 +123,7 @@ export function setup() {
     farCalendar,
     faCamera,
     faCheck,
+    faChevronRight,
     faCircle,
     faCircleCheck,
     farCircleCheck,
@@ -137,6 +141,7 @@ export function setup() {
     faExclamation,
     farEyeSlash,
     faFaceSmile,
+    faFire,
     faFloppyDisk,
     faGear,
     faGlobe,
@@ -150,15 +155,18 @@ export function setup() {
     faInfo,
     faLanguage,
     faLink,
+    faList,
     faListUl,
     faLock,
     faMagnifyingGlass,
     faMessage,
     faNoteSticky,
     faPaste,
+    faPause,
     faPen,
     faPenNib,
     faPenToSquare,
+    faPlay,
     faPlus,
     faQuoteLeft,
     faReply,
@@ -180,14 +188,10 @@ export function setup() {
     faUserPlus,
     faUserXmark,
     faUsersSlash,
+    faThumbtack,
     faTicket,
     faTrashCan,
-    faThumbtack,
     faX,
     faXmark,
-    faPlay,
-    faPause,
-    faList,
-    faChevronRight,
   )
 }
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
index af4d01843..eaa21f292 100644
--- a/src/view/screens/CustomFeed.tsx
+++ b/src/view/screens/CustomFeed.tsx
@@ -1,7 +1,7 @@
 import React, {useMemo, useRef} from 'react'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useNavigation} from '@react-navigation/native'
+import {useNavigation, useIsFocused} from '@react-navigation/native'
 import {usePalette} from 'lib/hooks/usePalette'
 import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {CommonNavigatorParams} from 'lib/routes/types'
@@ -14,11 +14,8 @@ import {PostsFeedModel} from 'state/models/feeds/posts'
 import {useCustomFeed} from 'lib/hooks/useCustomFeed'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {Feed} from 'view/com/posts/Feed'
-import {pluralize} from 'lib/strings/helpers'
-import {sanitizeHandle} from 'lib/strings/handles'
 import {TextLink} from 'view/com/util/Link'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {ViewHeader} from 'view/com/util/ViewHeader'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {Button} from 'view/com/util/forms/Button'
 import {Text} from 'view/com/util/text/Text'
 import * as Toast from 'view/com/util/Toast'
@@ -34,7 +31,6 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {EmptyState} from 'view/com/util/EmptyState'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {makeProfileLink} from 'lib/routes/links'
 import {resolveName} from 'lib/api'
 import {CenteredView} from 'view/com/util/Views'
 import {NavigationProp} from 'lib/routes/types'
@@ -125,7 +121,10 @@ export const CustomFeedScreenInner = observer(
   }: Props & {feedOwnerDid: string}) {
     const store = useStores()
     const pal = usePalette('default')
-    const {isTabletOrDesktop} = useWebMediaQueries()
+    const palInverted = usePalette('inverted')
+    const navigation = useNavigation<NavigationProp>()
+    const isScreenFocused = useIsFocused()
+    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
     const {track} = useAnalytics()
     const {rkey, name: handleOrDid} = route.params
     const uri = useMemo(
@@ -186,6 +185,10 @@ export const CustomFeedScreenInner = observer(
       })
     }, [store, currentFeed])
 
+    const onPressViewAuthor = React.useCallback(() => {
+      navigation.navigate('Profile', {name: handleOrDid})
+    }, [handleOrDid, navigation])
+
     const onPressShare = React.useCallback(() => {
       const url = toShareUrl(`/profile/${handleOrDid}/feed/${rkey}`)
       shareUrl(url)
@@ -210,9 +213,40 @@ export const CustomFeedScreenInner = observer(
       store.shell.openComposer({})
     }, [store])
 
+    const onSoftReset = React.useCallback(() => {
+      if (isScreenFocused) {
+        onScrollToTop()
+        algoFeed.refresh()
+      }
+    }, [isScreenFocused, onScrollToTop, algoFeed])
+
+    // fires when page within screen is activated/deactivated
+    React.useEffect(() => {
+      if (!isScreenFocused) {
+        return
+      }
+
+      const softResetSub = store.onScreenSoftReset(onSoftReset)
+      return () => {
+        softResetSub.remove()
+      }
+    }, [store, onSoftReset, isScreenFocused])
+
     const dropdownItems: DropdownItem[] = React.useMemo(() => {
       let items: DropdownItem[] = [
         {
+          testID: 'feedHeaderDropdownViewAuthorBtn',
+          label: 'View author',
+          onPress: onPressViewAuthor,
+          icon: {
+            ios: {
+              name: 'person',
+            },
+            android: '',
+            web: ['far', 'user'],
+          },
+        },
+        {
           testID: 'feedHeaderDropdownToggleSavedBtn',
           label: currentFeed?.isSaved
             ? 'Remove from my feeds'
@@ -260,232 +294,12 @@ export const CustomFeedScreenInner = observer(
         },
       ]
       return items
-    }, [currentFeed?.isSaved, onToggleSaved, onPressReport, onPressShare])
-
-    const renderHeaderBtns = React.useCallback(() => {
-      return (
-        <View style={styles.headerBtns}>
-          <Button
-            type="default-light"
-            testID="toggleLikeBtn"
-            accessibilityLabel="Like this feed"
-            accessibilityHint=""
-            onPress={onToggleLiked}>
-            {currentFeed?.isLiked ? (
-              <HeartIconSolid size={19} style={styles.liked} />
-            ) : (
-              <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
-            )}
-          </Button>
-          {currentFeed?.isSaved ? (
-            <Button
-              type="default-light"
-              accessibilityLabel={
-                isPinned ? 'Unpin this feed' : 'Pin this feed'
-              }
-              accessibilityHint=""
-              onPress={onTogglePinned}>
-              <FontAwesomeIcon
-                icon="thumb-tack"
-                size={17}
-                color={isPinned ? colors.blue3 : pal.colors.textLight}
-                style={styles.top1}
-              />
-            </Button>
-          ) : undefined}
-          {!currentFeed?.isSaved ? (
-            <Button
-              type="default-light"
-              onPress={onToggleSaved}
-              accessibilityLabel="Add to my feeds"
-              accessibilityHint=""
-              style={styles.headerAddBtn}>
-              <FontAwesomeIcon icon="plus" color={pal.colors.link} size={19} />
-              <Text type="xl-medium" style={pal.link}>
-                Add to My Feeds
-              </Text>
-            </Button>
-          ) : null}
-          <NativeDropdown testID="feedHeaderDropdownBtn" items={dropdownItems}>
-            <View
-              style={{
-                paddingLeft: currentFeed?.isSaved ? 12 : 6,
-                paddingRight: 12,
-                paddingVertical: 8,
-              }}>
-              <FontAwesomeIcon
-                icon="ellipsis"
-                size={20}
-                color={pal.colors.textLight}
-              />
-            </View>
-          </NativeDropdown>
-        </View>
-      )
     }, [
-      pal,
       currentFeed?.isSaved,
-      currentFeed?.isLiked,
-      isPinned,
-      onToggleSaved,
-      onTogglePinned,
-      onToggleLiked,
-      dropdownItems,
-    ])
-
-    const renderListHeaderComponent = React.useCallback(() => {
-      return (
-        <>
-          <View style={[styles.header, pal.border]}>
-            <View style={s.flex1}>
-              <Text
-                testID="feedName"
-                type="title-xl"
-                style={[pal.text, s.bold]}>
-                {currentFeed?.displayName}
-              </Text>
-              {currentFeed && (
-                <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-                  by{' '}
-                  {currentFeed.data.creator.did === store.me.did ? (
-                    'you'
-                  ) : (
-                    <TextLink
-                      text={sanitizeHandle(
-                        currentFeed.data.creator.handle,
-                        '@',
-                      )}
-                      href={makeProfileLink(currentFeed.data.creator)}
-                      style={[pal.textLight]}
-                    />
-                  )}
-                </Text>
-              )}
-              {isTabletOrDesktop && (
-                <View style={[styles.headerBtns, styles.headerBtnsDesktop]}>
-                  <Button
-                    type={currentFeed?.isSaved ? 'default' : 'inverted'}
-                    onPress={onToggleSaved}
-                    accessibilityLabel={
-                      currentFeed?.isSaved
-                        ? 'Unsave this feed'
-                        : 'Save this feed'
-                    }
-                    accessibilityHint=""
-                    label={
-                      currentFeed?.isSaved
-                        ? 'Remove from My Feeds'
-                        : 'Add to My Feeds'
-                    }
-                  />
-                  <Button
-                    type="default"
-                    accessibilityLabel={
-                      isPinned ? 'Unpin this feed' : 'Pin this feed'
-                    }
-                    accessibilityHint=""
-                    onPress={onTogglePinned}>
-                    <FontAwesomeIcon
-                      icon="thumb-tack"
-                      size={15}
-                      color={isPinned ? colors.blue3 : pal.colors.icon}
-                      style={styles.top2}
-                    />
-                  </Button>
-                  <Button
-                    type="default"
-                    accessibilityLabel="Like this feed"
-                    accessibilityHint=""
-                    onPress={onToggleLiked}>
-                    {currentFeed?.isLiked ? (
-                      <HeartIconSolid size={18} style={styles.liked} />
-                    ) : (
-                      <HeartIcon strokeWidth={3} size={18} style={pal.icon} />
-                    )}
-                  </Button>
-                  <Button
-                    type="default"
-                    accessibilityLabel="Share this feed"
-                    accessibilityHint=""
-                    onPress={onPressShare}>
-                    <FontAwesomeIcon
-                      icon="share"
-                      size={18}
-                      color={pal.colors.icon}
-                    />
-                  </Button>
-                  <Button
-                    type="default"
-                    accessibilityLabel="Report this feed"
-                    accessibilityHint=""
-                    onPress={onPressReport}>
-                    <FontAwesomeIcon
-                      icon="circle-exclamation"
-                      size={18}
-                      color={pal.colors.icon}
-                    />
-                  </Button>
-                </View>
-              )}
-            </View>
-            <View>
-              <UserAvatar
-                type="algo"
-                avatar={currentFeed?.data.avatar}
-                size={64}
-              />
-            </View>
-          </View>
-          <View style={styles.headerDetails}>
-            {currentFeed?.data.description ? (
-              <Text style={[pal.text, s.mb10]} numberOfLines={6}>
-                {currentFeed.data.description}
-              </Text>
-            ) : null}
-            <View style={styles.headerDetailsFooter}>
-              {currentFeed ? (
-                <TextLink
-                  type="md-medium"
-                  style={pal.textLight}
-                  href={`/profile/${handleOrDid}/feed/${rkey}/liked-by`}
-                  text={`Liked by ${currentFeed.data.likeCount} ${pluralize(
-                    currentFeed?.data.likeCount || 0,
-                    'user',
-                  )}`}
-                />
-              ) : null}
-            </View>
-          </View>
-          <View
-            style={[
-              styles.fakeSelector,
-              {
-                paddingHorizontal: isTabletOrDesktop ? 16 : 6,
-              },
-              pal.border,
-            ]}>
-            <View
-              style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}>
-              <Text type="md-medium" style={[pal.text]}>
-                Feed
-              </Text>
-            </View>
-          </View>
-        </>
-      )
-    }, [
-      pal,
-      currentFeed,
-      store.me.did,
       onToggleSaved,
-      onToggleLiked,
-      onPressShare,
-      handleOrDid,
       onPressReport,
-      rkey,
-      isPinned,
-      onTogglePinned,
-      isTabletOrDesktop,
+      onPressShare,
+      onPressViewAuthor,
     ])
 
     const renderEmptyState = React.useCallback(() => {
@@ -498,22 +312,100 @@ export const CustomFeedScreenInner = observer(
 
     return (
       <View style={s.hContentRegion}>
-        {!isTabletOrDesktop && (
-          <ViewHeader title="" renderButton={currentFeed && renderHeaderBtns} />
-        )}
+        <SimpleViewHeader
+          showBackButton={isMobile}
+          style={
+            !isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
+          }>
+          <Text type="title-lg" style={styles.headerText} numberOfLines={1}>
+            {currentFeed ? (
+              <TextLink
+                type="title-lg"
+                href="/"
+                style={[pal.text, {fontWeight: 'bold'}]}
+                text={currentFeed?.displayName || ''}
+                onPress={() => store.emitScreenSoftReset()}
+              />
+            ) : (
+              'Loading...'
+            )}
+          </Text>
+          {currentFeed ? (
+            <>
+              <Button
+                type="default-light"
+                testID="toggleLikeBtn"
+                accessibilityLabel="Like this feed"
+                accessibilityHint=""
+                onPress={onToggleLiked}
+                style={styles.headerBtn}>
+                {currentFeed?.isLiked ? (
+                  <HeartIconSolid size={19} style={styles.liked} />
+                ) : (
+                  <HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
+                )}
+              </Button>
+              {currentFeed?.isSaved ? (
+                <Button
+                  type="default-light"
+                  accessibilityLabel={
+                    isPinned ? 'Unpin this feed' : 'Pin this feed'
+                  }
+                  accessibilityHint=""
+                  onPress={onTogglePinned}
+                  style={styles.headerBtn}>
+                  <FontAwesomeIcon
+                    icon="thumb-tack"
+                    size={17}
+                    color={isPinned ? colors.blue3 : pal.colors.textLight}
+                    style={styles.top1}
+                  />
+                </Button>
+              ) : (
+                <Button
+                  type="inverted"
+                  onPress={onToggleSaved}
+                  accessibilityLabel="Add to my feeds"
+                  accessibilityHint=""
+                  style={styles.headerAddBtn}>
+                  <FontAwesomeIcon
+                    icon="plus"
+                    color={palInverted.colors.text}
+                    size={19}
+                  />
+                  <Text type="button" style={palInverted.text}>
+                    Add{!isMobile && ' to My Feeds'}
+                  </Text>
+                </Button>
+              )}
+            </>
+          ) : null}
+          <NativeDropdown testID="feedHeaderDropdownBtn" items={dropdownItems}>
+            <View
+              style={{
+                paddingLeft: 12,
+                paddingRight: isMobile ? 12 : 0,
+              }}>
+              <FontAwesomeIcon
+                icon="ellipsis"
+                size={20}
+                color={pal.colors.textLight}
+              />
+            </View>
+          </NativeDropdown>
+        </SimpleViewHeader>
         <Feed
           scrollElRef={scrollElRef}
           feed={algoFeed}
           onScroll={onMainScroll}
           scrollEventThrottle={100}
-          ListHeaderComponent={renderListHeaderComponent}
           renderEmptyState={renderEmptyState}
           extraData={[uri, isPinned]}
           style={!isTabletOrDesktop ? {flex: 1} : undefined}
         />
         {isScrolledDown ? (
           <LoadLatestBtn
-            onPress={onScrollToTop}
+            onPress={onSoftReset}
             label="Scroll to top"
             showIndicator={false}
           />
@@ -540,36 +432,19 @@ const styles = StyleSheet.create({
     paddingBottom: 16,
     borderTopWidth: 1,
   },
-  headerBtns: {
-    flexDirection: 'row',
-    alignItems: 'center',
+  headerText: {
+    flex: 1,
+    fontWeight: 'bold',
   },
-  headerBtnsDesktop: {
-    marginTop: 8,
-    gap: 4,
+  headerBtn: {
+    paddingVertical: 0,
   },
   headerAddBtn: {
     flexDirection: 'row',
     alignItems: 'center',
     gap: 4,
-    paddingLeft: 4,
-  },
-  headerDetails: {
-    paddingHorizontal: 16,
-    paddingBottom: 16,
-  },
-  headerDetailsFooter: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'space-between',
-  },
-  fakeSelector: {
-    flexDirection: 'row',
-  },
-  fakeSelectorItem: {
-    paddingHorizontal: 12,
-    paddingBottom: 8,
-    borderBottomWidth: 3,
+    paddingVertical: 4,
+    paddingLeft: 10,
   },
   liked: {
     color: colors.red3,
diff --git a/src/view/screens/DiscoverFeeds.tsx b/src/view/screens/DiscoverFeeds.tsx
deleted file mode 100644
index 6aa7a9e31..000000000
--- a/src/view/screens/DiscoverFeeds.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import React from 'react'
-import {RefreshControl, 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 {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {ViewHeader} from '../com/util/ViewHeader'
-import {useStores} from 'state/index'
-import {FeedsDiscoveryModel} from 'state/models/discovery/feeds'
-import {CenteredView, FlatList} from 'view/com/util/Views'
-import {CustomFeed} from 'view/com/feeds/CustomFeed'
-import {Text} from 'view/com/util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {CustomFeedModel} from 'state/models/feeds/custom-feed'
-import {HeaderWithInput} from 'view/com/search/HeaderWithInput'
-import debounce from 'lodash.debounce'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'DiscoverFeeds'>
-export const DiscoverFeedsScreen = withAuthRequired(
-  observer(function DiscoverFeedsScreenImpl({}: Props) {
-    const store = useStores()
-    const pal = usePalette('default')
-    const feeds = React.useMemo(() => new FeedsDiscoveryModel(store), [store])
-    const {isTabletOrDesktop} = useWebMediaQueries()
-
-    // search stuff
-    const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
-    const [query, setQuery] = React.useState<string>('')
-    const debouncedSearchFeeds = React.useMemo(
-      () => debounce(q => feeds.search(q), 500), // debounce for 500ms
-      [feeds],
-    )
-    const onChangeQuery = React.useCallback(
-      (text: string) => {
-        setQuery(text)
-        if (text.length > 1) {
-          debouncedSearchFeeds(text)
-        } else {
-          feeds.refresh()
-        }
-      },
-      [debouncedSearchFeeds, feeds],
-    )
-    const onPressClearQuery = React.useCallback(() => {
-      setQuery('')
-      feeds.refresh()
-    }, [feeds])
-    const onPressCancelSearch = React.useCallback(() => {
-      setIsInputFocused(false)
-      setQuery('')
-      feeds.refresh()
-    }, [feeds])
-    const onSubmitQuery = React.useCallback(() => {
-      debouncedSearchFeeds(query)
-      debouncedSearchFeeds.flush()
-    }, [debouncedSearchFeeds, query])
-
-    useFocusEffect(
-      React.useCallback(() => {
-        store.shell.setMinimalShellMode(false)
-        if (!feeds.hasLoaded) {
-          feeds.refresh()
-        }
-      }, [store, feeds]),
-    )
-
-    const onRefresh = React.useCallback(() => {
-      feeds.refresh()
-    }, [feeds])
-
-    const renderListEmptyComponent = () => {
-      return (
-        <View style={styles.empty}>
-          <Text type="lg" style={pal.textLight}>
-            {feeds.isLoading
-              ? isTabletOrDesktop
-                ? 'Loading...'
-                : ''
-              : query
-              ? `No results found for "${query}"`
-              : `We can't find any feeds for some reason. This is probably an error - try refreshing!`}
-          </Text>
-        </View>
-      )
-    }
-
-    const renderItem = React.useCallback(
-      ({item}: {item: CustomFeedModel}) => (
-        <CustomFeed
-          key={item.data.uri}
-          item={item}
-          showSaveBtn
-          showDescription
-          showLikes
-        />
-      ),
-      [],
-    )
-
-    return (
-      <CenteredView style={[styles.container, pal.view]}>
-        <View
-          style={[isTabletOrDesktop && styles.containerDesktop, pal.border]}>
-          <ViewHeader title="Discover Feeds" showOnDesktop />
-        </View>
-        <HeaderWithInput
-          isInputFocused={isInputFocused}
-          query={query}
-          setIsInputFocused={setIsInputFocused}
-          onChangeQuery={onChangeQuery}
-          onPressClearQuery={onPressClearQuery}
-          onPressCancelSearch={onPressCancelSearch}
-          onSubmitQuery={onSubmitQuery}
-          showMenu={false}
-        />
-        <FlatList
-          style={[!isTabletOrDesktop && s.flex1]}
-          data={feeds.feeds}
-          keyExtractor={item => item.data.uri}
-          contentContainerStyle={styles.contentContainer}
-          refreshControl={
-            <RefreshControl
-              refreshing={feeds.isRefreshing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-            />
-          }
-          renderItem={renderItem}
-          initialNumToRender={10}
-          ListEmptyComponent={renderListEmptyComponent}
-          onEndReached={() => feeds.loadMore()}
-          extraData={feeds.isLoading}
-        />
-      </CenteredView>
-    )
-  }),
-)
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-  },
-  contentContainer: {
-    paddingBottom: 100,
-  },
-  containerDesktop: {
-    borderLeftWidth: 1,
-    borderRightWidth: 1,
-  },
-  empty: {
-    paddingHorizontal: 16,
-    paddingTop: 10,
-  },
-})
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 97c6e8672..d2c4a6d2d 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,90 +1,72 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
-import isEqual from 'lodash.isequal'
+import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
+import {AtUri} from '@atproto/api'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {FlatList} from 'view/com/util/Views'
 import {ViewHeader} from 'view/com/util/ViewHeader'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {FAB} from 'view/com/util/fab/FAB'
 import {Link} from 'view/com/util/Link'
 import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
-import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed'
-import {MultiFeed} from 'view/com/posts/MultiFeed'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useTimer} from 'lib/hooks/useTimer'
 import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {ComposeIcon2, CogIcon} from 'lib/icons'
 import {s} from 'lib/styles'
-
-const LOAD_NEW_PROMPT_TIME = 60e3 // 60 seconds
-const MOBILE_HEADER_OFFSET = 40
+import {SearchInput} from 'view/com/util/forms/SearchInput'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+import debounce from 'lodash.debounce'
+import {Text} from 'view/com/util/text/Text'
+import {MyFeedsUIModel, MyFeedsItem} from 'state/models/ui/my-feeds'
+import {FlatList} from 'view/com/util/Views'
+import {useFocusEffect} from '@react-navigation/native'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
 
 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
 export const FeedsScreen = withAuthRequired(
   observer<Props>(function FeedsScreenImpl({}: Props) {
     const pal = usePalette('default')
     const store = useStores()
-    const {isMobile} = useWebMediaQueries()
-    const flatListRef = React.useRef<FlatList>(null)
-    const multifeed = React.useMemo<PostsMultiFeedModel>(
-      () => new PostsMultiFeedModel(store),
-      [store],
+    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+    const myFeeds = React.useMemo(() => new MyFeedsUIModel(store), [store])
+    const [query, setQuery] = React.useState<string>('')
+    const debouncedSearchFeeds = React.useMemo(
+      () => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms
+      [myFeeds],
     )
-    const [onMainScroll, isScrolledDown, resetMainScroll] =
-      useOnMainScroll(store)
-    const [loadPromptVisible, setLoadPromptVisible] = React.useState(false)
-    const [resetPromptTimer] = useTimer(LOAD_NEW_PROMPT_TIME, () => {
-      setLoadPromptVisible(true)
-    })
-
-    const onSoftReset = React.useCallback(() => {
-      flatListRef.current?.scrollToOffset({offset: 0})
-      multifeed.loadLatest()
-      resetPromptTimer()
-      setLoadPromptVisible(false)
-      resetMainScroll()
-    }, [
-      flatListRef,
-      resetMainScroll,
-      multifeed,
-      resetPromptTimer,
-      setLoadPromptVisible,
-    ])
 
     useFocusEffect(
       React.useCallback(() => {
-        const softResetSub = store.onScreenSoftReset(onSoftReset)
-        const multifeedCleanup = multifeed.registerListeners()
-        const cleanup = () => {
-          softResetSub.remove()
-          multifeedCleanup()
-        }
-
         store.shell.setMinimalShellMode(false)
-        return cleanup
-      }, [store, multifeed, onSoftReset]),
+        myFeeds.setup()
+      }, [store.shell, myFeeds]),
     )
 
-    React.useEffect(() => {
-      if (
-        isEqual(
-          multifeed.feedInfos.map(f => f.uri),
-          store.me.savedFeeds.all.map(f => f.uri),
-        )
-      ) {
-        // no changes
-        return
-      }
-      multifeed.refresh()
-    }, [multifeed, store.me.savedFeeds.all])
-
     const onPressCompose = React.useCallback(() => {
       store.shell.openComposer({})
     }, [store])
+    const onChangeQuery = React.useCallback(
+      (text: string) => {
+        setQuery(text)
+        if (text.length > 1) {
+          debouncedSearchFeeds(text)
+        } else {
+          myFeeds.discovery.refresh()
+        }
+      },
+      [debouncedSearchFeeds, myFeeds.discovery],
+    )
+    const onPressCancelSearch = React.useCallback(() => {
+      setQuery('')
+      myFeeds.discovery.refresh()
+    }, [myFeeds])
+    const onSubmitQuery = React.useCallback(() => {
+      debouncedSearchFeeds(query)
+      debouncedSearchFeeds.flush()
+    }, [debouncedSearchFeeds, query])
 
     const renderHeaderBtn = React.useCallback(() => {
       return (
@@ -99,30 +81,150 @@ export const FeedsScreen = withAuthRequired(
       )
     }, [pal])
 
+    const onRefresh = React.useCallback(() => {
+      myFeeds.refresh()
+    }, [myFeeds])
+
+    const renderItem = React.useCallback(
+      ({item}: {item: MyFeedsItem}) => {
+        if (item.type === 'discover-feeds-loading') {
+          return <FeedFeedLoadingPlaceholder />
+        } else if (item.type === 'spinner') {
+          return (
+            <View style={s.p10}>
+              <ActivityIndicator />
+            </View>
+          )
+        } else if (item.type === 'error') {
+          return <ErrorMessage message={item.error} />
+        } else if (item.type === 'saved-feeds-header') {
+          if (!isMobile) {
+            return (
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  pal.border,
+                  {
+                    borderBottomWidth: 1,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  My Feeds
+                </Text>
+                <Link href="/settings/saved-feeds">
+                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+                </Link>
+              </View>
+            )
+          }
+          return <View />
+        } else if (item.type === 'saved-feed') {
+          return (
+            <SavedFeed
+              uri={item.feed.uri}
+              avatar={item.feed.data.avatar}
+              displayName={item.feed.displayName}
+            />
+          )
+        } else if (item.type === 'discover-feeds-header') {
+          return (
+            <>
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  {
+                    marginTop: 16,
+                    paddingLeft: isMobile ? 12 : undefined,
+                    paddingRight: 10,
+                    paddingBottom: isMobile ? 6 : undefined,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  Discover new feeds
+                </Text>
+                {!isMobile && (
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                    style={{flex: 1, maxWidth: 250}}
+                  />
+                )}
+              </View>
+              {isMobile && (
+                <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                  />
+                </View>
+              )}
+            </>
+          )
+        } else if (item.type === 'discover-feed') {
+          return (
+            <CustomFeed
+              item={item.feed}
+              showSaveBtn
+              showDescription
+              showLikes
+            />
+          )
+        } else if (item.type === 'discover-feeds-no-results') {
+          return (
+            <View
+              style={{
+                paddingHorizontal: 16,
+                paddingTop: 10,
+                paddingBottom: '150%',
+              }}>
+              <Text type="lg" style={pal.textLight}>
+                No results found for "{query}"
+              </Text>
+            </View>
+          )
+        }
+        return null
+      },
+      [isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
+    )
+
     return (
       <View style={[pal.view, styles.container]}>
-        <MultiFeed
-          scrollElRef={flatListRef}
-          multifeed={multifeed}
-          onScroll={onMainScroll}
-          scrollEventThrottle={100}
-          headerOffset={isMobile ? MOBILE_HEADER_OFFSET : undefined}
-        />
         {isMobile && (
           <ViewHeader
-            title="My Feeds"
+            title="Feeds"
             canGoBack={false}
-            hideOnScroll
             renderButton={renderHeaderBtn}
+            showBorder
           />
         )}
-        {isScrolledDown || loadPromptVisible ? (
-          <LoadLatestBtn
-            onPress={onSoftReset}
-            label="Load latest posts"
-            showIndicator={loadPromptVisible}
-          />
-        ) : null}
+
+        <FlatList
+          style={[!isTabletOrDesktop && s.flex1, styles.list]}
+          data={myFeeds.items}
+          keyExtractor={item => item._reactKey}
+          contentContainerStyle={styles.contentContainer}
+          refreshControl={
+            <RefreshControl
+              refreshing={myFeeds.isRefreshing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          renderItem={renderItem}
+          initialNumToRender={10}
+          onEndReached={() => myFeeds.loadMore()}
+          extraData={myFeeds.isLoading}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
         <FAB
           testID="composeFAB"
           onPress={onPressCompose}
@@ -136,8 +238,76 @@ export const FeedsScreen = withAuthRequired(
   }),
 )
 
+function SavedFeed({
+  uri,
+  avatar,
+  displayName,
+}: {
+  uri: string
+  avatar: string | undefined
+  displayName: string
+}) {
+  const pal = usePalette('default')
+  const urip = new AtUri(uri)
+  const href = `/profile/${urip.hostname}/feed/${urip.rkey}`
+  const {isMobile} = useWebMediaQueries()
+  return (
+    <Link
+      testID={`saved-feed-${displayName}`}
+      href={href}
+      style={[pal.border, styles.savedFeed, isMobile && styles.savedFeedMobile]}
+      hoverStyle={pal.viewLight}
+      accessibilityLabel={displayName}
+      accessibilityHint=""
+      asAnchor
+      anchorNoUnderline>
+      <UserAvatar type="algo" size={28} avatar={avatar} />
+      <Text
+        type={isMobile ? 'lg' : 'lg-medium'}
+        style={[pal.text, s.flex1]}
+        numberOfLines={1}>
+        {displayName}
+      </Text>
+      {isMobile && (
+        <FontAwesomeIcon
+          icon="chevron-right"
+          size={14}
+          style={pal.textLight as FontAwesomeIconStyle}
+        />
+      )}
+    </Link>
+  )
+}
+
 const styles = StyleSheet.create({
   container: {
     flex: 1,
   },
+  list: {
+    height: '100%',
+  },
+  contentContainer: {
+    paddingBottom: 100,
+  },
+
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    gap: 16,
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+  },
+
+  savedFeed: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 14,
+    gap: 12,
+    borderBottomWidth: 1,
+  },
+  savedFeedMobile: {
+    paddingVertical: 10,
+  },
 })
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 33cc2e110..60cda31db 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,6 +1,8 @@
 import React from 'react'
 import {FlatList, View} from 'react-native'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
 import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
@@ -8,6 +10,7 @@ import isEqual from 'lodash.isequal'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
 import {PostsFeedModel} from 'state/models/feeds/posts'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {TextLink} from 'view/com/util/Link'
 import {Feed} from '../com/posts/Feed'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
@@ -16,14 +19,16 @@ import {FeedsTabBar} from '../com/pager/FeedsTabBar'
 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {FAB} from '../com/util/fab/FAB'
 import {useStores} from 'state/index'
-import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s, colors} from 'lib/styles'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {ComposeIcon2} from 'lib/icons'
 
 const HEADER_OFFSET_MOBILE = 78
-const HEADER_OFFSET_DESKTOP = 50
+const HEADER_OFFSET_TABLET = 50
+const HEADER_OFFSET_DESKTOP = 0
 const POLL_FREQ = 30e3 // 30sec
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
@@ -154,17 +159,23 @@ const FeedPage = observer(function FeedPageImpl({
   renderEmptyState?: () => JSX.Element
 }) {
   const store = useStores()
-  const {isMobile} = useWebMediaQueries()
+  const pal = usePalette('default')
+  const {isMobile, isTablet, isDesktop} = useWebMediaQueries()
   const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll(store)
   const {screen, track} = useAnalytics()
   const [headerOffset, setHeaderOffset] = React.useState(
-    isMobile ? HEADER_OFFSET_MOBILE : HEADER_OFFSET_DESKTOP,
+    isMobile
+      ? HEADER_OFFSET_MOBILE
+      : isTablet
+      ? HEADER_OFFSET_TABLET
+      : HEADER_OFFSET_DESKTOP,
   )
   const scrollElRef = React.useRef<FlatList>(null)
   const {appState} = useAppState({
     onForeground: () => doPoll(true),
   })
   const isScreenFocused = useIsFocused()
+  const hasNew = feed.hasNewLatest && !feed.isRefreshing
 
   React.useEffect(() => {
     // called on first load
@@ -205,8 +216,14 @@ const FeedPage = observer(function FeedPageImpl({
 
   // listens for resize events
   React.useEffect(() => {
-    setHeaderOffset(isMobile ? HEADER_OFFSET_MOBILE : HEADER_OFFSET_DESKTOP)
-  }, [isMobile])
+    setHeaderOffset(
+      isMobile
+        ? HEADER_OFFSET_MOBILE
+        : isTablet
+        ? HEADER_OFFSET_TABLET
+        : HEADER_OFFSET_DESKTOP,
+    )
+  }, [isMobile, isTablet])
 
   // fires when page within screen is activated/deactivated
   // - check for latest
@@ -222,9 +239,6 @@ const FeedPage = observer(function FeedPageImpl({
     screen('Feed')
     store.log.debug('HomeScreen: Updating feed')
     feed.checkForLatest()
-    if (feed.hasContent) {
-      feed.update()
-    }
 
     return () => {
       clearInterval(pollInterval)
@@ -247,7 +261,59 @@ const FeedPage = observer(function FeedPageImpl({
     feed.refresh()
   }, [feed, scrollToTop])
 
-  const hasNew = feed.hasNewLatest && !feed.isRefreshing
+  const ListHeaderComponent = React.useCallback(() => {
+    if (isDesktop) {
+      return (
+        <View
+          style={[
+            pal.view,
+            {
+              flexDirection: 'row',
+              alignItems: 'center',
+              justifyContent: 'space-between',
+              paddingHorizontal: 18,
+              paddingVertical: 12,
+            },
+          ]}>
+          <TextLink
+            type="title-lg"
+            href="/"
+            style={[pal.text, {fontWeight: 'bold'}]}
+            text={
+              <>
+                {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
+                {hasNew && (
+                  <View
+                    style={{
+                      top: -8,
+                      backgroundColor: colors.blue3,
+                      width: 8,
+                      height: 8,
+                      borderRadius: 4,
+                    }}
+                  />
+                )}
+              </>
+            }
+            onPress={() => store.emitScreenSoftReset()}
+          />
+          <TextLink
+            type="title-lg"
+            href="/settings/home-feed"
+            style={{fontWeight: 'bold'}}
+            text={
+              <FontAwesomeIcon
+                icon="sliders"
+                style={pal.textLight as FontAwesomeIconStyle}
+              />
+            }
+          />
+        </View>
+      )
+    }
+    return <></>
+  }, [isDesktop, pal, store, hasNew])
+
   return (
     <View testID={testID} style={s.h100pct}>
       <Feed
@@ -259,6 +325,7 @@ const FeedPage = observer(function FeedPageImpl({
         onScroll={onMainScroll}
         scrollEventThrottle={100}
         renderEmptyState={renderEmptyState}
+        ListHeaderComponent={ListHeaderComponent}
         headerOffset={headerOffset}
       />
       {(isScrolledDown || hasNew) && (
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 3c257fac8..243cc9596 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -9,12 +9,15 @@ import {
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
+import {TextLink} from 'view/com/util/Link'
 import {InvitedUsers} from '../com/notifications/InvitedUsers'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {useStores} from 'state/index'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect'
-import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {s, colors} from 'lib/styles'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {isWeb} from 'platform/detection'
 
@@ -29,6 +32,12 @@ export const NotificationsScreen = withAuthRequired(
       useOnMainScroll(store)
     const scrollElRef = React.useRef<FlatList>(null)
     const {screen} = useAnalytics()
+    const pal = usePalette('default')
+    const {isDesktop} = useWebMediaQueries()
+
+    const hasNew =
+      store.me.notifications.hasNewLatest &&
+      !store.me.notifications.isRefreshing
 
     // event handlers
     // =
@@ -88,9 +97,48 @@ export const NotificationsScreen = withAuthRequired(
       ),
     )
 
-    const hasNew =
-      store.me.notifications.hasNewLatest &&
-      !store.me.notifications.isRefreshing
+    const ListHeaderComponent = React.useCallback(() => {
+      if (isDesktop) {
+        return (
+          <View
+            style={[
+              pal.view,
+              {
+                flexDirection: 'row',
+                alignItems: 'center',
+                justifyContent: 'space-between',
+                paddingHorizontal: 18,
+                paddingVertical: 12,
+              },
+            ]}>
+            <TextLink
+              type="title-lg"
+              href="/notifications"
+              style={[pal.text, {fontWeight: 'bold'}]}
+              text={
+                <>
+                  Notifications{' '}
+                  {hasNew && (
+                    <View
+                      style={{
+                        top: -8,
+                        backgroundColor: colors.blue3,
+                        width: 8,
+                        height: 8,
+                        borderRadius: 4,
+                      }}
+                    />
+                  )}
+                </>
+              }
+              onPress={() => store.emitScreenSoftReset()}
+            />
+          </View>
+        )
+      }
+      return <></>
+    }, [isDesktop, pal, store, hasNew])
+
     return (
       <View testID="notificationsScreen" style={s.hContentRegion}>
         <ViewHeader title="Notifications" canGoBack={false} />
@@ -100,6 +148,7 @@ export const NotificationsScreen = withAuthRequired(
           onPressTryAgain={onPressTryAgain}
           onScroll={onMainScroll}
           scrollElRef={scrollElRef}
+          ListHeaderComponent={ListHeaderComponent}
         />
         {(isScrolledDown || hasNew) && (
           <LoadLatestBtn
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx
index 49c13bfa3..81bdfc95e 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -19,14 +19,7 @@ function RepliesThresholdInput({enabled}: {enabled: boolean}) {
   const [value, setValue] = useState(store.preferences.homeFeedRepliesThreshold)
 
   return (
-    <View style={[s.mt10, !enabled && styles.dimmed]}>
-      <Text type="xs" style={pal.text}>
-        {value === 0
-          ? `Show all replies`
-          : `Show replies with at least ${value} ${
-              value > 1 ? `likes` : `like`
-            }`}
-      </Text>
+    <View style={[!enabled && styles.dimmed]}>
       <Slider
         value={value}
         onValueChange={(v: number | number[]) => {
@@ -40,6 +33,13 @@ function RepliesThresholdInput({enabled}: {enabled: boolean}) {
         disabled={!enabled}
         thumbTintColor={colors.blue3}
       />
+      <Text type="xs" style={pal.text}>
+        {value === 0
+          ? `Show all replies`
+          : `Show replies with at least ${value} ${
+              value > 1 ? `likes` : `like`
+            }`}
+      </Text>
     </View>
   )
 }
@@ -79,8 +79,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
               Show Replies
             </Text>
             <Text style={[pal.text, s.pb10]}>
-              Adjust the number of likes a reply must have to be shown in your
-              feed.
+              Set this setting to "No" to hide all replies from your feed.
             </Text>
             <ToggleButton
               type="default-light"
@@ -88,7 +87,36 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
               isSelected={store.preferences.homeFeedRepliesEnabled}
               onPress={store.preferences.toggleHomeFeedRepliesEnabled}
             />
-
+          </View>
+          <View
+            style={[
+              pal.viewLight,
+              styles.card,
+              !store.preferences.homeFeedRepliesEnabled && styles.dimmed,
+            ]}>
+            <Text type="title-sm" style={[pal.text, s.pb5]}>
+              Reply Filters
+            </Text>
+            <Text style={[pal.text, s.pb10]}>
+              Enable this setting to only see replies between people you follow.
+            </Text>
+            <ToggleButton
+              type="default-light"
+              label="Followed users only"
+              isSelected={
+                store.preferences.homeFeedRepliesByFollowedOnlyEnabled
+              }
+              onPress={
+                store.preferences.homeFeedRepliesEnabled
+                  ? store.preferences.toggleHomeFeedRepliesByFollowedOnlyEnabled
+                  : undefined
+              }
+              style={[s.mb10]}
+            />
+            <Text style={[pal.text]}>
+              Adjust the number of likes a reply must have to be shown in your
+              feed.
+            </Text>
             <RepliesThresholdInput
               enabled={store.preferences.homeFeedRepliesEnabled}
             />
@@ -124,6 +152,22 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
               onPress={store.preferences.toggleHomeFeedQuotePostsEnabled}
             />
           </View>
+
+          <View style={[pal.viewLight, styles.card]}>
+            <Text type="title-sm" style={[pal.text, s.pb5]}>
+              Show Posts from My Feeds (Experimental)
+            </Text>
+            <Text style={[pal.text, s.pb10]}>
+              Set this setting to "Yes" to show samples of your saved feeds in
+              your following feed.
+            </Text>
+            <ToggleButton
+              type="default-light"
+              label={store.preferences.homeFeedMergeFeedEnabled ? 'Yes' : 'No'}
+              isSelected={store.preferences.homeFeedMergeFeedEnabled}
+              onPress={store.preferences.toggleHomeFeedMergeFeedEnabled}
+            />
+          </View>
         </View>
       </ScrollView>
 
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 69b5ceee6..241bae1ed 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -69,9 +69,7 @@ export const ProfileScreen = withAuthRequired(
         let aborted = false
         store.shell.setMinimalShellMode(false)
         const feedCleanup = uiState.feed.registerListeners()
-        if (hasSetup) {
-          uiState.update()
-        } else {
+        if (!hasSetup) {
           uiState.setup().then(() => {
             if (aborted) {
               return
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index d5c02ba63..5253c5bd6 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -70,7 +70,7 @@ export const SavedFeeds = withAuthRequired(
       return (
         <>
           <View style={[styles.footerLinks, pal.border]}>
-            <Link style={styles.footerLink} href="/search/feeds">
+            <Link style={styles.footerLink} href="/feeds">
               <FontAwesomeIcon
                 icon="search"
                 size={18}
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 8a543fa4c..761f50d0a 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -40,7 +40,7 @@ import {AccountData} from 'state/models/session'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {NavigationProp} from 'lib/routes/types'
 import {pluralize} from 'lib/strings/helpers'
-import {HandIcon} from 'lib/icons'
+import {HandIcon, HashtagIcon} from 'lib/icons'
 import {formatCount} from 'view/com/util/numeric/format'
 import Clipboard from '@react-native-clipboard/clipboard'
 import {reset as resetNavigation} from '../../Navigation'
@@ -423,17 +423,14 @@ export const SettingsScreen = withAuthRequired(
           <TouchableOpacity
             testID="savedFeedsBtn"
             style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
-            accessibilityHint="Saved Feeds"
+            accessibilityHint="My Saved Feeds"
             accessibilityLabel="Opens screen with all saved feeds"
             onPress={onPressSavedFeeds}>
             <View style={[styles.iconContainer, pal.btn]}>
-              <FontAwesomeIcon
-                icon="satellite-dish"
-                style={pal.text as FontAwesomeIconStyle}
-              />
+              <HashtagIcon style={pal.text} size={18} strokeWidth={3} />
             </View>
             <Text type="lg" style={pal.text}>
-              Saved Feeds
+              My Saved Feeds
             </Text>
           </TouchableOpacity>
           <TouchableOpacity
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 3a4b8947a..174e4a806 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -28,8 +28,7 @@ import {
   MagnifyingGlassIcon2,
   MagnifyingGlassIcon2Solid,
   UserIconSolid,
-  SatelliteDishIcon,
-  SatelliteDishIconSolid,
+  HashtagIcon,
   HandIcon,
 } from 'lib/icons'
 import {UserAvatar} from 'view/com/util/UserAvatar'
@@ -258,21 +257,21 @@ export const DrawerContent = observer(function DrawerContentImpl() {
           <MenuItem
             icon={
               isAtFeeds ? (
-                <SatelliteDishIconSolid
-                  strokeWidth={1.5}
+                <HashtagIcon
+                  strokeWidth={3}
                   style={pal.text as FontAwesomeIconStyle}
                   size={24}
                 />
               ) : (
-                <SatelliteDishIcon
-                  strokeWidth={1.5}
+                <HashtagIcon
+                  strokeWidth={2}
                   style={pal.text as FontAwesomeIconStyle}
                   size={24}
                 />
               )
             }
-            label="My Feeds"
-            accessibilityLabel="My Feeds"
+            label="Feeds"
+            accessibilityLabel="Feeds"
             accessibilityHint=""
             onPress={onPressMyFeeds}
           />
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 4a34371ea..8ba74da2e 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -18,8 +18,7 @@ import {
   HomeIconSolid,
   MagnifyingGlassIcon2,
   MagnifyingGlassIcon2Solid,
-  SatelliteDishIcon,
-  SatelliteDishIconSolid,
+  HashtagIcon,
   BellIcon,
   BellIconSolid,
 } from 'lib/icons'
@@ -134,16 +133,16 @@ export const BottomBar = observer(function BottomBarImpl({
         testID="bottomBarFeedsBtn"
         icon={
           isAtFeeds ? (
-            <SatelliteDishIconSolid
-              size={25}
-              style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
-              strokeWidth={1.8}
+            <HashtagIcon
+              size={24}
+              style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
+              strokeWidth={4}
             />
           ) : (
-            <SatelliteDishIcon
-              size={25}
-              style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
-              strokeWidth={1.8}
+            <HashtagIcon
+              size={24}
+              style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
+              strokeWidth={2.25}
             />
           )
         }
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index f31ab44cf..ae9381440 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -49,6 +49,9 @@ export const styles = StyleSheet.create({
   homeIcon: {
     top: 0,
   },
+  feedsIcon: {
+    top: -2,
+  },
   searchIcon: {
     top: -2,
   },
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index af70d3364..6448eea63 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -15,8 +15,7 @@ import {
   HomeIconSolid,
   MagnifyingGlassIcon2,
   MagnifyingGlassIcon2Solid,
-  SatelliteDishIcon,
-  SatelliteDishIconSolid,
+  HashtagIcon,
   UserIcon,
   UserIconSolid,
 } from 'lib/icons'
@@ -68,12 +67,11 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() {
       </NavItem>
       <NavItem routeName="Feeds" href="/feeds">
         {({isActive}) => {
-          const Icon = isActive ? SatelliteDishIconSolid : SatelliteDishIcon
           return (
-            <Icon
-              size={25}
-              style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
-              strokeWidth={1.8}
+            <HashtagIcon
+              size={22}
+              style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
+              strokeWidth={isActive ? 4 : 2.5}
             />
           )
         }}
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
new file mode 100644
index 000000000..4da1401c3
--- /dev/null
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -0,0 +1,92 @@
+import React from 'react'
+import {View, StyleSheet} from 'react-native'
+import {useNavigationState} from '@react-navigation/native'
+import {AtUri} from '@atproto/api'
+import {observer} from 'mobx-react-lite'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {TextLink} from 'view/com/util/Link'
+import {getCurrentRoute} from 'lib/routes/helpers'
+
+export const DesktopFeeds = observer(function DesktopFeeds() {
+  const store = useStores()
+  const pal = usePalette('default')
+
+  const route = useNavigationState(state => {
+    if (!state) {
+      return {name: 'Home'}
+    }
+    return getCurrentRoute(state)
+  })
+
+  return (
+    <View style={[styles.container, pal.view, pal.border]}>
+      <FeedItem href="/" title="Following" current={route.name === 'Home'} />
+      {store.me.savedFeeds.pinned.map(feed => {
+        try {
+          const {hostname, rkey} = new AtUri(feed.uri)
+          const href = `/profile/${hostname}/feed/${rkey}`
+          const params = route.params as Record<string, string>
+          return (
+            <FeedItem
+              key={feed.uri}
+              href={href}
+              title={feed.displayName}
+              current={
+                route.name === 'CustomFeed' &&
+                params.name === hostname &&
+                params.rkey === rkey
+              }
+            />
+          )
+        } catch {
+          return null
+        }
+      })}
+      <View style={{paddingTop: 8, paddingBottom: 6}}>
+        <TextLink
+          type="lg"
+          href="/feeds"
+          text="More feeds"
+          style={[pal.link]}
+        />
+      </View>
+    </View>
+  )
+})
+
+function FeedItem({
+  title,
+  href,
+  current,
+}: {
+  title: string
+  href: string
+  current: boolean
+}) {
+  const pal = usePalette('default')
+  return (
+    <View style={{paddingVertical: 6}}>
+      <TextLink
+        type="xl"
+        href={href}
+        text={title}
+        style={[
+          current ? pal.text : pal.textLight,
+          {letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'},
+        ]}
+      />
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    position: 'relative',
+    width: 300,
+    paddingHorizontal: 12,
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+    paddingVertical: 18,
+  },
+})
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 8c1a33245..907df8641 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -32,8 +32,7 @@ import {
   CogIconSolid,
   ComposeIcon2,
   HandIcon,
-  SatelliteDishIcon,
-  SatelliteDishIconSolid,
+  HashtagIcon,
 } from 'lib/icons'
 import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
 import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
@@ -272,20 +271,20 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
       <NavItem
         href="/feeds"
         icon={
-          <SatelliteDishIcon
-            strokeWidth={1.75}
+          <HashtagIcon
+            strokeWidth={2.25}
             style={pal.text as FontAwesomeIconStyle}
             size={isDesktop ? 24 : 28}
           />
         }
         iconFilled={
-          <SatelliteDishIconSolid
-            strokeWidth={1.75}
+          <HashtagIcon
+            strokeWidth={2.5}
             style={pal.text as FontAwesomeIconStyle}
             size={isDesktop ? 24 : 28}
           />
         }
-        label="My Feeds"
+        label="Feeds"
       />
       <NavItem
         href="/notifications"
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index e17fa6a84..12ca256d2 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -4,6 +4,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
 import {DesktopSearch} from './Search'
+import {DesktopFeeds} from './Feeds'
 import {Text} from 'view/com/util/text/Text'
 import {TextLink} from 'view/com/util/Link'
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
@@ -26,6 +27,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() {
   return (
     <View style={[styles.rightNav, pal.view]}>
       {store.session.hasSession && <DesktopSearch />}
+      {store.session.hasSession && <DesktopFeeds />}
       <View style={styles.message}>
         {store.session.isSandbox ? (
           <View style={[palError.view, styles.messageLine, s.p10]}>
@@ -126,7 +128,7 @@ const styles = StyleSheet.create({
   },
 
   message: {
-    marginTop: 20,
+    paddingVertical: 18,
     paddingHorizontal: 10,
   },
   messageLine: {
@@ -134,7 +136,6 @@ const styles = StyleSheet.create({
   },
 
   inviteCodes: {
-    marginTop: 12,
     borderTopWidth: 1,
     paddingHorizontal: 16,
     paddingVertical: 12,
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index c7b322b58..dfd4f50bf 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -113,6 +113,7 @@ const styles = StyleSheet.create({
   container: {
     position: 'relative',
     width: 300,
+    paddingBottom: 18,
   },
   search: {
     paddingHorizontal: 16,