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/notifications/Feed.tsx181
-rw-r--r--src/view/com/notifications/FeedItem.tsx167
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx4
-rw-r--r--src/view/com/post/Post.tsx272
-rw-r--r--src/view/com/posts/Feed.tsx10
-rw-r--r--src/view/com/posts/FeedItem.tsx2
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx98
-rw-r--r--src/view/com/util/forms/PostDropdownBtn2.tsx210
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx186
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls2.tsx200
-rw-r--r--src/view/screens/Notifications.tsx55
-rw-r--r--src/view/shell/Drawer.tsx10
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx10
-rw-r--r--src/view/shell/desktop/LeftNav.tsx4
14 files changed, 457 insertions, 952 deletions
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 4794a9867..e82c654be 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -1,8 +1,6 @@
 import React, {MutableRefObject} from 'react'
-import {observer} from 'mobx-react-lite'
 import {CenteredView, FlatList} from '../util/Views'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
-import {NotificationsFeedModel} from 'state/models/feeds/notifications'
 import {FeedItem} from './FeedItem'
 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -12,20 +10,22 @@ import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
+import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
+import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
 import {logger} from '#/logger'
+import {cleanError} from '#/lib/strings/errors'
+import {useModerationOpts} from '#/state/queries/preferences'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
-const LOADING_SPINNER = {_reactKey: '__loading_spinner__'}
+const LOADING_ITEM = {_reactKey: '__loading__'}
 
