Fix issue with stale unread message counts (#10953)
(cherry picked from commit da1f637160)
This commit is contained in:
@@ -300,7 +300,10 @@ export function ChatList({
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useListConvosQuery({status: 'accepted'})
|
||||
} = useListConvosQuery({
|
||||
status: 'accepted',
|
||||
kind: aa.flags.groupChatDisabled ? 'direct' : 'all',
|
||||
})
|
||||
|
||||
const {refetch: refetchInbox} = useListConvosQuery({
|
||||
status: 'request',
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
type ChatBskyActorDefs,
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoGetConvo,
|
||||
type ChatBskyConvoGetUnreadCounts,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type QueryClient,
|
||||
@@ -14,6 +15,11 @@ import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY,
|
||||
UNREAD_ACCEPTED_CAP,
|
||||
UNREAD_REQUEST_CAP,
|
||||
} from './get-unread-counts'
|
||||
import {
|
||||
type ConvoListQueryData,
|
||||
getConvoFromQueryData,
|
||||
@@ -74,11 +80,81 @@ export function useMarkAsReadMutation() {
|
||||
},
|
||||
onMutate({convoId}) {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
|
||||
// snapshot the list caches before the optimistic update so onError can
|
||||
// restore the convo rows alongside the badge count
|
||||
const prevListQueries = queryClient.getQueriesData<ConvoListQueryData>({
|
||||
queryKey: [LIST_CONVOS_KEY],
|
||||
})
|
||||
|
||||
// find the convo so we know which badge counter (if any) to decrement.
|
||||
// keep scanning past a stale unreadCount === 0 cache so another cache
|
||||
// holding the true unread state still drives the decrement
|
||||
let unreadStatus: ChatBskyConvoDefs.ConvoView['status'] | undefined
|
||||
for (const [, data] of prevListQueries) {
|
||||
if (!data) continue
|
||||
const convo = getConvoFromQueryData(convoId, data)
|
||||
if (convo?.unreadCount) {
|
||||
unreadStatus = convo.status
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
optimisticUpdate(convoId)
|
||||
|
||||
// the badge count query is a separate server query that the list caches
|
||||
// don't feed, so decrement it here to keep the badge in sync
|
||||
const prevUnreadCountsQueries =
|
||||
queryClient.getQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>({
|
||||
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
|
||||
})
|
||||
if (unreadStatus) {
|
||||
queryClient.setQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>(
|
||||
{queryKey: UNREAD_COUNTS_PARTIAL_KEY},
|
||||
old => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
...(unreadStatus === 'request'
|
||||
? {
|
||||
unreadRequestConvos:
|
||||
old.unreadRequestConvos >= UNREAD_REQUEST_CAP
|
||||
? old.unreadRequestConvos
|
||||
: Math.max(0, old.unreadRequestConvos - 1),
|
||||
}
|
||||
: {
|
||||
unreadAcceptedConvos:
|
||||
old.unreadAcceptedConvos >= UNREAD_ACCEPTED_CAP
|
||||
? old.unreadAcceptedConvos
|
||||
: Math.max(0, old.unreadAcceptedConvos - 1),
|
||||
}),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
return {prevListQueries, prevUnreadCountsQueries}
|
||||
},
|
||||
onError(_, __, context) {
|
||||
if (context?.prevListQueries) {
|
||||
for (const [queryKey, prevData] of context.prevListQueries) {
|
||||
queryClient.setQueryData(queryKey, prevData)
|
||||
}
|
||||
}
|
||||
if (context?.prevUnreadCountsQueries) {
|
||||
for (const [queryKey, prevData] of context.prevUnreadCountsQueries) {
|
||||
queryClient.setQueryData(queryKey, prevData)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess(_, {convoId}) {
|
||||
if (!convoId) return
|
||||
|
||||
// the optimistic badge arithmetic can drift from the server (e.g. a convo
|
||||
// whose status differs between caches, or a sentinel-capped count). invalidate
|
||||
// so the 15s-stale count query self-corrects on next access rather than
|
||||
// waiting for a log event
|
||||
void queryClient.invalidateQueries({queryKey: UNREAD_COUNTS_PARTIAL_KEY})
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{queryKey: [LIST_CONVOS_KEY]},
|
||||
(old?: ConvoListQueryData) => {
|
||||
|
||||
@@ -10,6 +10,14 @@ export const RQKEY = (includeGroupChats: boolean) =>
|
||||
[RQKEY_ROOT, includeGroupChats] as const
|
||||
export const RQKEY_PARTIAL = [RQKEY_ROOT] as const
|
||||
|
||||
// the server sentinel-caps the badge counts: unreadAcceptedConvos maxes at 31
|
||||
// (meaning "more than 30") and unreadRequestConvos at 11 (meaning "more than
|
||||
// 10"). at the cap the value is no longer an exact count, so consumers must not
|
||||
// treat it as one - both the optimistic decrement and the badge display ceiling
|
||||
// key off these.
|
||||
export const UNREAD_ACCEPTED_CAP = 31
|
||||
export const UNREAD_REQUEST_CAP = 11
|
||||
|
||||
export function useUnreadCountsQuery() {
|
||||
const agent = useAgent()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
@@ -23,6 +23,7 @@ import * as bsky from '#/types/bsky'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {
|
||||
RQKEY_PARTIAL as UNREAD_COUNTS_RQKEY_PARTIAL,
|
||||
UNREAD_ACCEPTED_CAP,
|
||||
useUnreadCountsQuery,
|
||||
} from './get-unread-counts'
|
||||
import {
|
||||
@@ -858,7 +859,15 @@ export function useUnreadMessageCount(): {
|
||||
const total = accepted + Math.min(request, 1)
|
||||
return {
|
||||
count: total,
|
||||
numUnread: total > 10 ? '10+' : String(total),
|
||||
// accepted is sentinel-capped at UNREAD_ACCEPTED_CAP (meaning "more than
|
||||
// cap - 1"). show the "+" overflow label only when accepted is actually
|
||||
// capped - the +1 request nudge must not trip it at exactly cap - 1
|
||||
// accepted convos. otherwise clamp the number to cap - 1 so the nudge
|
||||
// never surfaces the sentinel value (31) itself
|
||||
numUnread:
|
||||
accepted >= UNREAD_ACCEPTED_CAP
|
||||
? `${UNREAD_ACCEPTED_CAP - 1}+`
|
||||
: String(Math.min(total, UNREAD_ACCEPTED_CAP - 1)),
|
||||
// only needed when numUnread is undefined
|
||||
hasNew: false,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {type ChatBskyConvoGetUnreadCounts} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY} from './get-unread-counts'
|
||||
import {
|
||||
type ConvoRequestListQueryData,
|
||||
markAllRead as markAllRequestsRead,
|
||||
@@ -94,10 +96,36 @@ export function useUpdateAllRead(
|
||||
markAllRequestsRead,
|
||||
)
|
||||
}
|
||||
// zero out the badge count query that actually drives the unread badge,
|
||||
// since it's a separate server query that the list caches don't feed
|
||||
const prevUnreadCountsQueries =
|
||||
queryClient.getQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>({
|
||||
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
|
||||
})
|
||||
queryClient.setQueriesData<ChatBskyConvoGetUnreadCounts.OutputSchema>(
|
||||
{queryKey: UNREAD_COUNTS_PARTIAL_KEY},
|
||||
old => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
...(status === 'accepted'
|
||||
? {unreadAcceptedConvos: 0}
|
||||
: {unreadRequestConvos: 0}),
|
||||
}
|
||||
},
|
||||
)
|
||||
onMutate?.()
|
||||
return {prevConvoListQueries, prevRequestsQueries}
|
||||
return {
|
||||
prevConvoListQueries,
|
||||
prevRequestsQueries,
|
||||
prevUnreadCountsQueries,
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
// the optimistic badge zeroing can drift from the server, so invalidate
|
||||
// the count query to let it self-correct on next access rather than
|
||||
// waiting for a log event
|
||||
void queryClient.invalidateQueries({queryKey: UNREAD_COUNTS_PARTIAL_KEY})
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: CONVO_LIST_PARTIAL_KEY(status),
|
||||
})
|
||||
@@ -121,6 +149,11 @@ export function useUpdateAllRead(
|
||||
queryClient.setQueryData(queryKey, prevData)
|
||||
}
|
||||
}
|
||||
if (context?.prevUnreadCountsQueries) {
|
||||
for (const [queryKey, prevData] of context.prevUnreadCountsQueries) {
|
||||
queryClient.setQueryData(queryKey, prevData)
|
||||
}
|
||||
}
|
||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_ROOT_KEY]})
|
||||
if (status === 'request') {
|
||||
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||
|
||||
Reference in New Issue
Block a user