about summary refs log tree commit diff
path: root/src/state/queries
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/queries')
-rw-r--r--src/state/queries/messages/accept-conversation.ts135
-rw-r--r--src/state/queries/messages/conversation.ts53
-rw-r--r--src/state/queries/messages/get-convo-availability.ts25
-rw-r--r--src/state/queries/messages/get-convo-for-members.ts35
-rw-r--r--src/state/queries/messages/leave-conversation.ts18
-rw-r--r--src/state/queries/messages/list-conversations.tsx421
-rw-r--r--src/state/queries/messages/mute-conversation.ts4
-rw-r--r--src/state/queries/messages/update-all-read.ts105
-rw-r--r--src/state/queries/profile.ts4
9 files changed, 608 insertions, 192 deletions
diff --git a/src/state/queries/messages/accept-conversation.ts b/src/state/queries/messages/accept-conversation.ts
new file mode 100644
index 000000000..82acb33c8
--- /dev/null
+++ b/src/state/queries/messages/accept-conversation.ts
@@ -0,0 +1,135 @@
+import {ChatBskyConvoAcceptConvo, ChatBskyConvoListConvos} from '@atproto/api'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {useAgent} from '#/state/session'
+import {DM_SERVICE_HEADERS} from './const'
+import {
+  RQKEY as CONVO_LIST_KEY,
+  RQKEY_ROOT as CONVO_LIST_ROOT_KEY,
+} from './list-conversations'
+
+export function useAcceptConversation(
+  convoId: string,
+  {
+    onSuccess,
+    onMutate,
+    onError,
+  }: {
+    onMutate?: () => void
+    onSuccess?: (data: ChatBskyConvoAcceptConvo.OutputSchema) => void
+    onError?: (error: Error) => void
+  },
+) {
+  const queryClient = useQueryClient()
+  const agent = useAgent()
+
+  return useMutation({
+    mutationFn: async () => {
+      const {data} = await agent.chat.bsky.convo.acceptConvo(
+        {convoId},
+        {headers: DM_SERVICE_HEADERS},
+      )
+
+      return data
+    },
+    onMutate: () => {
+      let prevAcceptedPages: ChatBskyConvoListConvos.OutputSchema[] = []
+      let prevInboxPages: ChatBskyConvoListConvos.OutputSchema[] = []
+      let convoBeingAccepted:
+        | ChatBskyConvoListConvos.OutputSchema['convos'][number]
+        | undefined
+      queryClient.setQueryData(
+        CONVO_LIST_KEY('request'),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          prevInboxPages = old.pages
+          return {
+            ...old,
+            pages: old.pages.map(page => {
+              const found = page.convos.find(convo => convo.id === convoId)
+              if (found) {
+                convoBeingAccepted = found
+                return {
+                  ...page,
+                  convos: page.convos.filter(convo => convo.id !== convoId),
+                }
+              }
+              return page
+            }),
+          }
+        },
+      )
+      queryClient.setQueryData(
+        CONVO_LIST_KEY('accepted'),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          prevAcceptedPages = old.pages
+          if (convoBeingAccepted) {
+            return {
+              ...old,
+              pages: [
+                {
+                  ...old.pages[0],
+                  convos: [
+                    {
+                      ...convoBeingAccepted,
+                      status: 'accepted',
+                    },
+                    ...old.pages[0].convos,
+                  ],
+                },
+                ...old.pages.slice(1),
+              ],
+            }
+          } else {
+            return old
+          }
+        },
+      )
+      onMutate?.()
+      return {prevAcceptedPages, prevInboxPages}
+    },
+    onSuccess: data => {
+      queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
+      onSuccess?.(data)
+    },
+    onError: (error, _, context) => {
+      logger.error(error)
+      queryClient.setQueryData(
+        CONVO_LIST_KEY('accepted'),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          return {
+            ...old,
+            pages: context?.prevAcceptedPages || old.pages,
+          }
+        },
+      )
+      queryClient.setQueryData(
+        CONVO_LIST_KEY('request'),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          return {
+            ...old,
+            pages: context?.prevInboxPages || old.pages,
+          }
+        },
+      )
+      queryClient.invalidateQueries({queryKey: [CONVO_LIST_ROOT_KEY]})
+      onError?.(error)
+    },
+  })
+}
diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts
index 260524524..de5a90571 100644
--- a/src/state/queries/messages/conversation.ts
+++ b/src/state/queries/messages/conversation.ts
@@ -13,7 +13,7 @@ import {useAgent} from '#/state/session'
 import {
   ConvoListQueryData,
   getConvoFromQueryData,
-  RQKEY as LIST_CONVOS_KEY,
+  RQKEY_ROOT as LIST_CONVOS_KEY,
 } from './list-conversations'
 
 const RQKEY_ROOT = 'convo'
