* improve error screen

* add chat request prompt

* mock up inbox

* bigger button

* use two-button layout

* get inbox working somewhat

* fix type errors

* fetch both pages for badge

* don't include read convos in preview

* in-chat ui for non-accepted convos (part 1)

* add chatstatusinfo

* fix status info not disappearing

* get chat status info working

* change min item height

* move files around

* add updated sdk

* improve badge behaviour

* mock up mark all as read

* update sdk to 0.14.4

* hide chat status info if initiating convo

* fix unread count for deleted accounts

* add toasts after rejection

* add prompt to delete

* adjust badge on desktop

* requests -> chat requests

* fix height flicker

* add mark as read button to header

* add mark all as read APIs

* separate avatarstack into two components (#7845)

* fix messages being hidden behind chatstatusinfo

* show inbox preview on empty state

* fix empty state again

* Use new convo availability API (#7812)

* [Inbox] Accept button on convo screen (#7795)

* accept button on convo screen

* fix types

* fix type error

* improve spacing

* [DMs] Implement new log types (#7835)

* optimise badge state

* add read message log

* add isLogAcceptConvo

* mute/unmute convo logs

* use setqueriesdata

* always show label on button

* optimistically update badge

* change incorrect unread count change

* Update src/screens/Messages/Inbox.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestButtons.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestButtons.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestListItem.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* fix race condition with accepting convo

* fix back button on web

* filter left convos from badge

* update atproto to fix CI

* Add accept override external to convo (#7891)

* Add accept override external to convo

* rm log

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2025-03-04 13:54:19 +00:00
committed by GitHub
parent 5c14f69566
commit c995eb2f2f
40 changed files with 1804 additions and 344 deletions
+21
View File
@@ -105,6 +105,7 @@ export class Convo {
this.ingestFirehose = this.ingestFirehose.bind(this)
this.onFirehoseConnect = this.onFirehoseConnect.bind(this)
this.onFirehoseError = this.onFirehoseError.bind(this)
this.markConvoAccepted = this.markConvoAccepted.bind(this)
}
private commit() {
@@ -145,6 +146,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
markConvoAccepted: undefined,
}
}
case ConvoStatus.Disabled:
@@ -162,6 +164,7 @@ export class Convo {
deleteMessage: this.deleteMessage,
sendMessage: this.sendMessage,
fetchMessageHistory: this.fetchMessageHistory,
markConvoAccepted: this.markConvoAccepted,
}
}
case ConvoStatus.Error: {
@@ -176,6 +179,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
markConvoAccepted: undefined,
}
}
default: {
@@ -190,6 +194,7 @@ export class Convo {
deleteMessage: undefined,
sendMessage: undefined,
fetchMessageHistory: undefined,
markConvoAccepted: undefined,
}
}
}
@@ -780,6 +785,12 @@ export class Convo {
id: tempId,
message,
})
if (this.convo?.status === 'request') {
this.convo = {
...this.convo,
status: 'accepted',
}
}
this.commit()
if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) {
@@ -787,6 +798,16 @@ export class Convo {
}
}
markConvoAccepted() {
if (this.convo) {
this.convo = {
...this.convo,
status: 'accepted',
}
}
this.commit()
}
async processPendingMessages() {
logger.debug(
`Convo: processing messages (${this.pendingMessages.size} remaining)`,
+2 -2
View File
@@ -19,7 +19,7 @@ import {
RQKEY as getConvoKey,
useMarkAsReadMutation,
} from '#/state/queries/messages/conversation'
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-conversations'
import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
@@ -104,7 +104,7 @@ export function ConvoProvider({
})
}
queryClient.invalidateQueries({
queryKey: ListConvosQueryKey,
queryKey: [ListConvosQueryKeyRoot],
})
}
}
+8
View File
@@ -141,6 +141,7 @@ type SendMessage = (
message: ChatBskyConvoSendMessage.InputSchema['message'],
) => void
type FetchMessageHistory = () => Promise<void>
type MarkConvoAccepted = () => void
export type ConvoStateUninitialized = {
status: ConvoStatus.Uninitialized
@@ -153,6 +154,7 @@ export type ConvoStateUninitialized = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
markConvoAccepted: undefined
}
export type ConvoStateInitializing = {
status: ConvoStatus.Initializing
@@ -165,6 +167,7 @@ export type ConvoStateInitializing = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
markConvoAccepted: undefined
}
export type ConvoStateReady = {
status: ConvoStatus.Ready
@@ -177,6 +180,7 @@ export type ConvoStateReady = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
markConvoAccepted: MarkConvoAccepted
}
export type ConvoStateBackgrounded = {
status: ConvoStatus.Backgrounded
@@ -189,6 +193,7 @@ export type ConvoStateBackgrounded = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
markConvoAccepted: MarkConvoAccepted
}
export type ConvoStateSuspended = {
status: ConvoStatus.Suspended
@@ -201,6 +206,7 @@ export type ConvoStateSuspended = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
markConvoAccepted: MarkConvoAccepted
}
export type ConvoStateError = {
status: ConvoStatus.Error
@@ -213,6 +219,7 @@ export type ConvoStateError = {
deleteMessage: undefined
sendMessage: undefined
fetchMessageHistory: undefined
markConvoAccepted: undefined
}
export type ConvoStateDisabled = {
status: ConvoStatus.Disabled
@@ -225,6 +232,7 @@ export type ConvoStateDisabled = {
deleteMessage: DeleteMessage
sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory
markConvoAccepted: MarkConvoAccepted
}
export type ConvoState =
| ConvoStateUninitialized
+11 -7
View File
@@ -7,18 +7,22 @@ import {
ConvoStatus,
} from './types'
/**
* States where the convo is ready to be used - either ready, or backgrounded/suspended
* and ready to be resumed
*/
export type ActiveConvoStates =
| ConvoStateReady
| ConvoStateBackgrounded
| ConvoStateSuspended
| ConvoStateDisabled
/**
* Checks if a `Convo` has a `status` that is "active", meaning the chat is
* loaded and ready to be used, or its in a suspended or background state, and
* ready for resumption.
*/
export function isConvoActive(
convo: ConvoState,
): convo is
| ConvoStateReady
| ConvoStateBackgrounded
| ConvoStateSuspended
| ConvoStateDisabled {
export function isConvoActive(convo: ConvoState): convo is ActiveConvoStates {
return (
convo.status === ConvoStatus.Ready ||
convo.status === ConvoStatus.Backgrounded ||
@@ -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)
},
})
}
+28 -25
View File
@@ -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.
}
})
},
)
},
})
}
@@ -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,
})
}
@@ -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,
})
}
@@ -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],
)
}
+296 -125
View File
@@ -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) {
@@ -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,
@@ -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)
},
})
}
+2 -2
View File
@@ -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]})
},
})