remove leave-convo suppression, use real optimistic update
useLeftConvos() filtered convos out of every list based on leave-mutation state, working around a slow backend leaveConvo query. now that the backend is fast, replace it with a working optimistic update: the old onMutate used setQueryData with just the root key, which never matched any real query, so it was a no-op. switch to setQueriesData (prefix match) across both the convo-list and requests caches, with snapshot rollback on error, mirroring useAcceptConversation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,6 @@ import {listenSoftReset} from '#/state/events'
|
|||||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
|
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
|
||||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
@@ -237,14 +236,9 @@ export function ChatList({
|
|||||||
useRefreshOnFocus(refetch)
|
useRefreshOnFocus(refetch)
|
||||||
useRefreshOnFocus(refetchInbox)
|
useRefreshOnFocus(refetchInbox)
|
||||||
|
|
||||||
const leftConvos = useLeftConvos()
|
|
||||||
|
|
||||||
const conversations = useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
const conversations = data.pages
|
const conversations = data.pages.flatMap(page => page.convos)
|
||||||
.flatMap(page => page.convos)
|
|
||||||
// filter out convos that are actively being left
|
|
||||||
.filter(convo => !leftConvos.includes(convo.id))
|
|
||||||
|
|
||||||
return conversations.map(
|
return conversations.map(
|
||||||
convo =>
|
convo =>
|
||||||
@@ -256,7 +250,7 @@ export function ChatList({
|
|||||||
) satisfies ListItem[]
|
) satisfies ListItem[]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data, leftConvos, selectedChat])
|
}, [data, selectedChat])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
@@ -453,7 +447,6 @@ export function Header({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
const leftConvos = useLeftConvos()
|
|
||||||
const {isWithinSplitView} = useIsWithinSplitView()
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
// In split view, the left column (and this header) stays mounted while the
|
// In split view, the left column (and this header) stays mounted while the
|
||||||
@@ -473,7 +466,6 @@ export function Header({
|
|||||||
.flatMap(page => page.convos)
|
.flatMap(page => page.convos)
|
||||||
.filter(
|
.filter(
|
||||||
convo =>
|
convo =>
|
||||||
!leftConvos.includes(convo.id) &&
|
|
||||||
!convo.muted &&
|
!convo.muted &&
|
||||||
convo.members.every(member => member.handle !== 'missing.invalid') &&
|
convo.members.every(member => member.handle !== 'missing.invalid') &&
|
||||||
(ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
(ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {cleanError} from '#/lib/strings/errors'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
|
||||||
import {useListConvoRequests} from '#/state/queries/messages/list-conversation-requests'
|
import {useListConvoRequests} from '#/state/queries/messages/list-conversation-requests'
|
||||||
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
@@ -70,16 +69,12 @@ export function MessagesInboxScreenInner({}: Props) {
|
|||||||
const listConvosQuery = useListConvoRequests()
|
const listConvosQuery = useListConvoRequests()
|
||||||
const {data} = listConvosQuery
|
const {data} = listConvosQuery
|
||||||
|
|
||||||
const leftConvos = useLeftConvos()
|
|
||||||
|
|
||||||
const conversations = useMemo<RequestItem[]>(() => {
|
const conversations = useMemo<RequestItem[]>(() => {
|
||||||
if (!data?.pages) return []
|
if (!data?.pages) return []
|
||||||
const items: RequestItem[] = []
|
const items: RequestItem[] = []
|
||||||
for (const page of data.pages) {
|
for (const page of data.pages) {
|
||||||
for (const item of page.requests) {
|
for (const item of page.requests) {
|
||||||
if (ChatBskyConvoDefs.isConvoView(item)) {
|
if (ChatBskyConvoDefs.isConvoView(item)) {
|
||||||
// filter out convos that are actively being left
|
|
||||||
if (leftConvos.includes(item.id)) continue
|
|
||||||
items.push({type: 'incoming', view: item})
|
items.push({type: 'incoming', view: item})
|
||||||
} else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) {
|
} else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) {
|
||||||
items.push({type: 'outgoing', view: item})
|
items.push({type: 'outgoing', view: item})
|
||||||
@@ -87,7 +82,7 @@ export function MessagesInboxScreenInner({}: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}, [data, leftConvos])
|
}, [data])
|
||||||
|
|
||||||
const hasUnreadConvos = useMemo(() => {
|
const hasUnreadConvos = useMemo(() => {
|
||||||
return conversations.some(
|
return conversations.some(
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import {useMemo} from 'react'
|
|
||||||
import {
|
import {
|
||||||
type ChatBskyConvoLeaveConvo,
|
type ChatBskyConvoLeaveConvo,
|
||||||
type ChatBskyConvoListConvos,
|
type ChatBskyConvoListConvos,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
useMutation,
|
|
||||||
useMutationState,
|
|
||||||
useQueryClient,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
|
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {
|
||||||
|
type ConvoRequestListQueryData,
|
||||||
|
optimisticDelete as optimisticDeleteRequest,
|
||||||
|
RQKEY_ROOT as REQUESTS_RQKEY_ROOT,
|
||||||
|
} from './list-conversation-requests'
|
||||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'leave-convo'
|
const RQKEY_ROOT = 'leave-convo'
|
||||||
@@ -20,6 +20,11 @@ export function RQKEY(convoId: string | undefined) {
|
|||||||
return [RQKEY_ROOT, convoId]
|
return [RQKEY_ROOT, convoId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConvoListQueryData = {
|
||||||
|
pageParams: Array<string | undefined>
|
||||||
|
pages: Array<ChatBskyConvoListConvos.OutputSchema>
|
||||||
|
}
|
||||||
|
|
||||||
export function useLeaveConvo(
|
export function useLeaveConvo(
|
||||||
convoId: string | undefined,
|
convoId: string | undefined,
|
||||||
{
|
{
|
||||||
@@ -48,31 +53,37 @@ export function useLeaveConvo(
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
let prevPages: ChatBskyConvoListConvos.OutputSchema[] = []
|
const prevConvoListQueries =
|
||||||
queryClient.setQueryData(
|
queryClient.getQueriesData<ConvoListQueryData>({
|
||||||
[CONVO_LIST_KEY],
|
queryKey: [CONVO_LIST_KEY],
|
||||||
(old?: {
|
})
|
||||||
pageParams: Array<string | undefined>
|
queryClient.setQueriesData<ConvoListQueryData>(
|
||||||
pages: Array<ChatBskyConvoListConvos.OutputSchema>
|
{queryKey: [CONVO_LIST_KEY]},
|
||||||
}) => {
|
old => {
|
||||||
if (!old) return old
|
if (!old) return old
|
||||||
prevPages = old.pages
|
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
pages: old.pages.map(page => {
|
pages: old.pages.map(page => ({
|
||||||
return {
|
|
||||||
...page,
|
...page,
|
||||||
convos: page.convos.filter(convo => convo.id !== convoId),
|
convos: page.convos.filter(convo => convo.id !== convoId),
|
||||||
}
|
})),
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
const prevRequestsQueries =
|
||||||
|
queryClient.getQueriesData<ConvoRequestListQueryData>({
|
||||||
|
queryKey: [REQUESTS_RQKEY_ROOT],
|
||||||
|
})
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => (convoId ? optimisticDeleteRequest(convoId, old) : old),
|
||||||
|
)
|
||||||
onMutate?.()
|
onMutate?.()
|
||||||
return {prevPages}
|
return {prevConvoListQueries, prevRequestsQueries}
|
||||||
},
|
},
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
if (convoId) {
|
if (convoId) {
|
||||||
void invalidateJoinLinkPreviewsForConvo(queryClient, convoId)
|
void invalidateJoinLinkPreviewsForConvo(queryClient, convoId)
|
||||||
}
|
}
|
||||||
@@ -80,41 +91,19 @@ export function useLeaveConvo(
|
|||||||
},
|
},
|
||||||
onError: (error, _, context) => {
|
onError: (error, _, context) => {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
queryClient.setQueryData(
|
if (context?.prevConvoListQueries) {
|
||||||
[CONVO_LIST_KEY],
|
for (const [queryKey, prevData] of context.prevConvoListQueries) {
|
||||||
(old?: {
|
queryClient.setQueryData(queryKey, prevData)
|
||||||
pageParams: Array<string | undefined>
|
}
|
||||||
pages: Array<ChatBskyConvoListConvos.OutputSchema>
|
}
|
||||||
}) => {
|
if (context?.prevRequestsQueries) {
|
||||||
if (!old) return old
|
for (const [queryKey, prevData] of context.prevRequestsQueries) {
|
||||||
return {
|
queryClient.setQueryData(queryKey, prevData)
|
||||||
...old,
|
}
|
||||||
pages: context?.prevPages || old.pages,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
onError?.(error)
|
onError?.(error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets currently pending and successful leave convo mutations
|
|
||||||
*
|
|
||||||
* @returns Array of `convoId`
|
|
||||||
*/
|
|
||||||
export function useLeftConvos() {
|
|
||||||
const pending = useMutationState({
|
|
||||||
filters: {mutationKey: [RQKEY_ROOT], status: 'pending'},
|
|
||||||
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
|
|
||||||
})
|
|
||||||
const success = useMutationState({
|
|
||||||
filters: {mutationKey: [RQKEY_ROOT], status: 'success'},
|
|
||||||
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
|
|
||||||
})
|
|
||||||
return useMemo(
|
|
||||||
() => [...pending, ...success].filter(id => id !== undefined),
|
|
||||||
[pending, success],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {useAgeAssurance} from '#/ageAssurance'
|
|||||||
import {type AgeAssuranceFlags} from '#/ageAssurance/types'
|
import {type AgeAssuranceFlags} from '#/ageAssurance/types'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {RQKEY as CONVO_KEY} from './conversation'
|
import {RQKEY as CONVO_KEY} from './conversation'
|
||||||
import {useLeftConvos} from './leave-conversation'
|
|
||||||
import {
|
import {
|
||||||
type ConvoRequestListQueryData,
|
type ConvoRequestListQueryData,
|
||||||
optimisticDelete as optimisticDeleteRequest,
|
optimisticDelete as optimisticDeleteRequest,
|
||||||
@@ -136,7 +135,6 @@ export function ListConvosProviderInner({
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {currentConvoId} = useCurrentConvoId()
|
const {currentConvoId} = useCurrentConvoId()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const leftConvos = useLeftConvos()
|
|
||||||
|
|
||||||
const debouncedRefetch = useMemo(() => {
|
const debouncedRefetch = useMemo(() => {
|
||||||
const refetchAndInvalidate = () => {
|
const refetchAndInvalidate = () => {
|
||||||
@@ -664,15 +662,12 @@ export function ListConvosProviderInner({
|
|||||||
])
|
])
|
||||||
|
|
||||||
const ctx = useMemo(() => {
|
const ctx = useMemo(() => {
|
||||||
const convos =
|
const convos = data?.pages.flatMap(page => page.convos) ?? []
|
||||||
data?.pages
|
|
||||||
.flatMap(page => page.convos)
|
|
||||||
.filter(convo => !leftConvos.includes(convo.id)) ?? []
|
|
||||||
return {
|
return {
|
||||||
accepted: convos.filter(conv => conv.status === 'accepted'),
|
accepted: convos.filter(conv => conv.status === 'accepted'),
|
||||||
request: convos.filter(conv => conv.status === 'request'),
|
request: convos.filter(conv => conv.status === 'request'),
|
||||||
}
|
}
|
||||||
}, [data, leftConvos])
|
}, [data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListConvosContext.Provider value={ctx}>
|
<ListConvosContext.Provider value={ctx}>
|
||||||
|
|||||||
Reference in New Issue
Block a user