@@ -76,34 +76,37 @@ export function useMarkAsReadMutation() {
     onSuccess(_, {convoId}) {
       if (!convoId) return
 
-      queryClient.setQueryData(LIST_CONVOS_KEY, (old: ConvoListQueryData) => {
-        if (!old) return old
+      queryClient.setQueriesData(
+        {queryKey: [LIST_CONVOS_KEY]},
+        (old?: ConvoListQueryData) => {
+          if (!old) return old
 
-        const existingConvo = getConvoFromQueryData(convoId, old)
+          const existingConvo = getConvoFromQueryData(convoId, old)
 
-        if (existingConvo) {
-          return {
-            ...old,
-            pages: old.pages.map(page => {
-              return {
-                ...page,
-                convos: page.convos.map(convo => {
-                  if (convo.id === convoId) {
-                    return {
-                      ...convo,
-                      unreadCount: 0,
+          if (existingConvo) {
+            return {
+              ...old,
+              pages: old.pages.map(page => {
+                return {
+                  ...page,
+                  convos: page.convos.map(convo => {
+                    if (convo.id === convoId) {
+                      return {
+                        ...convo,
+                        unreadCount: 0,
+                      }
                     }
-                  }
-                  return convo
-                }),
-              }
-            }),
+                    return convo
+                  }),
+                }
+              }),
+            }
+          } else {
+            // If we somehow marked a convo as read that doesn't exist in the
+            // list, then we don't need to do anything.
           }
-        } else {
-          // If we somehow marked a convo as read that doesn't exist in the
-          // list, then we don't need to do anything.
-        }
-      })
+        },
+      )
     },
   })
 }
diff --git a/src/state/queries/messages/get-convo-availability.ts b/src/state/queries/messages/get-convo-availability.ts
new file mode 100644
index 000000000..f545c3bba
--- /dev/null
+++ b/src/state/queries/messages/get-convo-availability.ts
@@ -0,0 +1,25 @@
+import {useQuery} from '@tanstack/react-query'
+
+import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
+import {useAgent} from '#/state/session'
+import {STALE} from '..'
+
+const RQKEY_ROOT = 'convo-availability'
+export const RQKEY = (did: string) => [RQKEY_ROOT, did]
+
+export function useGetConvoAvailabilityQuery(did: string) {
+  const agent = useAgent()
+
+  return useQuery({
+    queryKey: RQKEY(did),
+    queryFn: async () => {
+      const {data} = await agent.chat.bsky.convo.getConvoAvailability(
+        {members: [did]},
+        {headers: DM_SERVICE_HEADERS},
+      )
+
+      return data
+    },
+    staleTime: STALE.INFINITY,
+  })
+}
diff --git a/src/state/queries/messages/get-convo-for-members.ts b/src/state/queries/messages/get-convo-for-members.ts
index 7979e0665..3f45c2328 100644
--- a/src/state/queries/messages/get-convo-for-members.ts
+++ b/src/state/queries/messages/get-convo-for-members.ts
@@ -1,14 +1,10 @@
 import {ChatBskyConvoGetConvoForMembers} from '@atproto/api'
-import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
 
 import {logger} from '#/logger'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