-export const Feed = observer(function Feed({
-  view,
+export function Feed({
   scrollElRef,
   onPressTryAgain,
   onScroll,
   ListHeaderComponent,
 }: {
-  view: NotificationsFeedModel
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
   onScroll?: OnScrollHandler
@@ -33,35 +33,54 @@ export const Feed = observer(function Feed({
 }) {
   const pal = usePalette('default')
   const [isPTRing, setIsPTRing] = React.useState(false)
-  const data = React.useMemo(() => {
-    let feedItems: any[] = []
-    if (view.isRefreshing && !isPTRing) {
-      feedItems = [LOADING_SPINNER]
+
+  const moderationOpts = useModerationOpts()
+  const {markAllRead} = useUnreadNotificationsApi()
+  const {
+    data,
+    dataUpdatedAt,
+    isFetching,
+    isFetched,
+    isError,
+    error,
+    refetch,
+    hasNextPage,
+    isFetchingNextPage,
+    fetchNextPage,
+  } = useNotificationFeedQuery({enabled: !!moderationOpts})
+  const isEmpty = !isFetching && !data?.pages[0]?.items.length
+  const firstItem = data?.pages[0]?.items[0]
+
+  // mark all read on fresh data
+  React.useEffect(() => {
+    if (firstItem) {
+      markAllRead()
     }
-    if (view.hasLoaded) {
-      if (view.isEmpty) {
-        feedItems = feedItems.concat([EMPTY_FEED_ITEM])
-      } else {
-        feedItems = feedItems.concat(view.notifications)
+  }, [firstItem, markAllRead])
+
+  const items = React.useMemo(() => {
+    let arr: any[] = []
+    if (isFetched) {
+      if (isEmpty) {
+        arr = arr.concat([EMPTY_FEED_ITEM])
+      } else if (data) {
+        for (const page of data?.pages) {
+          arr = arr.concat(page.items)
+        }
       }
+      if (isError && !isEmpty) {
+        arr = arr.concat([LOAD_MORE_ERROR_ITEM])
+      }
+    } else {
+      arr.push(LOADING_ITEM)
     }
-    if (view.loadMoreError) {
-      feedItems = (feedItems || []).concat([LOAD_MORE_ERROR_ITEM])
-    }
-    return feedItems
-  }, [
-    view.hasLoaded,
-    view.isEmpty,
-    view.notifications,
-    view.loadMoreError,
-    view.isRefreshing,
-    isPTRing,
-  ])
+    return arr
+  }, [isFetched, isError, isEmpty, data])
 
   const onRefresh = React.useCallback(async () => {
     try {
       setIsPTRing(true)
-      await view.refresh()
+      await refetch()
     } catch (err) {
       logger.error('Failed to refresh notifications feed', {
         error: err,
@@ -69,21 +88,21 @@ export const Feed = observer(function Feed({
     } finally {
       setIsPTRing(false)
     }
-  }, [view, setIsPTRing])
+  }, [refetch, setIsPTRing])
 
   const onEndReached = React.useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
+
     try {
-      await view.loadMore()
+      await fetchNextPage()
     } catch (err) {
-      logger.error('Failed to load more notifications', {
-        error: err,
-      })
+      logger.error('Failed to load more notifications', {error: err})
     }
-  }, [view])
+  }, [isFetching, hasNextPage, isError, fetchNextPage])
 
   const onPressRetryLoadMore = React.useCallback(() => {
-    view.retryLoadMore()
-  }, [view])
+    fetchNextPage()
+  }, [fetchNextPage])
 
   // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
   //   VirtualizedList: You have a large list that is slow to update - make sure your
@@ -106,78 +125,72 @@ export const Feed = observer(function Feed({
             onPress={onPressRetryLoadMore}
           />
         )
-      } else if (item === LOADING_SPINNER) {
-        return (
-          <View style={styles.loading}>
-            <ActivityIndicator size="small" />
-          </View>
-        )
+      } else if (item === LOADING_ITEM) {
+        return <NotificationFeedLoadingPlaceholder />
       }
-      return <FeedItem item={item} />
+      return (
+        <FeedItem
+          item={item}
+          dataUpdatedAt={dataUpdatedAt}
+          moderationOpts={moderationOpts!}
+        />
+      )
     },
-    [onPressRetryLoadMore],
+    [onPressRetryLoadMore, dataUpdatedAt, moderationOpts],
   )
 
   const FeedFooter = React.useCallback(
     () =>
-      view.isLoading ? (
+      isFetchingNextPage ? (
         <View style={styles.feedFooter}>
           <ActivityIndicator />
         </View>
       ) : (
         <View />
       ),
-    [view],
+    [isFetchingNextPage],
   )
 
   const scrollHandler = useAnimatedScrollHandler(onScroll || {})
   return (
     <View style={s.hContentRegion}>
-      <CenteredView>
-        {view.isLoading && !data.length && (
-          <NotificationFeedLoadingPlaceholder />
-        )}
-        {view.hasError && (
+      {error && (
+        <CenteredView>
           <ErrorMessage
-            message={view.error}
+            message={cleanError(error)}
             onPressTryAgain={onPressTryAgain}
           />
-        )}
-      </CenteredView>
-      {data.length ? (
-        <FlatList
-          testID="notifsFeed"
-          ref={scrollElRef}
-          data={data}
-          keyExtractor={item => item._reactKey}
-          renderItem={renderItem}
-          ListHeaderComponent={ListHeaderComponent}
-          ListFooterComponent={FeedFooter}
-          refreshControl={
-            <RefreshControl
-              refreshing={isPTRing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-            />
-          }
-          onEndReached={onEndReached}
-          onEndReachedThreshold={0.6}
-          onScroll={scrollHandler}
-          scrollEventThrottle={1}
-          contentContainerStyle={s.contentContainer}
-          // @ts-ignore our .web version only -prf
-          desktopFixedHeight
-        />
-      ) : null}
+        </CenteredView>
+      )}
+      <FlatList
+        testID="notifsFeed"
+        ref={scrollElRef}
+        data={items}
+        keyExtractor={item => item._reactKey}
+        renderItem={renderItem}
+        ListHeaderComponent={ListHeaderComponent}
+        ListFooterComponent={FeedFooter}
+        refreshControl={
+          <RefreshControl
+            refreshing={isPTRing}
+            onRefresh={onRefresh}
+            tintColor={pal.colors.text}
+            titleColor={pal.colors.text}
+          />
+        }
+        onEndReached={onEndReached}
+        onEndReachedThreshold={0.6}
+        onScroll={scrollHandler}
+        scrollEventThrottle={1}
+        contentContainerStyle={s.contentContainer}
+        // @ts-ignore our .web version only -prf
+        desktopFixedHeight
+      />
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
-  loading: {
-    paddingVertical: 20,
-  },
   feedFooter: {paddingTop: 20},
   emptyState: {paddingVertical: 40},
 })
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 0387ed38d..dd785a682 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -1,5 +1,4 @@
 import React, {useMemo, useState, useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
 import {
   Animated,
   TouchableOpacity,
@@ -9,6 +8,9 @@ import {
 } from 'react-native'
 import {
   AppBskyEmbedImages,
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  ModerationOpts,
   ProfileModeration,
   moderateProfile,
   AppBskyEmbedRecordWithMedia,
@@ -19,8 +21,7 @@ import {
   FontAwesomeIconStyle,
   Props,
 } from '@fortawesome/react-native-fontawesome'
-import {NotificationsFeedItemModel} from 'state/models/feeds/notifications'
-import {PostThreadModel} from 'state/models/content/post-thread'
+import {FeedNotification} from '#/state/queries/notifications/feed'
 import {s, colors} from 'lib/styles'
 import {niceDate} from 'lib/strings/time'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -33,7 +34,6 @@ import {UserPreviewLink} from '../util/UserPreviewLink'
 import {ImageHorzList} from '../util/images/ImageHorzList'
 import {Post} from '../post/Post'
 import {Link, TextLink} from '../util/Link'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {formatCount} from '../util/numeric/format'
@@ -56,40 +56,36 @@ interface Author {
   moderation: ProfileModeration
 }
 
-export const FeedItem = observer(function FeedItemImpl({
+export function FeedItem({
   item,
+  dataUpdatedAt,
+  moderationOpts,
 }: {
-  item: NotificationsFeedItemModel
+  item: FeedNotification
+  dataUpdatedAt: number
+  moderationOpts: ModerationOpts
 }) {
-  const store = useStores()
   const pal = usePalette('default')
   const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
   const itemHref = useMemo(() => {
-    if (item.isLike || item.isRepost) {
-      const urip = new AtUri(item.subjectUri)
-      return `/profile/${urip.host}/post/${urip.rkey}`
-    } else if (item.isFollow) {
-      return makeProfileLink(item.author)
-    } else if (item.isReply) {
-      const urip = new AtUri(item.uri)
+    if (item.type === 'post-like' || item.type === 'repost') {
+      if (item.subjectUri) {
+        const urip = new AtUri(item.subjectUri)
+        return `/profile/${urip.host}/post/${urip.rkey}`
+      }
+    } else if (item.type === 'follow') {
+      return makeProfileLink(item.notification.author)
+    } else if (item.type === 'reply') {
+      const urip = new AtUri(item.notification.uri)
       return `/profile/${urip.host}/post/${urip.rkey}`
-    } else if (item.isCustomFeedLike) {
-      const urip = new AtUri(item.subjectUri)
-      return `/profile/${urip.host}/feed/${urip.rkey}`
+    } else if (item.type === 'feedgen-like') {
+      if (item.subjectUri) {
+        const urip = new AtUri(item.subjectUri)
+        return `/profile/${urip.host}/feed/${urip.rkey}`
+      }
     }
     return ''
   }, [item])
-  const itemTitle = useMemo(() => {
-    if (item.isLike || item.isRepost) {
-      return 'Post'
-    } else if (item.isFollow) {
-      return item.author.handle
-    } else if (item.isReply) {
-      return 'Post'
-    } else if (item.isCustomFeedLike) {
-      return 'Custom Feed'
-    }
-  }, [item])
 
   const onToggleAuthorsExpanded = () => {
     setAuthorsExpanded(currentlyExpanded => !currentlyExpanded)
@@ -98,15 +94,12 @@ export const FeedItem = observer(function FeedItemImpl({
   const authors: Author[] = useMemo(() => {
     return [
       {
-        href: makeProfileLink(item.author),
-        did: item.author.did,
-        handle: item.author.handle,
-        displayName: item.author.displayName,
-        avatar: item.author.avatar,
-        moderation: moderateProfile(
-          item.author,
-          store.preferences.moderationOpts,
-        ),
+        href: makeProfileLink(item.notification.author),
+        did: item.notification.author.did,
+        handle: item.notification.author.handle,
+        displayName: item.notification.author.displayName,
+        avatar: item.notification.author.avatar,
+        moderation: moderateProfile(item.notification.author, moderationOpts),
       },
       ...(item.additional?.map(({author}) => {
         return {
@@ -115,33 +108,36 @@ export const FeedItem = observer(function FeedItemImpl({
           handle: author.handle,
           displayName: author.displayName,
           avatar: author.avatar,
-          moderation: moderateProfile(author, store.preferences.moderationOpts),
+          moderation: moderateProfile(author, moderationOpts),
         }
       }) || []),
     ]
-  }, [store, item.additional, item.author])
+  }, [item, moderationOpts])
 
-  if (item.additionalPost?.notFound) {
+  if (item.subjectUri && !item.subject) {
     // don't render anything if the target post was deleted or unfindable
     return <View />
   }
 
-  if (item.isReply || item.isMention || item.isQuote) {
-    if (!item.additionalPost || item.additionalPost?.error) {
-      // hide errors - it doesnt help the user to show them
-      return <View />
+  if (
+    item.type === 'reply' ||
+    item.type === 'mention' ||
+    item.type === 'quote'
+  ) {
+    if (!item.subject) {
+      return null
     }
     return (
       <Link
-        testID={`feedItem-by-${item.author.handle}`}
+        testID={`feedItem-by-${item.notification.author.handle}`}
         href={itemHref}
-        title={itemTitle}
         noFeedback
         accessible={false}>
         <Post
-          view={item.additionalPost}
+          post={item.subject}
+          dataUpdatedAt={dataUpdatedAt}
           style={
-            item.isRead
+            item.notification.isRead
               ? undefined
               : {
                   backgroundColor: pal.colors.unreadNotifBg,
@@ -156,23 +152,25 @@ export const FeedItem = observer(function FeedItemImpl({
   let action = ''
   let icon: Props['icon'] | 'HeartIconSolid'
   let iconStyle: Props['style'] = []
-  if (item.isLike) {
+  if (item.type === 'post-like') {
     action = 'liked your post'
     icon = 'HeartIconSolid'
     iconStyle = [
       s.likeColor as FontAwesomeIconStyle,
       {position: 'relative', top: -4},
     ]
-  } else if (item.isRepost) {
+  } else if (item.type === 'repost') {
     action = 'reposted your post'
     icon = 'retweet'
     iconStyle = [s.green3 as FontAwesomeIconStyle]
-  } else if (item.isFollow) {
+  } else if (item.type === 'follow') {
     action = 'followed you'
     icon = 'user-plus'
     iconStyle = [s.blue3 as FontAwesomeIconStyle]
-  } else if (item.isCustomFeedLike) {
-    action = `liked your custom feed '${new AtUri(item.subjectUri).rkey}'`
+  } else if (item.type === 'feedgen-like') {
+    action = `liked your custom feed${
+      item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}}'` : ''
+    }`
     icon = 'HeartIconSolid'
     iconStyle = [
       s.likeColor as FontAwesomeIconStyle,
@@ -184,12 +182,12 @@ export const FeedItem = observer(function FeedItemImpl({
 
   return (
     <Link
-      testID={`feedItem-by-${item.author.handle}`}
+      testID={`feedItem-by-${item.notification.author.handle}`}
       style={[
         styles.outer,
         pal.view,
         pal.border,
-        item.isRead
+        item.notification.isRead
           ? undefined
           : {
               backgroundColor: pal.colors.unreadNotifBg,
@@ -197,9 +195,11 @@ export const FeedItem = observer(function FeedItemImpl({
             },
       ]}
       href={itemHref}
-      title={itemTitle}
       noFeedback
-      accessible={(item.isLike && authors.length === 1) || item.isRepost}>
+      accessible={
+        (item.type === 'post-like' && authors.length === 1) ||
+        item.type === 'repost'
+      }>
       <View style={styles.layoutIcon}>
         {/* TODO: Prevent conditional rendering and move toward composable
         notifications for clearer accessibility labeling */}
@@ -244,24 +244,24 @@ export const FeedItem = observer(function FeedItemImpl({
               </>
             ) : undefined}
             <Text style={[pal.text]}> {action}</Text>
-            <TimeElapsed timestamp={item.indexedAt}>
+            <TimeElapsed timestamp={item.notification.indexedAt}>
               {({timeElapsed}) => (
                 <Text
                   style={[pal.textLight, styles.pointer]}
-                  title={niceDate(item.indexedAt)}>
+                  title={niceDate(item.notification.indexedAt)}>
                   {' ' + timeElapsed}
                 </Text>
               )}
             </TimeElapsed>
           </Text>
         </ExpandListPressable>
-        {item.isLike || item.isRepost || item.isQuote ? (
-          <AdditionalPostText additionalPost={item.additionalPost} />
+        {item.type === 'post-like' || item.type === 'repost' ? (
+          <AdditionalPostText post={item.subject} />
         ) : null}
       </View>
     </Link>
   )
-})
+}
 
 function ExpandListPressable({
   hasMultipleAuthors,
@@ -423,34 +423,25 @@ function ExpandedAuthorsList({
   )
 }
 
-function AdditionalPostText({
-  additionalPost,
-}: {
-  additionalPost?: PostThreadModel
-}) {
+function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
   const pal = usePalette('default')
-  if (
-    !additionalPost ||
-    !additionalPost.thread?.postRecord ||
-    additionalPost.error
-  ) {
-    return <View />
+  if (post && AppBskyFeedPost.isRecord(post?.record)) {
+    const text = post.record.text
+    const images = AppBskyEmbedImages.isView(post.embed)
+      ? post.embed.images
+      : AppBskyEmbedRecordWithMedia.isView(post.embed) &&
+        AppBskyEmbedImages.isView(post.embed.media)
+      ? post.embed.media.images
+      : undefined
+    return (
+      <>
+        {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
+        {images && images?.length > 0 && (
+          <ImageHorzList images={images} style={styles.additionalPostImages} />
+        )}
+      </>
+    )
   }
-  const text = additionalPost.thread?.postRecord.text
-  const images = AppBskyEmbedImages.isView(additionalPost.thread.post.embed)
-    ? additionalPost.thread.post.embed.images
-    : AppBskyEmbedRecordWithMedia.isView(additionalPost.thread.post.embed) &&
-      AppBskyEmbedImages.isView(additionalPost.thread.post.embed.media)
-    ? additionalPost.thread.post.embed.media.images
-    : undefined
-  return (
-    <>
-      {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
-      {images && images?.length > 0 && (
-        <ImageHorzList images={images} style={styles.additionalPostImages} />
-      )}
-    </>
-  )
 }
 
 const styles = StyleSheet.create({
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 0535cab53..88889fd18 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -23,8 +23,8 @@ import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
 import {useStores} from 'state/index'
 import {PostMeta} from '../util/PostMeta'
 import {PostEmbeds} from '../util/post-embeds'
-import {PostCtrls} from '../util/post-ctrls/PostCtrls2'
-import {PostDropdownBtn} from '../util/forms/PostDropdownBtn2'
+import {PostCtrls} from '../util/post-ctrls/PostCtrls'
+import {PostDropdownBtn} from '../util/forms/PostDropdownBtn'
 import {PostHider} from '../util/moderation/PostHider'
 import {ContentHider} from '../util/moderation/ContentHider'
 import {PostAlerts} from '../util/moderation/PostAlerts'
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 667584f68..4a5b8041e 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,19 +1,14 @@
-import React, {useState} from 'react'
+import React, {useState, useMemo} from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {
-  ActivityIndicator,
-  Linking,
-  StyleProp,
-  StyleSheet,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {AppBskyFeedPost as FeedPost} from '@atproto/api'
-import {observer} from 'mobx-react-lite'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {AtUri} from '@atproto/api'
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  AtUri,
+  moderatePost,
+  PostModeration,
+  RichText as RichTextAPI,
+} from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {PostThreadModel} from 'state/models/content/post-thread'
-import {PostThreadItemModel} from 'state/models/content/post-thread-item'
 import {Link, TextLink} from '../util/Link'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
@@ -23,174 +18,111 @@ import {ContentHider} from '../util/moderation/ContentHider'
 import {PostAlerts} from '../util/moderation/PostAlerts'
 import {Text} from '../util/text/Text'
 import {RichText} from '../util/text/RichText'
-import * as Toast from '../util/Toast'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {getTranslatorLink} from '../../../locale/helpers'
 import {makeProfileLink} from 'lib/routes/links'
 import {MAX_POST_LINES} from 'lib/constants'
 import {countLines} from 'lib/strings/helpers'
-import {logger} from '#/logger'
-import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
-import {useLanguagePrefs} from '#/state/preferences'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 
-export const Post = observer(function PostImpl({
-  view,
+export function Post({
+  post,
+  dataUpdatedAt,
   showReplyLine,
-  hideError,
   style,
 }: {
-  view: PostThreadModel
+  post: AppBskyFeedDefs.PostView
+  dataUpdatedAt: number
   showReplyLine?: boolean
-  hideError?: boolean
   style?: StyleProp<ViewStyle>
 }) {
-  const pal = usePalette('default')
-  const [deleted, setDeleted] = useState(false)
-
-  // deleted
-  // =
-  if (deleted) {
-    return <View />
-  }
-
-  // loading
-  // =
-  if (!view.hasContent && view.isLoading) {
-    return (
-      <View style={pal.view}>
-        <ActivityIndicator />
-      </View>
-    )
+  const moderationOpts = useModerationOpts()
+  const record = useMemo<AppBskyFeedPost.Record | undefined>(
+    () =>
+      AppBskyFeedPost.isRecord(post.record) &&
+      AppBskyFeedPost.validateRecord(post.record).success
+        ? post.record
+        : undefined,
+    [post],
+  )
+  const postShadowed = usePostShadow(post, dataUpdatedAt)
+  const richText = useMemo(
+    () =>
+      record
+        ? new RichTextAPI({
+            text: record.text,
+            facets: record.facets,
+          })
+        : undefined,
+    [record],
+  )
+  const moderation = useMemo(
+    () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined),
+    [moderationOpts, post],
+  )
+  if (postShadowed === POST_TOMBSTONE) {
+    return null
   }
-
-  // error
-  // =
-  if (view.hasError || !view.thread || !view.thread?.postRecord) {
-    if (hideError) {
-      return <View />
-    }
+  if (record && richText && moderation) {
     return (
-      <View style={pal.view}>
-        <Text>{view.error || 'Thread not found'}</Text>
-      </View>
+      <PostInner
+        post={postShadowed}
+        record={record}
+        richText={richText}
+        moderation={moderation}
+        showReplyLine={showReplyLine}
+        style={style}
+      />
     )
   }
+  return null
+}
 
-  // loaded
-  // =
-
-  return (
-    <PostLoaded
-      item={view.thread}
-      record={view.thread.postRecord}
-      setDeleted={setDeleted}
-      showReplyLine={showReplyLine}
-      style={style}
-    />
-  )
-})
-
-const PostLoaded = observer(function PostLoadedImpl({
-  item,
+function PostInner({
+  post,
   record,
-  setDeleted,
+  richText,
+  moderation,
   showReplyLine,
   style,
 }: {
-  item: PostThreadItemModel
-  record: FeedPost.Record
-  setDeleted: (v: boolean) => void
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
+  richText: RichTextAPI
+  moderation: PostModeration
   showReplyLine?: boolean
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
   const store = useStores()
-  const mutedThreads = useMutedThreads()
-  const toggleThreadMute = useToggleThreadMute()
-  const langPrefs = useLanguagePrefs()
-  const [limitLines, setLimitLines] = React.useState(
-    countLines(item.richText?.text) >= MAX_POST_LINES,
+  const [limitLines, setLimitLines] = useState(
+    countLines(richText?.text) >= MAX_POST_LINES,
   )
-  const itemUri = item.post.uri
-  const itemCid = item.post.cid
-  const itemUrip = new AtUri(item.post.uri)
-  const itemHref = makeProfileLink(item.post.author, 'post', itemUrip.rkey)
-  const itemTitle = `Post by ${item.post.author.handle}`
+  const itemUrip = new AtUri(post.uri)
+  const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
   let replyAuthorDid = ''
   if (record.reply) {
     const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
     replyAuthorDid = urip.hostname
   }
 
-  const translatorUrl = getTranslatorLink(
-    record?.text || '',
-    langPrefs.primaryLanguage,
-  )
-
   const onPressReply = React.useCallback(() => {
     store.shell.openComposer({
       replyTo: {
-        uri: item.post.uri,
-        cid: item.post.cid,
-        text: record.text as string,
+        uri: post.uri,
+        cid: post.cid,
+        text: record.text,
         author: {
-          handle: item.post.author.handle,
-          displayName: item.post.author.displayName,
-          avatar: item.post.author.avatar,
+          handle: post.author.handle,
+          displayName: post.author.displayName,
+          avatar: post.author.avatar,
         },
       },
     })
-  }, [store, item, record])
-
-  const onPressToggleRepost = React.useCallback(() => {
-    return item
-      .toggleRepost()
-      .catch(e => logger.error('Failed to toggle repost', {error: e}))
-  }, [item])
-
-  const onPressToggleLike = React.useCallback(() => {
-    return item
-      .toggleLike()
-      .catch(e => logger.error('Failed to toggle like', {error: e}))
-  }, [item])
-
-  const onCopyPostText = React.useCallback(() => {
-    Clipboard.setString(record.text)
-    Toast.show('Copied to clipboard')
-  }, [record])
-
-  const onOpenTranslate = React.useCallback(() => {
-    Linking.openURL(translatorUrl)
-  }, [translatorUrl])
-
-  const onToggleThreadMute = React.useCallback(() => {
-    try {
-      const muted = toggleThreadMute(item.data.rootUri)
-      if (muted) {
-        Toast.show('You will no longer receive notifications for this thread')
-      } else {
-        Toast.show('You will now receive notifications for this thread')
-      }
-    } catch (e) {
-      logger.error('Failed to toggle thread mute', {error: e})
-    }
-  }, [item, toggleThreadMute])
-
-  const onDeletePost = React.useCallback(() => {
-    item.delete().then(
-      () => {
-        setDeleted(true)
-        Toast.show('Post deleted')
-      },
-      e => {
-        logger.error('Failed to delete post', {error: e})
-        Toast.show('Failed to delete post, please try again')
-      },
-    )
-  }, [item, setDeleted])
+  }, [store, post, record])
 
   const onPressShowMore = React.useCallback(() => {
     setLimitLines(false)
@@ -203,17 +135,17 @@ const PostLoaded = observer(function PostLoadedImpl({
         <View style={styles.layoutAvi}>
           <PreviewableUserAvatar
             size={52}
-            did={item.post.author.did}
-            handle={item.post.author.handle}
-            avatar={item.post.author.avatar}
-            moderation={item.moderation.avatar}
+            did={post.author.did}
+            handle={post.author.handle}
+            avatar={post.author.avatar}
+            moderation={moderation.avatar}
           />
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
-            author={item.post.author}
-            authorHasWarning={!!item.post.author.labels?.length}
-            timestamp={item.post.indexedAt}
+            author={post.author}
+            authorHasWarning={!!post.author.labels?.length}
+            timestamp={post.indexedAt}
             postHref={itemHref}
           />
           {replyAuthorDid !== '' && (
@@ -239,19 +171,16 @@ const PostLoaded = observer(function PostLoadedImpl({
             </View>
           )}
           <ContentHider
-            moderation={item.moderation.content}
+            moderation={moderation.content}
             style={styles.contentHider}
             childContainerStyle={styles.contentHiderChild}>
-            <PostAlerts
-              moderation={item.moderation.content}
-              style={styles.alert}
-            />
-            {item.richText?.text ? (
+            <PostAlerts moderation={moderation.content} style={styles.alert} />
+            {richText.text ? (
               <View style={styles.postTextContainer}>
                 <RichText
                   testID="postText"
                   type="post-text"
-                  richText={item.richText}
+                  richText={richText}
                   lineHeight={1.3}
                   numberOfLines={limitLines ? MAX_POST_LINES : undefined}
                   style={s.flex1}
@@ -266,45 +195,20 @@ const PostLoaded = observer(function PostLoadedImpl({
                 href="#"
               />
             ) : undefined}
-            {item.post.embed ? (
+            {post.embed ? (
               <ContentHider
-                moderation={item.moderation.embed}
+                moderation={moderation.embed}
                 style={styles.contentHider}>
-                <PostEmbeds
-                  embed={item.post.embed}
-                  moderation={item.moderation.embed}
-                />
+                <PostEmbeds embed={post.embed} moderation={moderation.embed} />
               </ContentHider>
             ) : null}
           </ContentHider>
-          <PostCtrls
-            itemUri={itemUri}
-            itemCid={itemCid}
-            itemHref={itemHref}
-            itemTitle={itemTitle}
-            author={item.post.author}
-            indexedAt={item.post.indexedAt}
-            text={item.richText?.text || record.text}
-            isAuthor={item.post.author.did === store.me.did}
-            replyCount={item.post.replyCount}
-            repostCount={item.post.repostCount}
-            likeCount={item.post.likeCount}
-            isReposted={!!item.post.viewer?.repost}
-            isLiked={!!item.post.viewer?.like}
-            isThreadMuted={mutedThreads.includes(item.data.rootUri)}
-            onPressReply={onPressReply}
-            onPressToggleRepost={onPressToggleRepost}
-            onPressToggleLike={onPressToggleLike}
-            onCopyPostText={onCopyPostText}
-            onOpenTranslate={onOpenTranslate}
-            onToggleThreadMute={onToggleThreadMute}
-            onDeletePost={onDeletePost}
-          />
+          <PostCtrls post={post} record={record} onPressReply={onPressReply} />
         </View>
       </View>
     </Link>
   )
-})
+}
 
 const styles = StyleSheet.create({
   outer: {
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 50afc1950..6cbad7f71 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -68,7 +68,7 @@ export function Feed({
   const pal = usePalette('default')
   const theme = useTheme()
   const {track} = useAnalytics()
-  const [isRefreshing, setIsRefreshing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
 
   const opts = React.useMemo(() => ({enabled}), [enabled])
@@ -137,15 +137,15 @@ export function Feed({
 
   const onRefresh = React.useCallback(async () => {
     track('Feed:onRefresh')
-    setIsRefreshing(true)
+    setIsPTRing(true)
     try {
       await refetch()
       onHasNew?.(false)
     } catch (err) {
       logger.error('Failed to refresh posts feed', {error: err})
     }
-    setIsRefreshing(false)
-  }, [refetch, track, setIsRefreshing, onHasNew])
+    setIsPTRing(false)
+  }, [refetch, track, setIsPTRing, onHasNew])
 
   const onEndReached = React.useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
@@ -233,7 +233,7 @@ export function Feed({
         ListHeaderComponent={ListHeaderComponent}
         refreshControl={
           <RefreshControl
-            refreshing={isRefreshing}
+            refreshing={isPTRing}
             onRefresh={onRefresh}
             tintColor={pal.colors.text}
             titleColor={pal.colors.text}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index c5a841e31..d24a18f0e 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -16,7 +16,7 @@ import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
-import {PostCtrls} from '../util/post-ctrls/PostCtrls2'
+import {PostCtrls} from '../util/post-ctrls/PostCtrls'
 import {PostEmbeds} from '../util/post-embeds'
 import {ContentHider} from '../util/moderation/ContentHider'
 import {PostAlerts} from '../util/moderation/PostAlerts'
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index a10841450..c457e0a46 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -1,6 +1,8 @@
 import React from 'react'
-import {StyleProp, View, ViewStyle} from 'react-native'
+import {Linking, StyleProp, View, ViewStyle} from 'react-native'
+import Clipboard from '@react-native-clipboard/clipboard'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {useTheme} from 'lib/ThemeContext'
 import {shareUrl} from 'lib/sharing'
@@ -8,41 +10,83 @@ import {
   NativeDropdown,
   DropdownItem as NativeDropdownItem,
 } from './NativeDropdown'
+import * as Toast from '../Toast'
 import {EventStopper} from '../EventStopper'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
+import {makeProfileLink} from '#/lib/routes/links'
+import {getTranslatorLink} from '#/locale/helpers'
+import {useStores} from '#/state'
+import {usePostDeleteMutation} from '#/state/queries/post'
+import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
+import {useLanguagePrefs} from '#/state/preferences'
+import {logger} from '#/logger'
 
 export function PostDropdownBtn({
   testID,
-  itemUri,
-  itemCid,
-  itemHref,
-  isAuthor,
-  isThreadMuted,
-  onCopyPostText,
-  onOpenTranslate,
-  onToggleThreadMute,
-  onDeletePost,
+  post,
+  record,
   style,
 }: {
   testID: string
-  itemUri: string
-  itemCid: string
-  itemHref: string
-  itemTitle: string
-  isAuthor: boolean
-  isThreadMuted: boolean
-  onCopyPostText: () => void
-  onOpenTranslate: () => void
-  onToggleThreadMute: () => void
-  onDeletePost: () => void
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
   style?: StyleProp<ViewStyle>
 }) {
+  const store = useStores()
   const theme = useTheme()
-  const {_} = useLingui()
   const defaultCtrlColor = theme.palette.default.postCtrl
   const {openModal} = useModalControls()
+  const langPrefs = useLanguagePrefs()
+  const mutedThreads = useMutedThreads()
+  const toggleThreadMute = useToggleThreadMute()
+  const postDeleteMutation = usePostDeleteMutation()
+
+  const rootUri = record.reply?.root?.uri || post.uri
+  const isThreadMuted = mutedThreads.includes(rootUri)
+  const isAuthor = post.author.did === store.me.did
+  const href = React.useMemo(() => {
+    const urip = new AtUri(post.uri)
+    return makeProfileLink(post.author, 'post', urip.rkey)
+  }, [post.uri, post.author])
+
+  const translatorUrl = getTranslatorLink(
+    record.text,
+    langPrefs.primaryLanguage,
+  )
+
+  const onDeletePost = React.useCallback(() => {
+    postDeleteMutation.mutateAsync({uri: post.uri}).then(
+      () => {
+        Toast.show('Post deleted')
+      },
+      e => {
+        logger.error('Failed to delete post', {error: e})
+        Toast.show('Failed to delete post, please try again')
+      },
+    )
+  }, [post, postDeleteMutation])
+
+  const onToggleThreadMute = React.useCallback(() => {
+    try {
+      const muted = toggleThreadMute(rootUri)
+      if (muted) {
+        Toast.show('You will no longer receive notifications for this thread')
+      } else {
+        Toast.show('You will now receive notifications for this thread')
+      }
+    } catch (e) {
+      logger.error('Failed to toggle thread mute', {error: e})
+    }
+  }, [rootUri, toggleThreadMute])
+
+  const onCopyPostText = React.useCallback(() => {
+    Clipboard.setString(record?.text || '')
+    Toast.show('Copied to clipboard')
+  }, [record])
+
+  const onOpenTranslate = React.useCallback(() => {
+    Linking.openURL(translatorUrl)
+  }, [translatorUrl])
 
   const dropdownItems: NativeDropdownItem[] = [
     {
@@ -76,7 +120,7 @@ export function PostDropdownBtn({
     {
       label: 'Share',
       onPress() {
-        const url = toShareUrl(itemHref)
+        const url = toShareUrl(href)
         shareUrl(url)
       },
       testID: 'postDropdownShareBtn',
@@ -113,8 +157,8 @@ export function PostDropdownBtn({
       onPress() {
         openModal({
           name: 'report',
-          uri: itemUri,
-          cid: itemCid,
+          uri: post.uri,
+          cid: post.cid,
         })
       },
       testID: 'postDropdownReportBtn',
@@ -155,7 +199,7 @@ export function PostDropdownBtn({
       <NativeDropdown
         testID={testID}
         items={dropdownItems}
-        accessibilityLabel={_(msg`More post options`)}
+        accessibilityLabel="More post options"
         accessibilityHint="">
         <View style={style}>
           <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
diff --git a/src/view/com/util/forms/PostDropdownBtn2.tsx b/src/view/com/util/forms/PostDropdownBtn2.tsx
deleted file mode 100644
index c457e0a46..000000000
--- a/src/view/com/util/forms/PostDropdownBtn2.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import React from 'react'
-import {Linking, StyleProp, View, ViewStyle} from 'react-native'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {useTheme} from 'lib/ThemeContext'
-import {shareUrl} from 'lib/sharing'
-import {
-  NativeDropdown,
-  DropdownItem as NativeDropdownItem,
-} from './NativeDropdown'
-import * as Toast from '../Toast'
-import {EventStopper} from '../EventStopper'
-import {useModalControls} from '#/state/modals'
-import {makeProfileLink} from '#/lib/routes/links'
-import {getTranslatorLink} from '#/locale/helpers'
-import {useStores} from '#/state'
-import {usePostDeleteMutation} from '#/state/queries/post'
-import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
-import {useLanguagePrefs} from '#/state/preferences'
-import {logger} from '#/logger'
-
-export function PostDropdownBtn({
-  testID,
-  post,
-  record,
-  style,
-}: {
-  testID: string
-  post: AppBskyFeedDefs.PostView
-  record: AppBskyFeedPost.Record
-  style?: StyleProp<ViewStyle>
-}) {
-  const store = useStores()
-  const theme = useTheme()
-  const defaultCtrlColor = theme.palette.default.postCtrl
-  const {openModal} = useModalControls()
-  const langPrefs = useLanguagePrefs()
-  const mutedThreads = useMutedThreads()
-  const toggleThreadMute = useToggleThreadMute()
-  const postDeleteMutation = usePostDeleteMutation()
-
-  const rootUri = record.reply?.root?.uri || post.uri
-  const isThreadMuted = mutedThreads.includes(rootUri)
-  const isAuthor = post.author.did === store.me.did
-  const href = React.useMemo(() => {
-    const urip = new AtUri(post.uri)
-    return makeProfileLink(post.author, 'post', urip.rkey)
-  }, [post.uri, post.author])
-
-  const translatorUrl = getTranslatorLink(
-    record.text,
-    langPrefs.primaryLanguage,
-  )
-
-  const onDeletePost = React.useCallback(() => {
-    postDeleteMutation.mutateAsync({uri: post.uri}).then(
-      () => {
-        Toast.show('Post deleted')
-      },
-      e => {
-        logger.error('Failed to delete post', {error: e})
-        Toast.show('Failed to delete post, please try again')
-      },
-    )
-  }, [post, postDeleteMutation])
-
-  const onToggleThreadMute = React.useCallback(() => {
-    try {
-      const muted = toggleThreadMute(rootUri)
-      if (muted) {
-        Toast.show('You will no longer receive notifications for this thread')
-      } else {
-        Toast.show('You will now receive notifications for this thread')
-      }
-    } catch (e) {
-      logger.error('Failed to toggle thread mute', {error: e})
-    }
-  }, [rootUri, toggleThreadMute])
-
-  const onCopyPostText = React.useCallback(() => {
-    Clipboard.setString(record?.text || '')
-    Toast.show('Copied to clipboard')
-  }, [record])
-
-  const onOpenTranslate = React.useCallback(() => {
-    Linking.openURL(translatorUrl)
-  }, [translatorUrl])
-
-  const dropdownItems: NativeDropdownItem[] = [
-    {
-      label: 'Translate',
-      onPress() {
-        onOpenTranslate()
-      },
-      testID: 'postDropdownTranslateBtn',
-      icon: {
-        ios: {
-          name: 'character.book.closed',
-        },
-        android: 'ic_menu_sort_alphabetically',
-        web: 'language',
-      },
-    },
-    {
-      label: 'Copy post text',
-      onPress() {
-        onCopyPostText()
-      },
-      testID: 'postDropdownCopyTextBtn',
-      icon: {
-        ios: {
-          name: 'doc.on.doc',
-        },
-        android: 'ic_menu_edit',
-        web: ['far', 'paste'],
-      },
-    },
-    {
-      label: 'Share',
-      onPress() {
-        const url = toShareUrl(href)
-        shareUrl(url)
-      },
-      testID: 'postDropdownShareBtn',
-      icon: {
-        ios: {
-          name: 'square.and.arrow.up',
-        },
-        android: 'ic_menu_share',
-        web: 'share',
-      },
-    },
-    {
-      label: 'separator',
-    },
-    {
-      label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
-      onPress() {
-        onToggleThreadMute()
-      },
-      testID: 'postDropdownMuteThreadBtn',
-      icon: {
-        ios: {
-          name: 'speaker.slash',
-        },
-        android: 'ic_lock_silent_mode',
-        web: 'comment-slash',
-      },
-    },
-    {
-      label: 'separator',
-    },
-    !isAuthor && {
-      label: 'Report post',
-      onPress() {
-        openModal({
-          name: 'report',
-          uri: post.uri,
-          cid: post.cid,
-        })
-      },
-      testID: 'postDropdownReportBtn',
-      icon: {
-        ios: {
-          name: 'exclamationmark.triangle',
-        },
-        android: 'ic_menu_report_image',
-        web: 'circle-exclamation',
-      },
-    },
-    isAuthor && {
-      label: 'separator',
-    },
-    isAuthor && {
-      label: 'Delete post',
-      onPress() {
-        openModal({
-          name: 'confirm',
-          title: 'Delete this post?',
-          message: 'Are you sure? This can not be undone.',
-          onPressConfirm: onDeletePost,
-        })
-      },
-      testID: 'postDropdownDeleteBtn',
-      icon: {
-        ios: {
-          name: 'trash',
-        },
-        android: 'ic_menu_delete',
-        web: ['far', 'trash-can'],
-      },
-    },
-  ].filter(Boolean) as NativeDropdownItem[]
-
-  return (
-    <EventStopper>
-      <NativeDropdown
-        testID={testID}
-        items={dropdownItems}
-        accessibilityLabel="More post options"
-        accessibilityHint="">
-        <View style={style}>
-          <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
-        </View>
-      </NativeDropdown>
-    </EventStopper>
-  )
-}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 7bcea0e79..a764ed525 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -6,6 +6,7 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
 import {Text} from '../text/Text'
 import {PostDropdownBtn} from '../forms/PostDropdownBtn'
 import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
@@ -17,160 +18,155 @@ import {RepostButton} from './RepostButton'
 import {Haptics} from 'lib/haptics'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
 import {useModalControls} from '#/state/modals'
+import {
+  usePostLikeMutation,
+  usePostUnlikeMutation,
+  usePostRepostMutation,
+  usePostUnrepostMutation,
+} from '#/state/queries/post'
 
-interface PostCtrlsOpts {
-  itemUri: string
-  itemCid: string
-  itemHref: string
-  itemTitle: string
-  isAuthor: boolean
-  author: {
-    did: string
-    handle: string
-    displayName?: string | undefined
-    avatar?: string | undefined
-  }
-  text: string
-  indexedAt: string
+export function PostCtrls({
+  big,
+  post,
+  record,
+  style,
+  onPressReply,
+}: {
   big?: boolean
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
   style?: StyleProp<ViewStyle>
-  replyCount?: number
-  repostCount?: number
-  likeCount?: number
-  isReposted: boolean
-  isLiked: boolean
-  isThreadMuted: boolean
   onPressReply: () => void
-  onPressToggleRepost: () => Promise<void>
-  onPressToggleLike: () => Promise<void>
-  onCopyPostText: () => void
-  onOpenTranslate: () => void
-  onToggleThreadMute: () => void
-  onDeletePost: () => void
-}
-
-export function PostCtrls(opts: PostCtrlsOpts) {
+}) {
   const store = useStores()
   const theme = useTheme()
   const {closeModal} = useModalControls()
+  const postLikeMutation = usePostLikeMutation()
+  const postUnlikeMutation = usePostUnlikeMutation()
+  const postRepostMutation = usePostRepostMutation()
+  const postUnrepostMutation = usePostUnrepostMutation()
+
   const defaultCtrlColor = React.useMemo(
     () => ({
       color: theme.palette.default.postCtrl,
     }),
     [theme],
   ) as StyleProp<ViewStyle>
+
+  const onPressToggleLike = React.useCallback(async () => {
+    if (!post.viewer?.like) {
+      Haptics.default()
+      postLikeMutation.mutate({
+        uri: post.uri,
+        cid: post.cid,
+        likeCount: post.likeCount || 0,
+      })
+    } else {
+      postUnlikeMutation.mutate({
+        postUri: post.uri,
+        likeUri: post.viewer.like,
+        likeCount: post.likeCount || 0,
+      })
+    }
+  }, [post, postLikeMutation, postUnlikeMutation])
+
   const onRepost = useCallback(() => {
     closeModal()
-    if (!opts.isReposted) {
+    if (!post.viewer?.repost) {
       Haptics.default()
-      opts.onPressToggleRepost().catch(_e => undefined)
+      postRepostMutation.mutate({
+        uri: post.uri,
+        cid: post.cid,
+        repostCount: post.repostCount || 0,
+      })
     } else {
-      opts.onPressToggleRepost().catch(_e => undefined)
+      postUnrepostMutation.mutate({
+        postUri: post.uri,
+        repostUri: post.viewer.repost,
+        repostCount: post.repostCount || 0,
+      })
     }
-  }, [opts, closeModal])
+  }, [post, closeModal, postRepostMutation, postUnrepostMutation])
 
   const onQuote = useCallback(() => {
     closeModal()
     store.shell.openComposer({
       quote: {
-        uri: opts.itemUri,
-        cid: opts.itemCid,
-        text: opts.text,
-        author: opts.author,
-        indexedAt: opts.indexedAt,
+        uri: post.uri,
+        cid: post.cid,
+        text: record.text,
+        author: post.author,
+        indexedAt: post.indexedAt,
       },
     })
     Haptics.default()
-  }, [
-    opts.author,
-    opts.indexedAt,
-    opts.itemCid,
-    opts.itemUri,
-    opts.text,
-    store.shell,
-    closeModal,
-  ])
-
-  const onPressToggleLikeWrapper = async () => {
-    if (!opts.isLiked) {
-      Haptics.default()
-      await opts.onPressToggleLike().catch(_e => undefined)
-    } else {
-      await opts.onPressToggleLike().catch(_e => undefined)
-    }
-  }
-
+  }, [post, record, store.shell, closeModal])
   return (
-    <View style={[styles.ctrls, opts.style]}>
+    <View style={[styles.ctrls, style]}>
       <TouchableOpacity
         testID="replyBtn"
-        style={[styles.ctrl, !opts.big && styles.ctrlPad, {paddingLeft: 0}]}
-        onPress={opts.onPressReply}
+        style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
+        onPress={onPressReply}
         accessibilityRole="button"
-        accessibilityLabel={`Reply (${opts.replyCount} ${
-          opts.replyCount === 1 ? 'reply' : 'replies'
+        accessibilityLabel={`Reply (${post.replyCount} ${
+          post.replyCount === 1 ? 'reply' : 'replies'
         })`}
         accessibilityHint=""
-        hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
+        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
         <CommentBottomArrow
-          style={[defaultCtrlColor, opts.big ? s.mt2 : styles.mt1]}
+          style={[defaultCtrlColor, big ? s.mt2 : styles.mt1]}
           strokeWidth={3}
-          size={opts.big ? 20 : 15}
+          size={big ? 20 : 15}
         />
-        {typeof opts.replyCount !== 'undefined' ? (
+        {typeof post.replyCount !== 'undefined' ? (
           <Text style={[defaultCtrlColor, s.ml5, s.f15]}>
-            {opts.replyCount}
+            {post.replyCount}
           </Text>
         ) : undefined}
       </TouchableOpacity>
-      <RepostButton {...opts} onRepost={onRepost} onQuote={onQuote} />
+      <RepostButton
+        big={big}
+        isReposted={!!post.viewer?.repost}
+        repostCount={post.repostCount}
+        onRepost={onRepost}
+        onQuote={onQuote}
+      />
       <TouchableOpacity
         testID="likeBtn"
-        style={[styles.ctrl, !opts.big && styles.ctrlPad]}
-        onPress={onPressToggleLikeWrapper}
+        style={[styles.ctrl, !big && styles.ctrlPad]}
+        onPress={onPressToggleLike}
         accessibilityRole="button"
-        accessibilityLabel={`${opts.isLiked ? 'Unlike' : 'Like'} (${
-          opts.likeCount
-        } ${pluralize(opts.likeCount || 0, 'like')})`}
+        accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
+          post.likeCount
+        } ${pluralize(post.likeCount || 0, 'like')})`}
         accessibilityHint=""
-        hitSlop={opts.big ? HITSLOP_20 : HITSLOP_10}>
-        {opts.isLiked ? (
-          <HeartIconSolid
-            style={styles.ctrlIconLiked}
-            size={opts.big ? 22 : 16}
-          />
+        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
+        {post.viewer?.like ? (
+          <HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
         ) : (
           <HeartIcon
-            style={[defaultCtrlColor, opts.big ? styles.mt1 : undefined]}
+            style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
             strokeWidth={3}
-            size={opts.big ? 20 : 16}
+            size={big ? 20 : 16}
           />
         )}
-        {typeof opts.likeCount !== 'undefined' ? (
+        {typeof post.likeCount !== 'undefined' ? (
           <Text
             testID="likeCount"
             style={
-              opts.isLiked
+              post.viewer?.like
                 ? [s.bold, s.red3, s.f15, s.ml5]
                 : [defaultCtrlColor, s.f15, s.ml5]
             }>
-            {opts.likeCount}
+            {post.likeCount}
           </Text>
         ) : undefined}
       </TouchableOpacity>
-      {opts.big ? undefined : (
+      {big ? undefined : (
         <PostDropdownBtn
           testID="postDropdownBtn"
-          itemUri={opts.itemUri}
-          itemCid={opts.itemCid}
-          itemHref={opts.itemHref}
-          itemTitle={opts.itemTitle}
-          isAuthor={opts.isAuthor}
-          isThreadMuted={opts.isThreadMuted}
-          onCopyPostText={opts.onCopyPostText}
-          onOpenTranslate={opts.onOpenTranslate}
-          onToggleThreadMute={opts.onToggleThreadMute}
-          onDeletePost={opts.onDeletePost}
+          post={post}
+          record={record}
           style={styles.ctrlPad}
         />
       )}
diff --git a/src/view/com/util/post-ctrls/PostCtrls2.tsx b/src/view/com/util/post-ctrls/PostCtrls2.tsx
deleted file mode 100644
index 7c8ebaee7..000000000
--- a/src/view/com/util/post-ctrls/PostCtrls2.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import React, {useCallback} from 'react'
-import {
-  StyleProp,
-  StyleSheet,
-  TouchableOpacity,
-  View,
-  ViewStyle,
-} from 'react-native'
-import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
-import {Text} from '../text/Text'
-import {PostDropdownBtn} from '../forms/PostDropdownBtn2'
-import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
-import {s, colors} from 'lib/styles'
-import {pluralize} from 'lib/strings/helpers'
-import {useTheme} from 'lib/ThemeContext'
-import {useStores} from 'state/index'
-import {RepostButton} from './RepostButton'
-import {Haptics} from 'lib/haptics'
-import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
-import {useModalControls} from '#/state/modals'
-import {
-  usePostLikeMutation,
-  usePostUnlikeMutation,
-  usePostRepostMutation,
-  usePostUnrepostMutation,
-} from '#/state/queries/post'
-
-export function PostCtrls({
-  big,
-  post,
-  record,
-  style,
-  onPressReply,
-}: {
-  big?: boolean
-  post: AppBskyFeedDefs.PostView
-  record: AppBskyFeedPost.Record
-  style?: StyleProp<ViewStyle>
-  onPressReply: () => void
-}) {
-  const store = useStores()
-  const theme = useTheme()
-  const {closeModal} = useModalControls()
-  const postLikeMutation = usePostLikeMutation()
-  const postUnlikeMutation = usePostUnlikeMutation()
-  const postRepostMutation = usePostRepostMutation()
-  const postUnrepostMutation = usePostUnrepostMutation()
-
-  const defaultCtrlColor = React.useMemo(
-    () => ({
-      color: theme.palette.default.postCtrl,
-    }),
-    [theme],
-  ) as StyleProp<ViewStyle>
-
-  const onPressToggleLike = React.useCallback(async () => {
-    if (!post.viewer?.like) {
-      Haptics.default()
-      postLikeMutation.mutate({
-        uri: post.uri,
-        cid: post.cid,
-        likeCount: post.likeCount || 0,
-      })
-    } else {
-      postUnlikeMutation.mutate({
-        postUri: post.uri,
-        likeUri: post.viewer.like,
-        likeCount: post.likeCount || 0,
-      })
-    }
-  }, [post, postLikeMutation, postUnlikeMutation])
-
-  const onRepost = useCallback(() => {
-    closeModal()
-    if (!post.viewer?.repost) {
-      Haptics.default()
-      postRepostMutation.mutate({
-        uri: post.uri,
-        cid: post.cid,
-        repostCount: post.repostCount || 0,
-      })
-    } else {
-      postUnrepostMutation.mutate({
-        postUri: post.uri,
-        repostUri: post.viewer.repost,
-        repostCount: post.repostCount || 0,
-      })
-    }
-  }, [post, closeModal, postRepostMutation, postUnrepostMutation])
-
-  const onQuote = useCallback(() => {
-    closeModal()
-    store.shell.openComposer({
-      quote: {
-        uri: post.uri,
-        cid: post.cid,
-        text: record.text,
-        author: post.author,
-        indexedAt: post.indexedAt,
-      },
-    })
-    Haptics.default()
-  }, [post, record, store.shell, closeModal])
-  return (
-    <View style={[styles.ctrls, style]}>
-      <TouchableOpacity
-        testID="replyBtn"
-        style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
-        onPress={onPressReply}
-        accessibilityRole="button"
-        accessibilityLabel={`Reply (${post.replyCount} ${
-          post.replyCount === 1 ? 'reply' : 'replies'
-        })`}
-        accessibilityHint=""
-        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
-        <CommentBottomArrow
-          style={[defaultCtrlColor, big ? s.mt2 : styles.mt1]}
-          strokeWidth={3}
-          size={big ? 20 : 15}
-        />
-        {typeof post.replyCount !== 'undefined' ? (
-          <Text style={[defaultCtrlColor, s.ml5, s.f15]}>
-            {post.replyCount}
-          </Text>
-        ) : undefined}
-      </TouchableOpacity>
-      <RepostButton
-        big={big}
-        isReposted={!!post.viewer?.repost}
-        repostCount={post.repostCount}
-        onRepost={onRepost}
-        onQuote={onQuote}
-      />
-      <TouchableOpacity
-        testID="likeBtn"
-        style={[styles.ctrl, !big && styles.ctrlPad]}
-        onPress={onPressToggleLike}
-        accessibilityRole="button"
-        accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
-          post.likeCount
-        } ${pluralize(post.likeCount || 0, 'like')})`}
-        accessibilityHint=""
-        hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
-        {post.viewer?.like ? (
-          <HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
-        ) : (
-          <HeartIcon
-            style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
-            strokeWidth={3}
-            size={big ? 20 : 16}
-          />
-        )}
-        {typeof post.likeCount !== 'undefined' ? (
-          <Text
-            testID="likeCount"
-            style={
-              post.viewer?.like
-                ? [s.bold, s.red3, s.f15, s.ml5]
-                : [defaultCtrlColor, s.f15, s.ml5]
-            }>
-            {post.likeCount}
-          </Text>
-        ) : undefined}
-      </TouchableOpacity>
-      {big ? undefined : (
-        <PostDropdownBtn
-          testID="postDropdownBtn"
-          post={post}
-          record={record}
-          style={styles.ctrlPad}
-        />
-      )}
-      {/* used for adding pad to the right side */}
-      <View />
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  ctrls: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-  },
-  ctrl: {
-    flexDirection: 'row',
-    alignItems: 'center',
-  },
-  ctrlPad: {
-    paddingTop: 5,
-    paddingBottom: 5,
-    paddingLeft: 5,
-    paddingRight: 5,
-  },
-  ctrlIconLiked: {
-    color: colors.like,
-  },
-  mt1: {
-    marginTop: 1,
-  },
-})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index b03e73376..e0f234073 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {FlatList, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
-import {observer} from 'mobx-react-lite'
+import {useQueryClient} from '@tanstack/react-query'
 import {
   NativeStackScreenProps,
   NotificationsTabNavigatorParams,
@@ -13,21 +13,21 @@ import {TextLink} from 'view/com/util/Link'
 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 {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'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
 
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
 export const NotificationsScreen = withAuthRequired(
-  observer(function NotificationsScreenImpl({}: Props) {
+  function NotificationsScreenImpl({}: Props) {
     const store = useStores()
     const setMinimalShellMode = useSetMinimalShellMode()
     const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
@@ -35,17 +35,12 @@ export const NotificationsScreen = withAuthRequired(
     const {screen} = useAnalytics()
     const pal = usePalette('default')
     const {isDesktop} = useWebMediaQueries()
-
-    const hasNew =
-      store.me.notifications.hasNewLatest &&
-      !store.me.notifications.isRefreshing
+    const unreadNotifs = useUnreadNotifications()
+    const queryClient = useQueryClient()
+    const hasNew = !!unreadNotifs
 
     // event handlers
     // =
-    const onPressTryAgain = React.useCallback(() => {
-      store.me.notifications.refresh()
-    }, [store])
-
     const scrollToTop = React.useCallback(() => {
       scrollElRef.current?.scrollToOffset({offset: 0})
       resetMainScroll()
@@ -53,8 +48,8 @@ export const NotificationsScreen = withAuthRequired(
 
     const onPressLoadLatest = React.useCallback(() => {
       scrollToTop()
-      store.me.notifications.refresh()
-    }, [store, scrollToTop])
+      queryClient.invalidateQueries({queryKey: NOTIFS_RQKEY()})
+    }, [scrollToTop, queryClient])
 
     // on-visible setup
     // =
@@ -63,42 +58,14 @@ export const NotificationsScreen = withAuthRequired(
         setMinimalShellMode(false)
         logger.debug('NotificationsScreen: Updating feed')
         const softResetSub = store.onScreenSoftReset(onPressLoadLatest)
-        store.me.notifications.update()
         screen('Notifications')
 
         return () => {
           softResetSub.remove()
-          store.me.notifications.markAllRead()
         }
       }, [store, screen, onPressLoadLatest, setMinimalShellMode]),
     )
 
-    useTabFocusEffect(
-      'Notifications',
-      React.useCallback(
-        isInside => {
-          // on mobile:
-          // fires with `isInside=true` when the user navigates to the root tab
-          // but not when the user goes back to the screen by pressing back
-          // on web:
-          // essentially equivalent to useFocusEffect because we dont used tabbed
-          // navigation
-          if (isInside) {
-            if (isWeb) {
-              store.me.notifications.syncQueue()
-            } else {
-              if (store.me.notifications.unreadCount > 0) {
-                store.me.notifications.refresh()
-              } else {
-                store.me.notifications.syncQueue()
-              }
-            }
-          }
-        },
-        [store],
-      ),
-    )
-
     const ListHeaderComponent = React.useCallback(() => {
       if (isDesktop) {
         return (
@@ -145,8 +112,6 @@ export const NotificationsScreen = withAuthRequired(
       <View testID="notificationsScreen" style={s.hContentRegion}>
         <ViewHeader title="Notifications" canGoBack={false} />
         <Feed
-          view={store.me.notifications}
-          onPressTryAgain={onPressTryAgain}
           onScroll={onMainScroll}
           scrollElRef={scrollElRef}
           ListHeaderComponent={ListHeaderComponent}
@@ -160,5 +125,5 @@ export const NotificationsScreen = withAuthRequired(
         )}
       </View>
     )
-  }),
+  },
 )
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 609348e4d..8a84a07c6 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -49,6 +49,7 @@ import {useSetDrawerOpen} from '#/state/shell'
 import {useModalControls} from '#/state/modals'
 import {useSession, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
 
 export function DrawerProfileCard({
   account,
@@ -110,8 +111,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
   const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
     useNavigationTabState()
   const {currentAccount} = useSession()
-
-  const {notifications} = store.me
+  const numUnreadNotifications = useUnreadNotifications()
 
   // events
   // =
@@ -286,11 +286,11 @@ export const DrawerContent = observer(function DrawerContentImpl() {
             label="Notifications"
             accessibilityLabel={_(msg`Notifications`)}
             accessibilityHint={
-              notifications.unreadCountLabel === ''
+              numUnreadNotifications === ''
                 ? ''
-                : `${notifications.unreadCountLabel} unread`
+                : `${numUnreadNotifications} unread`
             }
-            count={notifications.unreadCountLabel}
+            count={numUnreadNotifications}
             bold={isAtNotifications}
             onPress={onPressNotifications}
           />
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 3dd7f57c5..81552635f 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -28,6 +28,7 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
@@ -43,9 +44,8 @@ export const BottomBar = observer(function BottomBarImpl({
   const {footerHeight} = useShellLayout()
   const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
     useNavigationTabState()
-
+  const numUnreadNotifications = useUnreadNotifications()
   const {footerMinimalShellTransform} = useMinimalShellMode()
-  const {notifications} = store.me
 
   const onPressTab = React.useCallback(
     (tab: TabOptions) => {
@@ -178,14 +178,14 @@ export const BottomBar = observer(function BottomBarImpl({
           )
         }
         onPress={onPressNotifications}
-        notificationCount={notifications.unreadCountLabel}
+        notificationCount={numUnreadNotifications}
         accessible={true}
         accessibilityRole="tab"
         accessibilityLabel={_(msg`Notifications`)}
         accessibilityHint={
-          notifications.unreadCountLabel === ''
+          numUnreadNotifications === ''
             ? ''
-            : `${notifications.unreadCountLabel} unread`
+            : `${numUnreadNotifications} unread`
         }
       />
       <Btn
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 0586323b4..c9a03ce62 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -43,6 +43,7 @@ import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
 
 const ProfileCard = observer(function ProfileCardImpl() {
   const {currentAccount} = useSession()
@@ -253,6 +254,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
   const store = useStores()
   const pal = usePalette('default')
   const {isDesktop, isTablet} = useWebMediaQueries()
+  const numUnread = useUnreadNotifications()
 
   return (
     <View
@@ -314,7 +316,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
       />
       <NavItem
         href="/notifications"
-        count={store.me.notifications.unreadCountLabel}
+        count={numUnread}
         icon={
           <BellIcon
             strokeWidth={2}