about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/auth/login/ChooseAccountForm.tsx53
-rw-r--r--src/view/com/feeds/FeedPage.tsx4
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx28
-rw-r--r--src/view/com/notifications/Feed.tsx31
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx34
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx34
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx2
-rw-r--r--src/view/com/pager/TabBar.tsx2
-rw-r--r--src/view/com/posts/Feed.tsx8
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx102
-rw-r--r--src/view/com/util/Link.tsx41
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx28
-rw-r--r--src/view/screens/Feeds.tsx1
-rw-r--r--src/view/screens/Home.tsx8
-rw-r--r--src/view/screens/Notifications.tsx22
-rw-r--r--src/view/screens/Profile.tsx3
-rw-r--r--src/view/screens/ProfileFeed.tsx3
-rw-r--r--src/view/screens/ProfileList.tsx4
-rw-r--r--src/view/screens/SavedFeeds.tsx133
-rw-r--r--src/view/screens/Settings.tsx18
-rw-r--r--src/view/shell/Drawer.tsx2
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx2
-rw-r--r--src/view/shell/bottom-bar/BottomBarWeb.tsx2
-rw-r--r--src/view/shell/desktop/Feeds.tsx2
-rw-r--r--src/view/shell/desktop/LeftNav.tsx2
25 files changed, 398 insertions, 171 deletions
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx
index 8c94ef2da..73ddfc9d6 100644
--- a/src/view/com/auth/login/ChooseAccountForm.tsx
+++ b/src/view/com/auth/login/ChooseAccountForm.tsx
@@ -1,23 +1,30 @@
 import React from 'react'
 import {ScrollView, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {Text} from '../../util/text/Text'
 import {UserAvatar} from '../../util/UserAvatar'
-import {s} from 'lib/styles'
+import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {styles} from './styles'
 import {useSession, useSessionApi, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import * as Toast from '#/view/com/util/Toast'
 
 function AccountItem({
   account,
   onSelect,
+  isCurrentAccount,
 }: {
   account: SessionAccount
   onSelect: (account: SessionAccount) => void
+  isCurrentAccount: boolean
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -48,11 +55,19 @@ function AccountItem({
             {account.handle}
           </Text>
         </Text>
-        <FontAwesomeIcon
-          icon="angle-right"
-          size={16}
-          style={[pal.text, s.mr10]}
-        />
+        {isCurrentAccount ? (
+          <FontAwesomeIcon
+            icon="check"
+            size={16}
+            style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
+          />
+        ) : (
+          <FontAwesomeIcon
+            icon="angle-right"
+            size={16}
+            style={[pal.text, s.mr10]}
+          />
+        )}
       </View>
     </TouchableOpacity>
   )
@@ -67,8 +82,9 @@ export const ChooseAccountForm = ({
   const {track, screen} = useAnalytics()
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {accounts} = useSession()
+  const {accounts, currentAccount} = useSession()
   const {initSession} = useSessionApi()
+  const {setShowLoggedOut} = useLoggedOutViewControls()
 
   React.useEffect(() => {
     screen('Choose Account')
@@ -77,13 +93,21 @@ export const ChooseAccountForm = ({
   const onSelect = React.useCallback(
     async (account: SessionAccount) => {
       if (account.accessJwt) {
-        await initSession(account)
-        track('Sign In', {resumedSession: true})
+        if (account.did === currentAccount?.did) {
+          setShowLoggedOut(false)
+          Toast.show(`Already signed in as @${account.handle}`)
+        } else {
+          await initSession(account)
+          track('Sign In', {resumedSession: true})
+          setTimeout(() => {
+            Toast.show(`Signed in as @${account.handle}`)
+          }, 100)
+        }
       } else {
         onSelectAccount(account)
       }
     },
-    [track, initSession, onSelectAccount],
+    [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut],
   )
 
   return (
@@ -94,7 +118,12 @@ export const ChooseAccountForm = ({
         <Trans>Sign in as...</Trans>
       </Text>
       {accounts.map(account => (
-        <AccountItem key={account.did} account={account} onSelect={onSelect} />
+        <AccountItem
+          key={account.did}
+          account={account}
+          onSelect={onSelect}
+          isCurrentAccount={account.did === currentAccount?.did}
+        />
       ))}
       <TouchableOpacity
         testID="chooseNewAccountBtn"
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 885cd2a15..1a32d29c8 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -62,7 +62,7 @@ export function FeedPage({
   const onSoftReset = React.useCallback(() => {
     if (isPageFocused) {
       scrollToTop()
-      queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
+      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
       setHasNew(false)
     }
   }, [isPageFocused, scrollToTop, queryClient, feed, setHasNew])
@@ -83,7 +83,7 @@ export function FeedPage({
 
   const onPressLoadLatest = React.useCallback(() => {
     scrollToTop()
-    queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
+    queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
     setHasNew(false)
   }, [scrollToTop, feed, queryClient, setHasNew])
 
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index d8b67767b..1f2af069b 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -17,12 +17,14 @@ import {useModalControls} from '#/state/modals'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
+  usePinFeedMutation,
   UsePreferencesQueryResponse,
   usePreferencesQuery,
   useSaveFeedMutation,
   useRemoveFeedMutation,
 } from '#/state/queries/preferences'
 import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
+import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 
 export function FeedSourceCard({
   feedUri,
@@ -30,17 +32,27 @@ export function FeedSourceCard({
   showSaveBtn = false,
   showDescription = false,
   showLikes = false,
+  LoadingComponent,
+  pinOnSave = false,
 }: {
   feedUri: string
   style?: StyleProp<ViewStyle>
   showSaveBtn?: boolean
   showDescription?: boolean
   showLikes?: boolean
+  LoadingComponent?: JSX.Element
+  pinOnSave?: boolean
 }) {
   const {data: preferences} = usePreferencesQuery()
   const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
 
-  if (!feed || !preferences) return null
+  if (!feed || !preferences) {
+    return LoadingComponent ? (
+      LoadingComponent
+    ) : (
+      <FeedLoadingPlaceholder style={{flex: 1}} />
+    )
+  }
 
   return (
     <FeedSourceCardLoaded
@@ -50,6 +62,7 @@ export function FeedSourceCard({
       showSaveBtn={showSaveBtn}
       showDescription={showDescription}
       showLikes={showLikes}
+      pinOnSave={pinOnSave}
     />
   )
 }
@@ -61,6 +74,7 @@ export function FeedSourceCardLoaded({
   showSaveBtn = false,
   showDescription = false,
   showLikes = false,
+  pinOnSave = false,
 }: {
   feed: FeedSourceInfo
   preferences: UsePreferencesQueryResponse
@@ -68,6 +82,7 @@ export function FeedSourceCardLoaded({
   showSaveBtn?: boolean
   showDescription?: boolean
   showLikes?: boolean
+  pinOnSave?: boolean
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -78,6 +93,7 @@ export function FeedSourceCardLoaded({
     useSaveFeedMutation()
   const {isPending: isRemovePending, mutateAsync: removeFeed} =
     useRemoveFeedMutation()
+  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
 
   const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri))
 
@@ -103,14 +119,18 @@ export function FeedSourceCardLoaded({
       })
     } else {
       try {
-        await saveFeed({uri: feed.uri})
+        if (pinOnSave) {
+          await pinFeed({uri: feed.uri})
+        } else {
+          await saveFeed({uri: feed.uri})
+        }
         Toast.show('Added to my feeds')
       } catch (e) {
         Toast.show('There was an issue contacting your server')
         logger.error('Failed to save feed', {error: e})
       }
     }
-  }, [isSaved, openModal, feed, removeFeed, saveFeed, _])
+  }, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
 
   if (!feed || !preferences) return null
 
@@ -150,7 +170,7 @@ export function FeedSourceCardLoaded({
         {showSaveBtn && feed.type === 'feed' && (
           <View>
             <Pressable
-              disabled={isSavePending || isRemovePending}
+              disabled={isSavePending || isPinPending || isRemovePending}
               accessibilityRole="button"
               accessibilityLabel={
                 isSaved ? 'Remove from my feeds' : 'Add to my feeds'
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index ba88f78c0..c496d5f7c 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -35,15 +35,13 @@ export function Feed({
   const [isPTRing, setIsPTRing] = React.useState(false)
 
   const moderationOpts = useModerationOpts()
-  const {markAllRead} = useUnreadNotificationsApi()
+  const {markAllRead, checkUnread} = useUnreadNotificationsApi()
   const {
     data,
-    isLoading,
     isFetching,
     isFetched,
     isError,
     error,
-    refetch,
     hasNextPage,
     isFetchingNextPage,
     fetchNextPage,
@@ -52,13 +50,11 @@ export function Feed({
   const firstItem = data?.pages[0]?.items[0]
 
   // mark all read on fresh data
+  // (this will fire each time firstItem changes)
   React.useEffect(() => {
-    let cleanup
     if (firstItem) {
-      const to = setTimeout(() => markAllRead(), 250)
-      cleanup = () => clearTimeout(to)
+      markAllRead()
     }
-    return cleanup
   }, [firstItem, markAllRead])
 
   const items = React.useMemo(() => {
@@ -83,7 +79,7 @@ export function Feed({
   const onRefresh = React.useCallback(async () => {
     try {
       setIsPTRing(true)
-      await refetch()
+      await checkUnread({invalidate: true})
     } catch (err) {
       logger.error('Failed to refresh notifications feed', {
         error: err,
@@ -91,7 +87,7 @@ export function Feed({
     } finally {
       setIsPTRing(false)
     }
-  }, [refetch, setIsPTRing])
+  }, [checkUnread, setIsPTRing])
 
   const onEndReached = React.useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
@@ -136,21 +132,6 @@ export function Feed({
     [onPressRetryLoadMore, moderationOpts],
   )
 
-  const showHeaderSpinner = !isPTRing && isFetching && !isLoading
-  const FeedHeader = React.useCallback(
-    () => (
-      <View>
-        {ListHeaderComponent ? <ListHeaderComponent /> : null}
-        {showHeaderSpinner ? (
-          <View style={{padding: 10}}>
-            <ActivityIndicator />
-          </View>
-        ) : null}
-      </View>
-    ),
-    [ListHeaderComponent, showHeaderSpinner],
-  )
-
   const FeedFooter = React.useCallback(
     () =>
       isFetchingNextPage ? (
@@ -180,7 +161,7 @@ export function Feed({
         data={items}
         keyExtractor={item => item._reactKey}
         renderItem={renderItem}
-        ListHeaderComponent={FeedHeader}
+        ListHeaderComponent={ListHeaderComponent}
         ListFooterComponent={FeedFooter}
         refreshControl={
           <RefreshControl
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index a39499b24..57c83f17c 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -12,6 +12,9 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
 import {useSession} from '#/state/session'
 import {TextLink} from '#/view/com/util/Link'
 import {CenteredView} from '../util/Views'
+import {isWeb} from 'platform/detection'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
 
 export function FeedsTabBar(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@@ -79,11 +82,37 @@ function FeedsTabBarPublic() {
 function FeedsTabBarTablet(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
-  const feeds = usePinnedFeedsInfos()
+  const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
   const pal = usePalette('default')
+  const {hasSession} = useSession()
+  const navigation = useNavigation<NavigationProp>()
   const {headerMinimalShellTransform} = useMinimalShellMode()
   const {headerHeight} = useShellLayout()
-  const items = feeds.map(f => f.displayName)
+  const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
+  const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
+  const items = showFeedsLinkInTabBar
+    ? pinnedDisplayNames.concat('Feeds ✨')
+    : pinnedDisplayNames
+
+  const onPressDiscoverFeeds = React.useCallback(() => {
+    if (isWeb) {
+      navigation.navigate('Feeds')
+    } else {
+      navigation.navigate('FeedsTab')
+      navigation.popToTop()
+    }
+  }, [navigation])
+
+  const onSelect = React.useCallback(
+    (index: number) => {
+      if (showFeedsLinkInTabBar && index === items.length - 1) {
+        onPressDiscoverFeeds()
+      } else if (props.onSelect) {
+        props.onSelect(index)
+      }
+    },
+    [items.length, onPressDiscoverFeeds, props, showFeedsLinkInTabBar],
+  )
 
   return (
     // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
@@ -95,6 +124,7 @@ function FeedsTabBarTablet(
       <TabBar
         key={items.join(',')}
         {...props}
+        onSelect={onSelect}
         items={items}
         indicatorColor={pal.colors.link}
       />
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 2983a4575..882b6cfc5 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -18,6 +18,9 @@ import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {useSession} from '#/state/session'
 import {usePinnedFeedsInfos} from '#/state/queries/feed'
+import {isWeb} from 'platform/detection'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
 
 export function FeedsTabBar(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@@ -26,11 +29,36 @@ export function FeedsTabBar(
   const {isSandbox, hasSession} = useSession()
   const {_} = useLingui()
   const setDrawerOpen = useSetDrawerOpen()
-  const feeds = usePinnedFeedsInfos()
+  const navigation = useNavigation<NavigationProp>()
+  const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
   const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
   const {headerHeight} = useShellLayout()
   const {headerMinimalShellTransform} = useMinimalShellMode()
-  const items = feeds.map(f => f.displayName)
+  const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
+  const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
+  const items = showFeedsLinkInTabBar
+    ? pinnedDisplayNames.concat('Feeds ✨')
+    : pinnedDisplayNames
+
+  const onPressFeedsLink = React.useCallback(() => {
+    if (isWeb) {
+      navigation.navigate('Feeds')
+    } else {
+      navigation.navigate('FeedsTab')
+      navigation.popToTop()
+    }
+  }, [navigation])
+
+  const onSelect = React.useCallback(
+    (index: number) => {
+      if (showFeedsLinkInTabBar && index === items.length - 1) {
+        onPressFeedsLink()
+      } else if (props.onSelect) {
+        props.onSelect(index)
+      }
+    },
+    [items.length, onPressFeedsLink, props, showFeedsLinkInTabBar],
+  )
 
   const onPressAvi = React.useCallback(() => {
     setDrawerOpen(true)
@@ -84,7 +112,7 @@ export function FeedsTabBar(
           key={items.join(',')}
           onPressSelected={props.onPressSelected}
           selectedPage={props.selectedPage}
-          onSelect={props.onSelect}
+          onSelect={onSelect}
           testID={props.testID}
           items={items}
           indicatorColor={pal.colors.link}
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 3d2a3c55f..2c7640c43 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -108,6 +108,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
                 pointerEvents: isHeaderReady ? 'auto' : 'none',
               }}>
               <TabBar
+                testID={testID}
                 items={items}
                 selectedPage={currentPage}
                 onSelect={props.onSelect}
@@ -127,6 +128,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
         isMobile,
         onTabBarLayout,
         onHeaderOnlyLayout,
+        testID,
       ],
     )
 
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 0e08b22d8..c3a95c5c0 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -68,6 +68,7 @@ export function TabBar({
   return (
     <View testID={testID} style={[pal.view, styles.outer]}>
       <DraggableScrollView
+        testID={`${testID}-selector`}
         horizontal={true}
         showsHorizontalScrollIndicator={false}
         ref={scrollElRef}
@@ -76,6 +77,7 @@ export function TabBar({
           const selected = i === selectedPage
           return (
             <PressableWithHover
+              testID={`${testID}-selector-${i}`}
               key={item}
               onLayout={e => onItemLayout(e, i)}
               style={[styles.item, selected && indicatorStyle]}
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index fc6d77696..393c1bc91 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -23,6 +23,7 @@ import {
   FeedDescriptor,
   FeedParams,
   usePostFeedQuery,
+  pollLatest,
 } from '#/state/queries/post-feed'
 import {useModerationOpts} from '#/state/queries/preferences'
 
@@ -84,22 +85,21 @@ let Feed = ({
     hasNextPage,
     isFetchingNextPage,
     fetchNextPage,
-    pollLatest,
   } = usePostFeedQuery(feed, feedParams, opts)
   const isEmpty = !isFetching && !data?.pages[0]?.slices.length
 
   const checkForNew = React.useCallback(async () => {
-    if (!isFetched || isFetching || !onHasNew) {
+    if (!data?.pages[0] || isFetching || !onHasNew) {
       return
     }
     try {
-      if (await pollLatest()) {
+      if (await pollLatest(data.pages[0])) {
         onHasNew(true)
       }
     } catch (e) {
       logger.error('Poll latest failed', {feed, error: String(e)})
     }
-  }, [feed, isFetched, isFetching, pollLatest, onHasNew])
+  }, [feed, data, isFetching, onHasNew])
 
   React.useEffect(() => {
     // we store the interval handler in a ref to avoid needless
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index 5a9290f66..63d9d5956 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -17,28 +17,15 @@ import {EmptyState} from '../util/EmptyState'
 import {cleanError} from '#/lib/strings/errors'
 import {useRemoveFeedMutation} from '#/state/queries/preferences'
 
-enum KnownError {
-  Block,
-  FeedgenDoesNotExist,
-  FeedgenMisconfigured,
-  FeedgenBadResponse,
-  FeedgenOffline,
-  FeedgenUnknown,
-  Unknown,
-}
-
-const MESSAGES = {
-  [KnownError.Unknown]: '',
-  [KnownError.Block]: '',
-  [KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`,
-  [KnownError.FeedgenMisconfigured]:
-    'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.',
-  [KnownError.FeedgenBadResponse]:
-    'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.',
-  [KnownError.FeedgenOffline]:
-    'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.',
-  [KnownError.FeedgenUnknown]:
-    'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.',
+export enum KnownError {
+  Block = 'Block',
+  FeedgenDoesNotExist = 'FeedgenDoesNotExist',
+  FeedgenMisconfigured = 'FeedgenMisconfigured',
+  FeedgenBadResponse = 'FeedgenBadResponse',
+  FeedgenOffline = 'FeedgenOffline',
+  FeedgenUnknown = 'FeedgenUnknown',
+  FeedNSFPublic = 'FeedNSFPublic',
+  Unknown = 'Unknown',
 }
 
 export function FeedErrorMessage({
@@ -90,7 +77,32 @@ function FeedgenErrorMessage({
   const pal = usePalette('default')
   const {_: _l} = useLingui()
   const navigation = useNavigation<NavigationProp>()
-  const msg = MESSAGES[knownError]
+  const msg = React.useMemo(
+    () =>
+      ({
+        [KnownError.Unknown]: '',
+        [KnownError.Block]: '',
+        [KnownError.FeedgenDoesNotExist]: _l(
+          msgLingui`Hmmm, we're having trouble finding this feed. It may have been deleted.`,
+        ),
+        [KnownError.FeedgenMisconfigured]: _l(
+          msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`,
+        ),
+        [KnownError.FeedgenBadResponse]: _l(
+          msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`,
+        ),
+        [KnownError.FeedgenOffline]: _l(
+          msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`,
+        ),
+        [KnownError.FeedNSFPublic]: _l(
+          msgLingui`We're sorry, but this content is not viewable without a Bluesky account.`,
+        ),
+        [KnownError.FeedgenUnknown]: _l(
+          msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`,
+        ),
+      }[knownError]),
+    [_l, knownError],
+  )
   const [_, uri] = feedDesc.split('|')
   const [ownerDid] = safeParseFeedgenUri(uri)
   const {openModal, closeModal} = useModalControls()
@@ -121,6 +133,36 @@ function FeedgenErrorMessage({
     })
   }, [openModal, closeModal, uri, removeFeed, _l])
 
+  const cta = React.useMemo(() => {
+    switch (knownError) {
+      case KnownError.FeedNSFPublic: {
+        return null
+      }
+      case KnownError.FeedgenDoesNotExist:
+      case KnownError.FeedgenMisconfigured:
+      case KnownError.FeedgenBadResponse:
+      case KnownError.FeedgenOffline:
+      case KnownError.FeedgenUnknown: {
+        return (
+          <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
+            {knownError === KnownError.FeedgenDoesNotExist && (
+              <Button
+                type="inverted"
+                label="Remove feed"
+                onPress={onRemoveFeed}
+              />
+            )}
+            <Button
+              type="default-light"
+              label="View profile"
+              onPress={onViewProfile}
+            />
+          </View>
+        )
+      }
+    }
+  }, [knownError, onViewProfile, onRemoveFeed])
+
   return (
     <View
       style={[
@@ -134,16 +176,7 @@ function FeedgenErrorMessage({
         },
       ]}>
       <Text style={pal.text}>{msg}</Text>
-      <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
-        {knownError === KnownError.FeedgenDoesNotExist && (
-          <Button type="inverted" label="Remove feed" onPress={onRemoveFeed} />
-        )}
-        <Button
-          type="default-light"
-          label="View profile"
-          onPress={onViewProfile}
-        />
-      </View>
+      {cta}
     </View>
   )
 }
@@ -196,5 +229,8 @@ function detectKnownError(
   if (error.includes('feed provided an invalid response')) {
     return KnownError.FeedgenBadResponse
   }
+  if (error.includes(KnownError.FeedNSFPublic)) {
+    return KnownError.FeedNSFPublic
+  }
   return KnownError.FeedgenUnknown
 }
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 074ab2329..dcbec7cb4 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
   noFeedback?: boolean
   asAnchor?: boolean
   anchorNoUnderline?: boolean
+  navigationAction?: 'push' | 'replace' | 'navigate'
 }
 
 export const Link = memo(function Link({
@@ -58,6 +59,7 @@ export const Link = memo(function Link({
   asAnchor,
   accessible,
   anchorNoUnderline,
+  navigationAction,
   ...props
 }: Props) {
   const {closeModal} = useModalControls()
@@ -67,10 +69,16 @@ export const Link = memo(function Link({
   const onPress = React.useCallback(
     (e?: Event) => {
       if (typeof href === 'string') {
-        return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
+        return onPressInner(
+          closeModal,
+          navigation,
+          sanitizeUrl(href),
+          navigationAction,
+          e,
+        )
       }
     },
-    [closeModal, navigation, href],
+    [closeModal, navigation, navigationAction, href],
   )
 
   if (noFeedback) {
@@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
   title,
   onPress,
   warnOnMismatchingLabel,
+  navigationAction,
   ...orgProps
 }: {
   testID?: string
@@ -158,6 +167,7 @@ export const TextLink = memo(function TextLink({
   dataSet?: any
   title?: string
   warnOnMismatchingLabel?: boolean
+  navigationAction?: 'push' | 'replace' | 'navigate'
 } & TextProps) {
   const {...props} = useLinkProps({to: sanitizeUrl(href)})
   const navigation = useNavigation<NavigationProp>()
@@ -185,7 +195,13 @@ export const TextLink = memo(function TextLink({
         // @ts-ignore function signature differs by platform -prf
         return onPress()
       }
-      return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
+      return onPressInner(
+        closeModal,
+        navigation,
+        sanitizeUrl(href),
+        navigationAction,
+        e,
+      )
     },
     [
       onPress,
@@ -195,6 +211,7 @@ export const TextLink = memo(function TextLink({
       href,
       text,
       warnOnMismatchingLabel,
+      navigationAction,
     ],
   )
   const hrefAttrs = useMemo(() => {
@@ -241,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
   accessibilityLabel?: string
   accessibilityHint?: string
   title?: string
+  navigationAction?: 'push' | 'replace' | 'navigate'
 }
 export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   testID,
@@ -250,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
   text,
   numberOfLines,
   lineHeight,
+  navigationAction,
   ...props
 }: TextLinkOnWebOnlyProps) {
   if (isWeb) {
@@ -263,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
         numberOfLines={numberOfLines}
         lineHeight={lineHeight}
         title={props.title}
+        navigationAction={navigationAction}
         {...props}
       />
     )
@@ -296,6 +316,7 @@ function onPressInner(
   closeModal = () => {},
   navigation: NavigationProp,
   href: string,
+  navigationAction: 'push' | 'replace' | 'navigate' = 'push',
   e?: Event,
 ) {
   let shouldHandle = false
@@ -328,8 +349,18 @@ function onPressInner(
     } else {
       closeModal() // close any active modals
 
-      // @ts-ignore we're not able to type check on this one -prf
-      navigation.dispatch(StackActions.push(...router.matchPath(href)))
+      if (navigationAction === 'push') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.dispatch(StackActions.push(...router.matchPath(href)))
+      } else if (navigationAction === 'replace') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.dispatch(StackActions.replace(...router.matchPath(href)))
+      } else if (navigationAction === 'navigate') {
+        // @ts-ignore we're not able to type check on this one -prf
+        navigation.navigate(...router.matchPath(href))
+      } else {
+        throw Error('Unsupported navigator action.')
+      }
     }
   }
 }
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 461cbcbe5..74e36ff7b 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -171,14 +171,22 @@ export function ProfileCardFeedLoadingPlaceholder() {
 
 export function FeedLoadingPlaceholder({
   style,
+  showLowerPlaceholder = true,
+  showTopBorder = true,
 }: {
   style?: StyleProp<ViewStyle>
+  showTopBorder?: boolean
+  showLowerPlaceholder?: boolean
 }) {
   const pal = usePalette('default')
   return (
     <View
       style={[
-        {paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
+        {
+          paddingHorizontal: 12,
+          paddingVertical: 18,
+          borderTopWidth: showTopBorder ? 1 : 0,
+        },
         pal.border,
         style,
       ]}>
@@ -193,14 +201,16 @@ export function FeedLoadingPlaceholder({
           <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>
+      {showLowerPlaceholder && (
+        <View style={{paddingHorizontal: 5}}>
+          <LoadingPlaceholder
+            width={260}
+            height={8}
+            style={{marginVertical: 12}}
+          />
+          <LoadingPlaceholder width={120} height={8} />
+        </View>
+      )}
     </View>
   )
 }
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index ced8592c5..f319fbc39 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -437,6 +437,7 @@ export function FeedsScreen(_props: Props) {
             showSaveBtn={hasSession}
             showDescription
             showLikes
+            pinOnSave
           />
         )
       } else if (item.type === 'popularFeedsNoResults') {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 28f01b683..e5a3035a4 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -40,6 +40,12 @@ function HomeScreenReady({
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
   const [selectedPage, setSelectedPage] = React.useState(0)
 
+  /**
+   * Used to ensure that we re-compute `customFeeds` AND force a re-render of
+   * the pager with the new order of feeds.
+   */
+  const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
+
   const customFeeds = React.useMemo(() => {
     const pinned = preferences.feeds.pinned
     const feeds: FeedDescriptor[] = []
@@ -83,7 +89,6 @@ function HomeScreenReady({
     emitSoftReset()
   }, [])
 
-  // TODO(pwi) may need this in public view
   const onPageScrollStateChanged = React.useCallback(
     (state: 'idle' | 'dragging' | 'settling') => {
       if (state === 'dragging') {
@@ -118,6 +123,7 @@ function HomeScreenReady({
 
   return hasSession ? (
     <Pager
+      key={pinnedFeedOrderKey}
       testID="homeScreen"
       onPageSelected={onPageSelected}
       onPageScrollStateChanged={onPageScrollStateChanged}
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 8516d1667..0f442038b 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -19,7 +19,10 @@ import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {
+  useUnreadNotifications,
+  useUnreadNotificationsApi,
+} from '#/state/queries/notifications/unread'
 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
 import {listenSoftReset, emitSoftReset} from '#/state/events'
 
@@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) {
   const {screen} = useAnalytics()
   const pal = usePalette('default')
   const {isDesktop} = useWebMediaQueries()
-  const unreadNotifs = useUnreadNotifications()
   const queryClient = useQueryClient()
+  const unreadNotifs = useUnreadNotifications()
+  const unreadApi = useUnreadNotificationsApi()
   const hasNew = !!unreadNotifs
 
   // event handlers
@@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) {
 
   const onPressLoadLatest = React.useCallback(() => {
     scrollToTop()
-    queryClient.invalidateQueries({
-      queryKey: NOTIFS_RQKEY(),
-    })
-  }, [scrollToTop, queryClient])
+    if (hasNew) {
+      // render what we have now
+      queryClient.resetQueries({
+        queryKey: NOTIFS_RQKEY(),
+      })
+    } else {
+      // check with the server
+      unreadApi.checkUnread({invalidate: true})
+    }
+  }, [scrollToTop, queryClient, unreadApi, hasNew])
 
   // on-visible setup
   // =
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 7ddcf17af..3e9a59929 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -267,6 +267,7 @@ function ProfileScreenLoaded({
       screenDescription="profile"
       moderation={moderation.account}>
       <PagerWithHeader
+        testID="profilePager"
         isHeaderReady={true}
         items={sectionTitles}
         onPageSelected={onPageSelected}
@@ -403,7 +404,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
 
     const onScrollToTop = React.useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
+      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
       setHasNew(false)
     }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
     React.useImperativeHandle(ref, () => ({
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 95589b22a..e38543e6b 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -353,6 +353,7 @@ export function ProfileFeedScreenInner({
               style={styles.btn}
             />
             <Button
+              testID={isPinned ? 'unpinBtn' : 'pinBtn'}
               disabled={isPinPending || isUnpinPending}
               type={isPinned ? 'default' : 'inverted'}
               label={isPinned ? 'Unpin' : 'Pin to home'}
@@ -501,7 +502,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
+      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
       setHasNew(false)
     }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
 
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index cc6d85e6f..9be499561 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -127,7 +127,7 @@ function ProfileListScreenLoaded({
       list,
       onChange() {
         if (isCurateList) {
-          queryClient.invalidateQueries({
+          queryClient.resetQueries({
             // TODO(eric) should construct these strings with a fn too
             queryKey: FEED_RQKEY(`list|${list.uri}`),
           })
@@ -530,7 +530,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: -headerHeight})
-      queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
+      queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
       setHasNew(false)
     }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
     React.useImperativeHandle(ref, () => ({
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 640d76a5c..858a58a3c 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,14 +1,7 @@
 import React from 'react'
-import {
-  StyleSheet,
-  View,
-  ActivityIndicator,
-  Pressable,
-  TouchableOpacity,
-} from 'react-native'
+import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useQueryClient} from '@tanstack/react-query'
 import {track} from '#/lib/analytics/analytics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -32,9 +25,8 @@ import {
   usePinFeedMutation,
   useUnpinFeedMutation,
   useSetSaveFeedsMutation,
-  preferencesQueryKey,
-  UsePreferencesQueryResponse,
 } from '#/state/queries/preferences'
+import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -57,6 +49,24 @@ export function SavedFeeds({}: Props) {
   const {screen} = useAnalytics()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {data: preferences} = usePreferencesQuery()
+  const {
+    mutateAsync: setSavedFeeds,
+    variables: optimisticSavedFeedsResponse,
+    reset: resetSaveFeedsMutationState,
+    error: setSavedFeedsError,
+  } = useSetSaveFeedsMutation()
+
+  /*
+   * Use optimistic data if exists and no error, otherwise fallback to remote
+   * data
+   */
+  const currentFeeds =
+    optimisticSavedFeedsResponse && !setSavedFeedsError
+      ? optimisticSavedFeedsResponse
+      : preferences?.feeds || {saved: [], pinned: []}
+  const unpinned = currentFeeds.saved.filter(f => {
+    return !currentFeeds.pinned?.includes(f)
+  })
 
   useFocusEffect(
     React.useCallback(() => {
@@ -80,7 +90,7 @@ export function SavedFeeds({}: Props) {
           </Text>
         </View>
         {preferences?.feeds ? (
-          !preferences.feeds.pinned.length ? (
+          !currentFeeds.pinned.length ? (
             <View
               style={[
                 pal.border,
@@ -93,8 +103,15 @@ export function SavedFeeds({}: Props) {
               </Text>
             </View>
           ) : (
-            preferences?.feeds?.pinned?.map(uri => (
-              <ListItem key={uri} feedUri={uri} isPinned />
+            currentFeeds.pinned.map(uri => (
+              <ListItem
+                key={uri}
+                feedUri={uri}
+                isPinned
+                setSavedFeeds={setSavedFeeds}
+                resetSaveFeedsMutationState={resetSaveFeedsMutationState}
+                currentFeeds={currentFeeds}
+              />
             ))
           )
         ) : (
@@ -106,7 +123,7 @@ export function SavedFeeds({}: Props) {
           </Text>
         </View>
         {preferences?.feeds ? (
-          !preferences.feeds.unpinned.length ? (
+          !unpinned.length ? (
             <View
               style={[
                 pal.border,
@@ -119,8 +136,15 @@ export function SavedFeeds({}: Props) {
               </Text>
             </View>
           ) : (
-            preferences.feeds.unpinned.map(uri => (
-              <ListItem key={uri} feedUri={uri} isPinned={false} />
+            unpinned.map(uri => (
+              <ListItem
+                key={uri}
+                feedUri={uri}
+                isPinned={false}
+                setSavedFeeds={setSavedFeeds}
+                resetSaveFeedsMutationState={resetSaveFeedsMutationState}
+                currentFeeds={currentFeeds}
+              />
             ))
           )
         ) : (
@@ -151,22 +175,30 @@ export function SavedFeeds({}: Props) {
 function ListItem({
   feedUri,
   isPinned,
+  currentFeeds,
+  setSavedFeeds,
+  resetSaveFeedsMutationState,
 }: {
   feedUri: string // uri
   isPinned: boolean
+  currentFeeds: {saved: string[]; pinned: string[]}
+  setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
+  resetSaveFeedsMutationState: ReturnType<
+    typeof useSetSaveFeedsMutation
+  >['reset']
 }) {
   const pal = usePalette('default')
-  const queryClient = useQueryClient()
   const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
   const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
     useUnpinFeedMutation()
-  const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
-    useSetSaveFeedsMutation()
+  const isPending = isPinPending || isUnpinPending
 
   const onTogglePinned = React.useCallback(async () => {
     Haptics.default()
 
     try {
+      resetSaveFeedsMutationState()
+
       if (isPinned) {
         await unpinFeed({uri: feedUri})
       } else {
@@ -176,23 +208,20 @@ function ListItem({
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to toggle pinned feed', {error: e})
     }
-  }, [feedUri, isPinned, pinFeed, unpinFeed])
+  }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
 
   const onPressUp = React.useCallback(async () => {
     if (!isPinned) return
 
-    const feeds =
-      queryClient.getQueryData<UsePreferencesQueryResponse>(
-        preferencesQueryKey,
-      )?.feeds
-    const pinned = feeds?.pinned ?? []
+    // create new array, do not mutate
+    const pinned = [...currentFeeds.pinned]
     const index = pinned.indexOf(feedUri)
 
     if (index === -1 || index === 0) return
     ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
 
     try {
-      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      await setSavedFeeds({saved: currentFeeds.saved, pinned})
       track('CustomFeed:Reorder', {
         uri: feedUri,
         index: pinned.indexOf(feedUri),
@@ -201,23 +230,19 @@ function ListItem({
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to set pinned feed order', {error: e})
     }
-  }, [feedUri, isPinned, queryClient, setSavedFeeds])
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
 
   const onPressDown = React.useCallback(async () => {
     if (!isPinned) return
 
-    const feeds =
-      queryClient.getQueryData<UsePreferencesQueryResponse>(
-        preferencesQueryKey,
-      )?.feeds
-    const pinned = feeds?.pinned ?? []
+    const pinned = [...currentFeeds.pinned]
     const index = pinned.indexOf(feedUri)
 
     if (index === -1 || index >= pinned.length - 1) return
     ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
 
     try {
-      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      await setSavedFeeds({saved: currentFeeds.saved, pinned})
       track('CustomFeed:Reorder', {
         uri: feedUri,
         index: pinned.indexOf(feedUri),
@@ -226,7 +251,7 @@ function ListItem({
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to set pinned feed order', {error: e})
     }
-  }, [feedUri, isPinned, queryClient, setSavedFeeds])
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
 
   return (
     <Pressable
@@ -234,24 +259,30 @@ function ListItem({
       style={[styles.itemContainer, pal.border]}>
       {isPinned ? (
         <View style={styles.webArrowButtonsContainer}>
-          <TouchableOpacity
-            disabled={isMovePending}
+          <Pressable
+            disabled={isPending}
             accessibilityRole="button"
             onPress={onPressUp}
-            hitSlop={HITSLOP_TOP}>
+            hitSlop={HITSLOP_TOP}
+            style={state => ({
+              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+            })}>
             <FontAwesomeIcon
               icon="arrow-up"
               size={12}
               style={[pal.text, styles.webArrowUpButton]}
             />
-          </TouchableOpacity>
-          <TouchableOpacity
-            disabled={isMovePending}
+          </Pressable>
+          <Pressable
+            disabled={isPending}
             accessibilityRole="button"
             onPress={onPressDown}
-            hitSlop={HITSLOP_BOTTOM}>
+            hitSlop={HITSLOP_BOTTOM}
+            style={state => ({
+              opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+            })}>
             <FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
-          </TouchableOpacity>
+          </Pressable>
         </View>
       ) : null}
       <FeedSourceCard
@@ -259,18 +290,28 @@ function ListItem({
         feedUri={feedUri}
         style={styles.noBorder}
         showSaveBtn
+        LoadingComponent={
+          <FeedLoadingPlaceholder
+            style={{flex: 1}}
+            showLowerPlaceholder={false}
+            showTopBorder={false}
+          />
+        }
       />
-      <TouchableOpacity
-        disabled={isPinPending || isUnpinPending}
+      <Pressable
+        disabled={isPending}
         accessibilityRole="button"
         hitSlop={10}
-        onPress={onTogglePinned}>
+        onPress={onTogglePinned}
+        style={state => ({
+          opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
+        })}>
         <FontAwesomeIcon
           icon="thumb-tack"
           size={20}
           color={isPinned ? colors.blue3 : pal.colors.icon}
         />
-      </TouchableOpacity>
+      </Pressable>
     </Pressable>
   )
 }
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 944f5e81a..6e2f8dbfe 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -10,11 +10,7 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {
-  useFocusEffect,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -73,6 +69,8 @@ import {STATUS_PAGE_URL} from 'lib/constants'
 import {Plural, Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
@@ -154,13 +152,14 @@ export function SettingsScreen({}: Props) {
   const {screen, track} = useAnalytics()
   const {openModal} = useModalControls()
   const {isSwitchingAccounts, accounts, currentAccount} = useSession()
-  const {clearCurrentAccount} = useSessionApi()
   const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
     getAgent(),
   )
   const {mutate: clearPreferences} = useClearPreferencesMutation()
   const {data: invites} = useInviteCodesQuery()
   const invitesAvailable = invites?.available?.length ?? 0
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+  const closeAllActiveElements = useCloseAllActiveElements()
 
   const primaryBg = useCustomPalette<ViewStyle>({
     light: {backgroundColor: colors.blue0},
@@ -189,10 +188,9 @@ export function SettingsScreen({}: Props) {
 
   const onPressAddAccount = React.useCallback(() => {
     track('Settings:AddAccountButtonClicked')
-    navigation.navigate('HomeTab')
-    navigation.dispatch(StackActions.popToTop())
-    clearCurrentAccount()
-  }, [track, navigation, clearCurrentAccount])
+    setShowLoggedOut(true)
+    closeAllActiveElements()
+  }, [track, setShowLoggedOut, closeAllActiveElements])
 
   const onPressChangeHandle = React.useCallback(() => {
     track('Settings:ChangeHandleButtonClicked')
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index ee6c980b8..a72f3fe36 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -141,7 +141,7 @@ export function DrawerContent() {
         } else {
           if (tab === 'Notifications') {
             // fetch new notifs on view
-            queryClient.invalidateQueries({
+            queryClient.resetQueries({
               queryKey: NOTIFS_RQKEY(),
             })
           }
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index dfb18cc4a..a97ff8afc 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -62,7 +62,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
       } else {
         if (tab === 'Notifications') {
           // fetch new notifs on view
-          queryClient.invalidateQueries({
+          queryClient.resetQueries({
             queryKey: NOTIFS_RQKEY(),
           })
         }
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index 8efd7b6b0..3a60bd3b1 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -137,7 +137,7 @@ const NavItem: React.FC<{
       : isTab(currentRoute.name, routeName)
 
   return (
-    <Link href={href} style={styles.ctrl}>
+    <Link href={href} style={styles.ctrl} navigationAction="navigate">
       {children({isActive})}
     </Link>
   )
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index eeeca4fd8..ff51ffe22 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -11,7 +11,7 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
 export function DesktopFeeds() {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const feeds = usePinnedFeedsInfos()
+  const {feeds} = usePinnedFeedsInfos()
 
   const route = useNavigationState(state => {
     if (!state) {
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index a0052e0ca..8daa381d5 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -150,7 +150,7 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
       } else {
         if (href === '/notifications') {
           // fetch new notifs on view
-          queryClient.invalidateQueries({
+          queryClient.resetQueries({
             queryKey: NOTIFS_RQKEY(),
           })
         }