-import {STALE} from '..'
-import {RQKEY as CONVO_KEY} from './conversation'
-
-const RQKEY_ROOT = 'convo-for-user'
-export const RQKEY = (did: string) => [RQKEY_ROOT, did]
+import {precacheConvoQuery} from './conversation'
 
 export function useGetConvoForMembers({
   onSuccess,
@@ -22,7 +18,7 @@ export function useGetConvoForMembers({
 
   return useMutation({
     mutationFn: async (members: string[]) => {
-      const {data} = await agent.api.chat.bsky.convo.getConvoForMembers(
+      const {data} = await agent.chat.bsky.convo.getConvoForMembers(
         {members: members},
         {headers: DM_SERVICE_HEADERS},
       )
@@ -30,7 +26,7 @@ export function useGetConvoForMembers({
       return data
     },
     onSuccess: data => {
-      queryClient.setQueryData(CONVO_KEY(data.convo.id), data.convo)
+      precacheConvoQuery(queryClient, data.convo)
       onSuccess?.(data)
     },
     onError: error => {
@@ -39,26 +35,3 @@ export function useGetConvoForMembers({
     },
   })
 }
-
-/**
- * Gets the conversation ID for a given DID. Returns null if it's not possible to message them.
- */
-export function useMaybeConvoForUser(did: string) {
-  const agent = useAgent()
-
-  return useQuery({
-    queryKey: RQKEY(did),
-    queryFn: async () => {
-      const convo = await agent.api.chat.bsky.convo
-        .getConvoForMembers({members: [did]}, {headers: DM_SERVICE_HEADERS})
-        .catch(() => ({success: null}))
-
-      if (convo.success) {
-        return convo.data.convo
-      } else {
-        return null
-      }
-    },
-    staleTime: STALE.INFINITY,
-  })
-}
diff --git a/src/state/queries/messages/leave-conversation.ts b/src/state/queries/messages/leave-conversation.ts
index 21cd1f18c..b17e515be 100644
--- a/src/state/queries/messages/leave-conversation.ts
+++ b/src/state/queries/messages/leave-conversation.ts
@@ -1,3 +1,4 @@
+import {useMemo} from 'react'
 import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
 import {
   useMutation,
@@ -8,7 +9,7 @@ import {
 import {logger} from '#/logger'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
-import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
+import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
 
 const RQKEY_ROOT = 'leave-convo'
 export function RQKEY(convoId: string | undefined) {
@@ -35,7 +36,7 @@ export function useLeaveConvo(
     mutationFn: async () => {
       if (!convoId) throw new Error('No convoId provided')
 
-      const {data} = await agent.api.chat.bsky.convo.leaveConvo(
+      const {data} = await agent.chat.bsky.convo.leaveConvo(
         {convoId},
         {headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
       )
@@ -45,7 +46,7 @@ export function useLeaveConvo(
     onMutate: () => {
       let prevPages: ChatBskyConvoListConvos.OutputSchema[] = []
       queryClient.setQueryData(
-        CONVO_LIST_KEY,
+        [CONVO_LIST_KEY],
         (old?: {
           pageParams: Array<string | undefined>
           pages: Array<ChatBskyConvoListConvos.OutputSchema>
@@ -67,13 +68,13 @@ export function useLeaveConvo(
       return {prevPages}
     },
     onSuccess: data => {
-      queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY})
+      queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
       onSuccess?.(data)
     },
     onError: (error, _, context) => {
       logger.error(error)
       queryClient.setQueryData(
-        CONVO_LIST_KEY,
+        [CONVO_LIST_KEY],
         (old?: {
           pageParams: Array<string | undefined>
           pages: Array<ChatBskyConvoListConvos.OutputSchema>
@@ -85,7 +86,7 @@ export function useLeaveConvo(
           }
         },
       )
-      queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY})
+      queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
       onError?.(error)
     },
   })
@@ -105,5 +106,8 @@ export function useLeftConvos() {
     filters: {mutationKey: [RQKEY_ROOT], status: 'success'},
     select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
   })
-  return [...pending, ...success].filter(id => id !== undefined)
+  return useMemo(
+    () => [...pending, ...success].filter(id => id !== undefined),
+    [pending, success],
+  )
 }
diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx
index 8c9d6c429..f5fce6347 100644
--- a/src/state/queries/messages/list-conversations.tsx
+++ b/src/state/queries/messages/list-conversations.tsx
@@ -9,6 +9,7 @@ import {
   ChatBskyConvoDefs,
   ChatBskyConvoListConvos,
   moderateProfile,
+  ModerationOpts,
 } from '@atproto/api'
 import {
   InfiniteData,
@@ -23,26 +24,39 @@ import {useMessagesEventBus} from '#/state/messages/events'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent, useSession} from '#/state/session'
+import {useLeftConvos} from './leave-conversation'
 
-export const RQKEY = ['convo-list']
+export const RQKEY_ROOT = 'convo-list'
+export const RQKEY = (
+  status: 'accepted' | 'request' | 'all',
+  readState: 'all' | 'unread' = 'all',
+) => [RQKEY_ROOT, status, readState]
 type RQPageParam = string | undefined
 
 export function useListConvosQuery({
   enabled,
+  status,
+  readState = 'all',
 }: {
   enabled?: boolean
+  status?: 'request' | 'accepted'
+  readState?: 'all' | 'unread'
 } = {}) {
   const agent = useAgent()
 
   return useInfiniteQuery({
     enabled,
-    queryKey: RQKEY,
+    queryKey: RQKEY(status ?? 'all', readState),
     queryFn: async ({pageParam}) => {
-      const {data} = await agent.api.chat.bsky.convo.listConvos(
-        {cursor: pageParam, limit: 20},
+      const {data} = await agent.chat.bsky.convo.listConvos(
+        {
+          limit: 20,
+          cursor: pageParam,
+          readState: readState === 'unread' ? 'unread' : undefined,
+          status,
+        },
         {headers: DM_SERVICE_HEADERS},
       )
-
       return data
     },
     initialPageParam: undefined as RQPageParam,
@@ -50,9 +64,10 @@ export function useListConvosQuery({
   })
 }
 
-const ListConvosContext = createContext<ChatBskyConvoDefs.ConvoView[] | null>(
-  null,
-)
+const ListConvosContext = createContext<{
+  accepted: ChatBskyConvoDefs.ConvoView[]
+  request: ChatBskyConvoDefs.ConvoView[]
+} | null>(null)
 
 export function useListConvos() {
   const ctx = useContext(ListConvosContext)
@@ -62,12 +77,13 @@ export function useListConvos() {
   return ctx
 }
 
+const empty = {accepted: [], request: []}
 export function ListConvosProvider({children}: {children: React.ReactNode}) {
   const {hasSession} = useSession()
 
   if (!hasSession) {
     return (
-      <ListConvosContext.Provider value={[]}>
+      <ListConvosContext.Provider value={empty}>
         {children}
       </ListConvosContext.Provider>
     )
@@ -81,20 +97,23 @@ export function ListConvosProviderInner({
 }: {
   children: React.ReactNode
 }) {
-  const {refetch, data} = useListConvosQuery()
+  const {refetch, data} = useListConvosQuery({readState: 'unread'})
   const messagesBus = useMessagesEventBus()
   const queryClient = useQueryClient()
   const {currentConvoId} = useCurrentConvoId()
   const {currentAccount} = useSession()
+  const leftConvos = useLeftConvos()
 
-  const debouncedRefetch = useMemo(
-    () =>
-      throttle(refetch, 500, {
-        leading: true,
-        trailing: true,
-      }),
-    [refetch],
-  )
+  const debouncedRefetch = useMemo(() => {
+    const refetchAndInvalidate = () => {
+      refetch()
+      queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
+    }
+    return throttle(refetchAndInvalidate, 500, {
+      leading: true,
+      trailing: true,
+    })
+  }, [refetch, queryClient])
 
   useEffect(() => {
     const unsub = messagesBus.on(
@@ -105,69 +124,159 @@ export function ListConvosProviderInner({
           if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
             debouncedRefetch()
           } else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
-            queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) =>
-              optimisticDelete(log.convoId, old),
+            queryClient.setQueriesData(
+              {queryKey: [RQKEY_ROOT]},
+              (old?: ConvoListQueryData) => optimisticDelete(log.convoId, old),
             )
           } else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
-            queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) =>
-              optimisticUpdate(log.convoId, old, convo => {
-                if (
-                  (ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
-                    ChatBskyConvoDefs.isMessageView(log.message)) &&
-                  (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) ||
-                    ChatBskyConvoDefs.isMessageView(convo.lastMessage))
-                ) {
-                  return log.message.id === convo.lastMessage.id
-                    ? {
-                        ...convo,
-                        rev: log.rev,
-                        lastMessage: log.message,
-                      }
-                    : convo
-                } else {
-                  return convo
-                }
-              }),
+            queryClient.setQueriesData(
+              {queryKey: [RQKEY_ROOT]},
+              (old?: ConvoListQueryData) =>
+                optimisticUpdate(log.convoId, old, convo => {
+                  if (
+                    (ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
+                      ChatBskyConvoDefs.isMessageView(log.message)) &&
+                    (ChatBskyConvoDefs.isDeletedMessageView(
+                      convo.lastMessage,
+                    ) ||
+                      ChatBskyConvoDefs.isMessageView(convo.lastMessage))
+                  ) {
+                    return log.message.id === convo.lastMessage.id
+                      ? {
+                          ...convo,
+                          rev: log.rev,
+                          lastMessage: log.message,
+                        }
+                      : convo
+                  } else {
+                    return convo
+                  }
+                }),
             )
           } else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
             // Store in a new var to avoid TS errors due to closures.
             const logRef: ChatBskyConvoDefs.LogCreateMessage = log
 
-            queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
-              if (!old) return old
+            // Get all matching queries
+            const queries = queryClient.getQueriesData<ConvoListQueryData>({
+              queryKey: [RQKEY_ROOT],
+            })
 
-              function updateConvo(convo: ChatBskyConvoDefs.ConvoView) {
-                let unreadCount = convo.unreadCount
-                if (convo.id !== currentConvoId) {
-                  if (
-                    ChatBskyConvoDefs.isMessageView(logRef.message) ||
-                    ChatBskyConvoDefs.isDeletedMessageView(logRef.message)
-                  ) {
-                    if (logRef.message.sender.did !== currentAccount?.did) {
-                      unreadCount++
+            // Check if convo exists in any query
+            let foundConvo: ChatBskyConvoDefs.ConvoView | null = null
+            for (const [_key, query] of queries) {
+              if (!query) continue
+              const convo = getConvoFromQueryData(logRef.convoId, query)
+              if (convo) {
+                foundConvo = convo
+                break
+              }
+            }
+
+            if (!foundConvo) {
+              // Convo not found, trigger refetch
+              debouncedRefetch()
+              return
+            }
+
+            // Update the convo
+            const updatedConvo = {
+              ...foundConvo,
+              rev: logRef.rev,
+              lastMessage: logRef.message,
+              unreadCount:
+                foundConvo.id !== currentConvoId
+                  ? (ChatBskyConvoDefs.isMessageView(logRef.message) ||
+                      ChatBskyConvoDefs.isDeletedMessageView(logRef.message)) &&
+                    logRef.message.sender.did !== currentAccount?.did
+                    ? foundConvo.unreadCount + 1
+                    : foundConvo.unreadCount
+                  : 0,
+            }
+
+            function filterConvoFromPage(convo: ChatBskyConvoDefs.ConvoView[]) {
+              return convo.filter(c => c.id !== logRef.convoId)
+            }
+
+            // Update all matching queries
+            function updateFn(old?: ConvoListQueryData) {
+              if (!old) return old
+              return {
+                ...old,
+                pages: old.pages.map((page, i) => {
+                  if (i === 0) {
+                    return {
+                      ...page,
+                      convos: [
+                        updatedConvo,
+                        ...filterConvoFromPage(page.convos),
+                      ],
                     }
                   }
-                } else {
-                  unreadCount = 0
-                }
-
-                return {
+                  return {
+                    ...page,
+                    convos: filterConvoFromPage(page.convos),
+                  }
+                }),
+              }
+            }
+            // always update the unread one
+            queryClient.setQueriesData(
+              {queryKey: RQKEY('all', 'unread')},
+              (old?: ConvoListQueryData) =>
+                old
+                  ? updateFn(old)
+                  : ({
+                      pageParams: [undefined],
+                      pages: [{convos: [updatedConvo], cursor: undefined}],
+                    } satisfies ConvoListQueryData),
+            )
+            // update the other ones based on status of the incoming message
+            if (updatedConvo.status === 'accepted') {
+              queryClient.setQueriesData(
+                {queryKey: RQKEY('accepted')},
+                updateFn,
+              )
+            } else if (updatedConvo.status === 'request') {
+              queryClient.setQueriesData({queryKey: RQKEY('request')}, updateFn)
+            }
+          } else if (ChatBskyConvoDefs.isLogReadMessage(log)) {
+            const logRef: ChatBskyConvoDefs.LogReadMessage = log
+            queryClient.setQueriesData(
+              {queryKey: [RQKEY_ROOT]},
+              (old?: ConvoListQueryData) =>
+                optimisticUpdate(logRef.convoId, old, convo => ({
                   ...convo,
+                  unreadCount: 0,
                   rev: logRef.rev,
-                  lastMessage: logRef.message,
-                  unreadCount,
+                })),
+            )
+          } else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
+            const logRef: ChatBskyConvoDefs.LogAcceptConvo = log
+            const requests = queryClient.getQueryData<ConvoListQueryData>(
+              RQKEY('request'),
+            )
+            if (!requests) {
+              debouncedRefetch()
+              return
+            }
+            const acceptedConvo = getConvoFromQueryData(log.convoId, requests)
+            if (!acceptedConvo) {
+              debouncedRefetch()
+              return
+            }
+            queryClient.setQueryData(
+              RQKEY('request'),
+              (old?: ConvoListQueryData) =>
+                optimisticDelete(logRef.convoId, old),
+            )
+            queryClient.setQueriesData(
+              {queryKey: RQKEY('accepted')},
+              (old?: ConvoListQueryData) => {
+                if (!old) {
+                  debouncedRefetch()
+                  return old
                 }
-              }
-
-              function filterConvoFromPage(
-                convo: ChatBskyConvoDefs.ConvoView[],
-              ) {
-                return convo.filter(c => c.id !== logRef.convoId)
-              }
-
-              const existingConvo = getConvoFromQueryData(logRef.convoId, old)
-
-              if (existingConvo) {
                 return {
                   ...old,
                   pages: old.pages.map((page, i) => {
@@ -175,26 +284,38 @@ export function ListConvosProviderInner({
                       return {
                         ...page,
                         convos: [
-                          updateConvo(existingConvo),
-                          ...filterConvoFromPage(page.convos),
+                          {...acceptedConvo, status: 'accepted'},
+                          ...page.convos,
                         ],
                       }
                     }
-                    return {
-                      ...page,
-                      convos: filterConvoFromPage(page.convos),
-                    }
+                    return page
                   }),
                 }
-              } else {
-                /**
-                 * We received a message from an conversation old enough that
-                 * it doesn't exist in the query cache, meaning we need to
-                 * refetch and bump the old convo to the top.
-                 */
-                debouncedRefetch()
-              }
-            })
+              },
+            )
+          } else if (ChatBskyConvoDefs.isLogMuteConvo(log)) {
+            const logRef: ChatBskyConvoDefs.LogMuteConvo = log
+            queryClient.setQueriesData(
+              {queryKey: [RQKEY_ROOT]},
+              (old?: ConvoListQueryData) =>
+                optimisticUpdate(logRef.convoId, old, convo => ({
+                  ...convo,
+                  muted: true,
+                  rev: logRef.rev,
+                })),
+            )
+          } else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) {
+            const logRef: ChatBskyConvoDefs.LogUnmuteConvo = log
+            queryClient.setQueriesData(
+              {queryKey: [RQKEY_ROOT]},
+              (old?: ConvoListQueryData) =>
+                optimisticUpdate(logRef.convoId, old, convo => ({
+                  ...convo,
+                  muted: false,
+                  rev: logRef.rev,
+                })),
+            )
           }
         }
       },
@@ -208,15 +329,21 @@ export function ListConvosProviderInner({
   }, [
     messagesBus,
     currentConvoId,
-    refetch,
     queryClient,
     currentAccount?.did,
     debouncedRefetch,
   ])
 
   const ctx = useMemo(() => {
-    return data?.pages.flatMap(page => page.convos) ?? []
-  }, [data])
+    const convos =
+      data?.pages
+        .flatMap(page => page.convos)
+        .filter(convo => !leftConvos.includes(convo.id)) ?? []
+    return {
+      accepted: convos.filter(conv => conv.status === 'accepted'),
+      request: convos.filter(conv => conv.status === 'request'),
+    }
+  }, [data, leftConvos])
 
   return (
     <ListConvosContext.Provider value={ctx}>
@@ -228,38 +355,76 @@ export function ListConvosProviderInner({
 export function useUnreadMessageCount() {
   const {currentConvoId} = useCurrentConvoId()
   const {currentAccount} = useSession()
-  const convos = useListConvos()
+  const {accepted, request} = useListConvos()
   const moderationOpts = useModerationOpts()
 
-  const count = useMemo(() => {
-    return (
-      convos
-        .filter(convo => convo.id !== currentConvoId)
-        .reduce((acc, convo) => {
-          const otherMember = convo.members.find(
-            member => member.did !== currentAccount?.did,
-          )
-
-          if (!otherMember || !moderationOpts) return acc
-
-          const moderation = moderateProfile(otherMember, moderationOpts)
-          const shouldIgnore =
-            convo.muted ||
-            moderation.blocked ||
-            otherMember.did === 'missing.invalid'
-          const unreadCount = !shouldIgnore && convo.unreadCount > 0 ? 1 : 0
-
-          return acc + unreadCount
-        }, 0) ?? 0
+  return useMemo<{
+    count: number
+    numUnread?: string
+    hasNew: boolean
+  }>(() => {
+    const acceptedCount = calculateCount(
+      accepted,
+      currentAccount?.did,
+      currentConvoId,
+      moderationOpts,
     )
-  }, [convos, currentAccount?.did, currentConvoId, moderationOpts])
-
-  return useMemo(() => {
-    return {
-      count,
-      numUnread: count > 0 ? (count > 10 ? '10+' : String(count)) : undefined,
+    const requestCount = calculateCount(
+      request,
+      currentAccount?.did,
+      currentConvoId,
+      moderationOpts,
+    )
+    if (acceptedCount > 0) {
+      const total = acceptedCount + Math.min(requestCount, 1)
+      return {
+        count: total,
+        numUnread: total > 10 ? '10+' : String(total),
+        // only needed when numUnread is undefined
+        hasNew: false,
+      }
+    } else if (requestCount > 0) {
+      return {
+        count: 1,
+        numUnread: undefined,
+        hasNew: true,
+      }
+    } else {
+      return {
+        count: 0,
+        numUnread: undefined,
+        hasNew: false,
+      }
     }
-  }, [count])
+  }, [accepted, request, currentAccount?.did, currentConvoId, moderationOpts])
+}
+
+function calculateCount(
+  convos: ChatBskyConvoDefs.ConvoView[],
+  currentAccountDid: string | undefined,
+  currentConvoId: string | undefined,
+  moderationOpts: ModerationOpts | undefined,
+) {
+  return (
+    convos
+      .filter(convo => convo.id !== currentConvoId)
+      .reduce((acc, convo) => {
+        const otherMember = convo.members.find(
+          member => member.did !== currentAccountDid,
+        )
+
+        if (!otherMember || !moderationOpts) return acc
+
+        const moderation = moderateProfile(otherMember, moderationOpts)
+        const shouldIgnore =
+          convo.muted ||
+          moderation.blocked ||
+          otherMember.handle === 'missing.invalid'
+        const unreadCount = !shouldIgnore && convo.unreadCount > 0 ? 1 : 0
+
+        return acc + unreadCount
+      }, 0) ?? 0
+  )
 }
 
 export type ConvoListQueryData = {
@@ -272,12 +437,16 @@ export function useOnMarkAsRead() {
 
   return useCallback(
     (chatId: string) => {
-      queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
-        return optimisticUpdate(chatId, old, convo => ({
-          ...convo,
-          unreadCount: 0,
-        }))
-      })
+      queryClient.setQueriesData(
+        {queryKey: [RQKEY_ROOT]},
+        (old?: ConvoListQueryData) => {
+          if (!old) return old
+          return optimisticUpdate(chatId, old, convo => ({
+            ...convo,
+            unreadCount: 0,
+          }))
+        },
+      )
     },
     [queryClient],
   )
@@ -285,10 +454,12 @@ export function useOnMarkAsRead() {
 
 function optimisticUpdate(
   chatId: string,
-  old: ConvoListQueryData,
-  updateFn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
+  old?: ConvoListQueryData,
+  updateFn?: (
+    convo: ChatBskyConvoDefs.ConvoView,
+  ) => ChatBskyConvoDefs.ConvoView,
 ) {
-  if (!old) return old
+  if (!old || !updateFn) return old
 
   return {
     ...old,
@@ -301,7 +472,7 @@ function optimisticUpdate(
   }
 }
 
-function optimisticDelete(chatId: string, old: ConvoListQueryData) {
+function optimisticDelete(chatId: string, old?: ConvoListQueryData) {
   if (!old) return old
 
   return {
@@ -331,7 +502,7 @@ export function* findAllProfilesInQueryData(
   const queryDatas = queryClient.getQueriesData<
     InfiniteData<ChatBskyConvoListConvos.OutputSchema>
   >({
-    queryKey: RQKEY,
+    queryKey: [RQKEY_ROOT],
   })
   for (const [_queryKey, queryData] of queryDatas) {
     if (!queryData?.pages) {
diff --git a/src/state/queries/messages/mute-conversation.ts b/src/state/queries/messages/mute-conversation.ts
index f32d02229..da9644145 100644
--- a/src/state/queries/messages/mute-conversation.ts
+++ b/src/state/queries/messages/mute-conversation.ts
@@ -8,7 +8,7 @@ import {InfiniteData, useMutation, useQueryClient} from '@tanstack/react-query'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
 import {RQKEY as CONVO_KEY} from './conversation'
-import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
+import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
 
 export function useMuteConvo(
   convoId: string | undefined,
@@ -53,7 +53,7 @@ export function useMuteConvo(
       )
       queryClient.setQueryData<
         InfiniteData<ChatBskyConvoListConvos.OutputSchema>
-      >(CONVO_LIST_KEY, prev => {
+      >([CONVO_LIST_KEY], prev => {
         if (!prev?.pages) return
         return {
           ...prev,
diff --git a/src/state/queries/messages/update-all-read.ts b/src/state/queries/messages/update-all-read.ts
new file mode 100644
index 000000000..72fa65ee6
--- /dev/null
+++ b/src/state/queries/messages/update-all-read.ts
@@ -0,0 +1,105 @@
+import {ChatBskyConvoListConvos} from '@atproto/api'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
+import {useAgent} from '#/state/session'
+import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
+
+export function useUpdateAllRead(
+  status: 'accepted' | 'request',
+  {
+    onSuccess,
+    onMutate,
+    onError,
+  }: {
+    onMutate?: () => void
+    onSuccess?: () => void
+    onError?: (error: Error) => void
+  },
+) {
+  const queryClient = useQueryClient()
+  const agent = useAgent()
+
+  return useMutation({
+    mutationFn: async () => {
+      const {data} = await agent.chat.bsky.convo.updateAllRead(
+        {status},
+        {headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
+      )
+
+      return data
+    },
+    onMutate: () => {
+      let prevPages: ChatBskyConvoListConvos.OutputSchema[] = []
+      queryClient.setQueryData(
+        CONVO_LIST_KEY(status),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          prevPages = old.pages
+          return {
+            ...old,
+            pages: old.pages.map(page => {
+              return {
+                ...page,
+                convos: page.convos.map(convo => {
+                  return {
+                    ...convo,
+                    unreadCount: 0,
+                  }
+                }),
+              }
+            }),
+          }
+        },
+      )
+      // remove unread convos from the badge query
+      queryClient.setQueryData(
+        CONVO_LIST_KEY('all', 'unread'),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          return {
+            ...old,
+            pages: old.pages.map(page => {
+              return {
+                ...page,
+                convos: page.convos.filter(convo => convo.status !== status),
+              }
+            }),
+          }
+        },
+      )
+      onMutate?.()
+      return {prevPages}
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
+      onSuccess?.()
+    },
+    onError: (error, _, context) => {
+      logger.error(error)
+      queryClient.setQueryData(
+        CONVO_LIST_KEY(status),
+        (old?: {
+          pageParams: Array<string | undefined>
+          pages: Array<ChatBskyConvoListConvos.OutputSchema>
+        }) => {
+          if (!old) return old
+          return {
+            ...old,
+            pages: context?.prevPages || old.pages,
+          }
+        },
+      )
+      queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
+      queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY('all', 'unread')})
+      onError?.(error)
+    },
+  })
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 2c98df634..227ca9d66 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -37,7 +37,7 @@ import {
   ProgressGuideAction,
   useProgressGuideControls,
 } from '../shell/progress-guide'
-import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-conversations'
+import {RQKEY_ROOT as RQKEY_LIST_CONVOS} from './messages/list-conversations'
 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
 import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
 
@@ -456,7 +456,7 @@ export function useProfileBlockMutationQueue(
       updateProfileShadow(queryClient, did, {
         blockingUri: finalBlockingUri,
       })
-      queryClient.invalidateQueries({queryKey: RQKEY_LIST_CONVOS})
+      queryClient.invalidateQueries({queryKey: [RQKEY_LIST_CONVOS]})
     },
   })