88} from '@atproto/api'
99import {
1010 type InfiniteData ,
11+ type Query ,
1112 type QueryClient ,
13+ type QueryKey ,
1214 useInfiniteQuery ,
1315 useQueryClient ,
1416} from '@tanstack/react-query'
@@ -48,7 +50,60 @@ export const RQKEY = (
4850 | 'locked-permanently'
4951 | undefined = undefined ,
5052 limit ?: number ,
51- ) => [ RQKEY_ROOT , status , readState , kind , lockStatus , limit ]
53+ ) => [ RQKEY_ROOT , status , readState , kind , lockStatus , limit ] as const
54+
55+ /**
56+ * Prefix key matching every convo-list query with the given status (and
57+ * optionally readState), regardless of the remaining params (kind,
58+ * lockStatus, limit). Only valid with prefix-matching APIs (setQueriesData,
59+ * getQueriesData, invalidateQueries) - exact-match APIs (getQueryData,
60+ * setQueryData) hash the full key and will never match a prefix.
61+ */
62+ export const RQKEY_PARTIAL = (
63+ status : 'accepted' | 'request' | 'all' ,
64+ readState ?: 'all' | 'unread' ,
65+ ) => ( readState ? [ RQKEY_ROOT , status , readState ] : [ RQKEY_ROOT , status ] )
66+
67+ /**
68+ * Whether a convo satisfies the filters encoded in a convo-list query key.
69+ * Caches are server-filtered, so optimistic inserts must apply the same
70+ * filters client-side or convos leak into lists that should exclude them.
71+ */
72+ export function convoMatchesQueryKey (
73+ convo : ChatBskyConvoDefs . ConvoView ,
74+ queryKey : QueryKey ,
75+ ) : boolean {
76+ const [ , status , readState , kind , lockStatus ] = queryKey as ReturnType <
77+ typeof RQKEY
78+ >
79+ if ( status !== 'all' && status !== convo . status ) return false
80+ if ( readState === 'unread' && convo . unreadCount === 0 ) return false
81+ if ( ChatBskyConvoDefs . isGroupConvo ( convo . kind ) ) {
82+ if ( kind === 'direct' ) return false
83+ if ( lockStatus && convo . kind . lockStatus !== lockStatus ) return false
84+ } else {
85+ if ( kind === 'group' ) return false
86+ // direct convos are never locked
87+ if ( lockStatus && lockStatus !== 'unlocked' ) return false
88+ }
89+ return true
90+ }
91+
92+ /**
93+ * Query predicate for optimistically upserting a convo into convo-list
94+ * caches. Targets caches whose filters the convo satisfies, plus caches the
95+ * convo is already in - those get updated in place even if the convo no
96+ * longer matches (e.g. unreadCount dropped to 0), mirroring how read/mute
97+ * log events update convos in place everywhere.
98+ */
99+ export function convoListQueryPredicate ( convo : ChatBskyConvoDefs . ConvoView ) {
100+ return ( query : Query ) : boolean => {
101+ const data = query . state . data as ConvoListQueryData | undefined
102+ if ( data && getConvoFromQueryData ( convo . id , data ) ) return true
103+ return convoMatchesQueryKey ( convo , query . queryKey )
104+ }
105+ }
106+
52107type RQPageParam = string | undefined
53108
54109export function useListConvosQuery ( {
@@ -347,9 +402,12 @@ export function ListConvosProviderInner({
347402 } ) ,
348403 }
349404 }
350- // always update the unread one
405+ // always update the unread ones, where the convo qualifies
351406 queryClient . setQueriesData (
352- { queryKey : RQKEY ( 'all' , 'unread' ) } ,
407+ {
408+ queryKey : RQKEY_PARTIAL ( 'all' , 'unread' ) ,
409+ predicate : convoListQueryPredicate ( updatedConvo ) ,
410+ } ,
353411 ( old ?: ConvoListQueryData ) =>
354412 old
355413 ? updateFn ( old )
@@ -361,11 +419,20 @@ export function ListConvosProviderInner({
361419 // update the other ones based on status of the incoming message
362420 if ( updatedConvo . status === 'accepted' ) {
363421 queryClient . setQueriesData (
364- { queryKey : RQKEY ( 'accepted' ) } ,
422+ {
423+ queryKey : RQKEY_PARTIAL ( 'accepted' ) ,
424+ predicate : convoListQueryPredicate ( updatedConvo ) ,
425+ } ,
365426 updateFn ,
366427 )
367428 } else if ( updatedConvo . status === 'request' ) {
368- queryClient . setQueriesData ( { queryKey : RQKEY ( 'request' ) } , updateFn )
429+ queryClient . setQueriesData (
430+ {
431+ queryKey : RQKEY_PARTIAL ( 'request' ) ,
432+ predicate : convoListQueryPredicate ( updatedConvo ) ,
433+ } ,
434+ updateFn ,
435+ )
369436 // also move-to-top in the new requests cache
370437 queryClient . setQueriesData < ConvoRequestListQueryData > (
371438 { queryKey : [ REQUESTS_RQKEY_ROOT ] } ,
@@ -385,20 +452,26 @@ export function ListConvosProviderInner({
385452 rev : log . rev ,
386453 } ) )
387454 } else if ( ChatBskyConvoDefs . isLogAcceptConvo ( log ) ) {
388- const requests = queryClient . getQueryData < ConvoListQueryData > (
389- RQKEY ( 'request' ) ,
390- )
391- if ( ! requests ) {
392- debouncedRefetch ( )
393- return
455+ const requestQueries =
456+ queryClient . getQueriesData < ConvoListQueryData > ( {
457+ queryKey : RQKEY_PARTIAL ( 'request' ) ,
458+ } )
459+ let foundConvo : ChatBskyConvoDefs . ConvoView | null = null
460+ for ( const [ _key , data ] of requestQueries ) {
461+ if ( ! data ) continue
462+ foundConvo = getConvoFromQueryData ( log . convoId , data )
463+ if ( foundConvo ) break
394464 }
395- const acceptedConvo = getConvoFromQueryData ( log . convoId , requests )
396- if ( ! acceptedConvo ) {
465+ if ( ! foundConvo ) {
397466 debouncedRefetch ( )
398467 return
399468 }
400- queryClient . setQueryData (
401- RQKEY ( 'request' ) ,
469+ const acceptedConvo : ChatBskyConvoDefs . ConvoView = {
470+ ...foundConvo ,
471+ status : 'accepted' ,
472+ }
473+ queryClient . setQueriesData (
474+ { queryKey : RQKEY_PARTIAL ( 'request' ) } ,
402475 ( old ?: ConvoListQueryData ) => optimisticDelete ( log . convoId , old ) ,
403476 )
404477 // also remove from the new requests cache
@@ -407,7 +480,10 @@ export function ListConvosProviderInner({
407480 old => optimisticDeleteRequest ( log . convoId , old ) ,
408481 )
409482 queryClient . setQueriesData (
410- { queryKey : RQKEY ( 'accepted' ) } ,
483+ {
484+ queryKey : RQKEY_PARTIAL ( 'accepted' ) ,
485+ predicate : convoListQueryPredicate ( acceptedConvo ) ,
486+ } ,
411487 ( old ?: ConvoListQueryData ) => {
412488 if ( ! old ) {
413489 debouncedRefetch ( )
@@ -420,12 +496,15 @@ export function ListConvosProviderInner({
420496 return {
421497 ...page ,
422498 convos : [
423- { ... acceptedConvo , status : 'accepted' } ,
424- ...page . convos ,
499+ acceptedConvo ,
500+ ...page . convos . filter ( c => c . id !== log . convoId ) ,
425501 ] ,
426502 }
427503 }
428- return page
504+ return {
505+ ...page ,
506+ convos : page . convos . filter ( c => c . id !== log . convoId ) ,
507+ }
429508 } ) ,
430509 }
431510 } ,
@@ -723,7 +802,14 @@ export function useUnreadMessageCount() {
723802 hasNew : false ,
724803 }
725804 }
726- } , [ accepted , request , currentAccount ?. did , currentConvoId , moderationOpts ] )
805+ } , [
806+ accepted ,
807+ request ,
808+ currentAccount ?. did ,
809+ currentConvoId ,
810+ moderationOpts ,
811+ aa . flags ,
812+ ] )
727813}
728814
729815function calculateCount (
@@ -911,7 +997,7 @@ function addMemberToConvoView(
911997 }
912998}
913999
914- function optimisticDelete ( chatId : string , old ?: ConvoListQueryData ) {
1000+ export function optimisticDelete ( chatId : string , old ?: ConvoListQueryData ) {
9151001 if ( ! old ) return old
9161002
9171003 return {
0 commit comments