diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index cafd7ca5..f456fa47 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -202,7 +202,7 @@ let MessageItemMetadata = ({
)}
- {item.type === 'pending-message' && item.retry && (
+ {item.type === 'pending-message' && item.failed && (
<>
{' '}
·{' '}
@@ -214,15 +214,20 @@ let MessageItemMetadata = ({
},
]}>
{_(msg`Failed to send`)}
- {' '}
- ·{' '}
-
- {_(msg`Retry`)}
-
+
+ {item.retry && (
+ <>
+ {' '}
+ ·{' '}
+
+ {_(msg`Retry`)}
+
+ >
+ )}
>
)}
diff --git a/src/screens/Messages/Conversation/MessageListError.tsx b/src/screens/Messages/Conversation/MessageListError.tsx
index c6e246a3..6a6ce5e6 100644
--- a/src/screens/Messages/Conversation/MessageListError.tsx
+++ b/src/screens/Messages/Conversation/MessageListError.tsx
@@ -5,27 +5,25 @@ import {useLingui} from '@lingui/react'
import {ConvoItem, ConvoItemError} from '#/state/messages/convo/types'
import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from '#/components/icons/ArrowRotateCounterClockwise'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
-export function MessageListError({
- item,
-}: {
- item: ConvoItem & {type: 'error-recoverable'}
-}) {
+export function MessageListError({item}: {item: ConvoItem & {type: 'error'}}) {
const t = useTheme()
const {_} = useLingui()
- const message = React.useMemo(() => {
+ const {description, help, cta} = React.useMemo(() => {
return {
- [ConvoItemError.Network]: _(
- msg`There was an issue connecting to the chat.`,
- ),
- [ConvoItemError.FirehoseFailed]: _(
- msg`This chat was disconnected due to a network error.`,
- ),
- [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
+ [ConvoItemError.FirehoseFailed]: {
+ description: _(msg`This chat was disconnected`),
+ help: _(msg`Press to attempt reconnection`),
+ cta: _(msg`Reconnect`),
+ },
+ [ConvoItemError.HistoryFailed]: {
+ description: _(msg`Failed to load past messages`),
+ help: _(msg`Press to retry`),
+ cta: _(msg`Retry`),
+ },
}[item.code]
}, [_, item.code])
@@ -36,37 +34,31 @@ export function MessageListError({
a.flex_row,
a.align_center,
a.justify_between,
- a.gap_lg,
- a.py_md,
- a.px_lg,
- a.rounded_md,
- t.atoms.bg_contrast_25,
+ a.gap_sm,
+ a.pb_lg,
{maxWidth: 400},
]}>
-
-
-
- {message}
-
-
+
-
+
+ {description} ·{' '}
+ {item.retry && (
+ {
+ e.preventDefault()
+ item.retry?.()
+ return false
+ }}>
+ {cta}
+
+ )}
+
)
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index a8f9d344..fd9368b4 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -46,7 +46,7 @@ function renderItem({item}: {item: ConvoItem}) {
return
} else if (item.type === 'deleted-message') {
return Deleted message
- } else if (item.type === 'error-recoverable') {
+ } else if (item.type === 'error') {
return
}
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index 94bb8dda..8673c70a 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -5,6 +5,8 @@ import {
ChatBskyConvoGetLog,
ChatBskyConvoSendMessage,
} from '@atproto/api'
+import {XRPCError} from '@atproto/xrpc'
+import EventEmitter from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry'
@@ -14,11 +16,14 @@ import {
ACTIVE_POLL_INTERVAL,
BACKGROUND_POLL_INTERVAL,
INACTIVE_TIMEOUT,
+ NETWORK_FAILURE_STATUSES,
} from '#/state/messages/convo/const'
import {
ConvoDispatch,
ConvoDispatchEvent,
+ ConvoError,
ConvoErrorCode,
+ ConvoEvent,
ConvoItem,
ConvoItemError,
ConvoParams,
@@ -51,13 +56,7 @@ export class Convo {
private senderUserDid: string
private status: ConvoStatus = ConvoStatus.Uninitialized
- private error:
- | {
- code: ConvoErrorCode
- exception?: Error
- retry: () => void
- }
- | undefined
+ private error: ConvoError | undefined
private oldestRev: string | undefined | null = undefined
private isFetchingHistory = false
private latestRev: string | undefined = undefined
@@ -75,13 +74,13 @@ export class Convo {
{id: string; message: ChatBskyConvoSendMessage.InputSchema['message']}
> = new Map()
private deletedMessages: Set = new Set()
- private footerItems: Map = new Map()
- private headerItems: Map = new Map()
private isProcessingPendingMessages = false
private lastActiveTimestamp: number | undefined
+ private emitter = new EventEmitter<{event: [ConvoEvent]}>()
+
convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined
@@ -174,7 +173,7 @@ export class Convo {
status: ConvoStatus.Error,
items: [],
convo: undefined,
- error: this.error,
+ error: this.error!,
sender: undefined,
recipients: undefined,
isFetchingHistory: false,
@@ -282,6 +281,7 @@ export class Convo {
if (this.convo) {
this.status = ConvoStatus.Ready
this.refreshConvo()
+ this.maybeRecoverFromNetworkError()
} else {
this.status = ConvoStatus.Initializing
this.setup()
@@ -379,12 +379,30 @@ export class Convo {
this.newMessages = new Map()
this.pendingMessages = new Map()
this.deletedMessages = new Set()
- this.footerItems = new Map()
- this.headerItems = new Map()
+
+ this.pendingMessageFailure = null
+ this.fetchMessageHistoryError = undefined
+ this.firehoseError = undefined
this.dispatch({event: ConvoDispatchEvent.Init})
}
+ maybeRecoverFromNetworkError() {
+ if (this.firehoseError) {
+ this.firehoseError.retry()
+ this.firehoseError = undefined
+ this.commit()
+ } else {
+ this.batchRetryPendingMessages()
+ }
+
+ if (this.fetchMessageHistoryError) {
+ this.fetchMessageHistoryError.retry()
+ this.fetchMessageHistoryError = undefined
+ this.commit()
+ }
+ }
+
private async setup() {
try {
const {convo, sender, recipients} = await this.fetchConvo()
@@ -520,6 +538,11 @@ export class Convo {
}
}
+ private fetchMessageHistoryError:
+ | {
+ retry: () => void
+ }
+ | undefined
async fetchMessageHistory() {
logger.debug('Convo: fetch message history', {}, logger.DebugContext.convo)
@@ -537,7 +560,7 @@ export class Convo {
* If we've rendered a retry state for history fetching, exit. Upon retry,
* this will be removed and we'll try again.
*/
- if (this.headerItems.has(ConvoItemError.HistoryFailed)) return
+ if (this.fetchMessageHistoryError) return
try {
this.isFetchingHistory = true
@@ -586,15 +609,11 @@ export class Convo {
} catch (e: any) {
logger.error('Convo: failed to fetch message history')
- this.headerItems.set(ConvoItemError.HistoryFailed, {
- type: 'error-recoverable',
- key: ConvoItemError.HistoryFailed,
- code: ConvoItemError.HistoryFailed,
+ this.fetchMessageHistoryError = {
retry: () => {
- this.headerItems.delete(ConvoItemError.HistoryFailed)
this.fetchMessageHistory()
},
- })
+ }
} finally {
this.isFetchingHistory = false
this.commit()
@@ -628,22 +647,16 @@ export class Convo {
)
}
+ private firehoseError: MessagesEventBusError | undefined
+
onFirehoseConnect() {
- this.footerItems.delete(ConvoItemError.FirehoseFailed)
+ this.firehoseError = undefined
+ this.batchRetryPendingMessages()
this.commit()
}
onFirehoseError(error?: MessagesEventBusError) {
- this.footerItems.set(ConvoItemError.FirehoseFailed, {
- type: 'error-recoverable',
- key: ConvoItemError.FirehoseFailed,
- code: ConvoItemError.FirehoseFailed,
- retry: () => {
- this.footerItems.delete(ConvoItemError.FirehoseFailed)
- this.commit()
- error?.retry()
- },
- })
+ this.firehoseError = error
this.commit()
}
@@ -724,7 +737,7 @@ export class Convo {
}
}
- private pendingFailed = false
+ private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
// Ignore empty messages for now since they have no other purpose atm
@@ -734,13 +747,14 @@ export class Convo {
const tempId = nanoid()
+ this.pendingMessageFailure = null
this.pendingMessages.set(tempId, {
id: tempId,
message,
})
this.commit()
- if (!this.isProcessingPendingMessages && !this.pendingFailed) {
+ if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) {
this.processPendingMessages()
}
}
@@ -765,7 +779,6 @@ export class Convo {
try {
this.isProcessingPendingMessages = true
- // throw new Error('UNCOMMENT TO TEST RETRY')
const {id, message} = pendingMessage
const response = await networkRetry(2, () => {
@@ -794,23 +807,65 @@ export class Convo {
this.commit()
} catch (e: any) {
logger.error(e, {context: `Convo: failed to send message`})
- this.pendingFailed = true
- this.commit()
+ this.handleSendMessageFailure(e)
} finally {
this.isProcessingPendingMessages = false
}
}
+ private handleSendMessageFailure(e: any) {
+ if (e instanceof XRPCError) {
+ if (NETWORK_FAILURE_STATUSES.includes(e.status)) {
+ this.pendingMessageFailure = 'recoverable'
+ } else {
+ switch (e.message) {
+ case 'block between recipient and sender':
+ this.pendingMessageFailure = 'unrecoverable'
+ this.emitter.emit('event', {
+ type: 'invalidate-block-state',
+ accountDids: [
+ this.sender!.did,
+ ...this.recipients!.map(r => r.did),
+ ],
+ })
+ break
+ default:
+ logger.warn(
+ `Convo handleSendMessageFailure could not handle error`,
+ {
+ status: e.status,
+ message: e.message,
+ },
+ )
+ break
+ }
+ }
+ } else {
+ logger.error(e, {
+ context: `Convo handleSendMessageFailure received unknown error`,
+ })
+ }
+
+ this.commit()
+ }
+
async batchRetryPendingMessages() {
+ if (this.pendingMessageFailure === null) return
+
+ const messageArray = Array.from(this.pendingMessages.values())
+ if (messageArray.length === 0) return
+
+ this.pendingMessageFailure = null
+ this.commit()
+
logger.debug(
- `Convo: retrying ${this.pendingMessages.size} pending messages`,
+ `Convo: batch retrying ${this.pendingMessages.size} pending messages`,
{},
logger.DebugContext.convo,
)
try {
// throw new Error('UNCOMMENT TO TEST RETRY')
- const messageArray = Array.from(this.pendingMessages.values())
const {data} = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.sendMessageBatch(
{
@@ -848,8 +903,7 @@ export class Convo {
)
} catch (e: any) {
logger.error(e, {context: `Convo: failed to batch retry messages`})
- this.pendingFailed = true
- this.commit()
+ this.handleSendMessageFailure(e)
}
}
@@ -877,6 +931,14 @@ export class Convo {
}
}
+ on(handler: (event: ConvoEvent) => void) {
+ this.emitter.on('event', handler)
+
+ return () => {
+ this.emitter.off('event', handler)
+ }
+ }
+
/*
* Items in reverse order, since FlatList inverts
*/
@@ -901,9 +963,16 @@ export class Convo {
}
})
- this.headerItems.forEach(item => {
- items.unshift(item)
- })
+ if (this.fetchMessageHistoryError) {
+ items.unshift({
+ type: 'error',
+ code: ConvoItemError.HistoryFailed,
+ key: ConvoItemError.HistoryFailed,
+ retry: () => {
+ this.maybeRecoverFromNetworkError()
+ },
+ })
+ }
this.newMessages.forEach(m => {
if (ChatBskyConvoDefs.isMessageView(m)) {
@@ -940,19 +1009,26 @@ export class Convo {
sender: this.sender!,
},
nextMessage: null,
- retry: this.pendingFailed
- ? () => {
- this.pendingFailed = false
- this.commit()
- this.batchRetryPendingMessages()
- }
- : undefined,
+ failed: this.pendingMessageFailure !== null,
+ retry:
+ this.pendingMessageFailure === 'recoverable'
+ ? () => {
+ this.maybeRecoverFromNetworkError()
+ }
+ : undefined,
})
})
- this.footerItems.forEach(item => {
- items.push(item)
- })
+ if (this.firehoseError) {
+ items.push({
+ type: 'error',
+ code: ConvoItemError.FirehoseFailed,
+ key: ConvoItemError.FirehoseFailed,
+ retry: () => {
+ this.firehoseError?.retry()
+ },
+ })
+ }
return items
.filter(item => {
diff --git a/src/state/messages/convo/const.ts b/src/state/messages/convo/const.ts
index abea5205..6ce100d1 100644
--- a/src/state/messages/convo/const.ts
+++ b/src/state/messages/convo/const.ts
@@ -1,3 +1,7 @@
export const ACTIVE_POLL_INTERVAL = 1e3
export const BACKGROUND_POLL_INTERVAL = 5e3
export const INACTIVE_TIMEOUT = 60e3 * 5
+
+export const NETWORK_FAILURE_STATUSES = [
+ 1, 408, 425, 429, 500, 502, 503, 504, 522, 524,
+]
diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx
index e955d411..d6648f48 100644
--- a/src/state/messages/convo/index.tsx
+++ b/src/state/messages/convo/index.tsx
@@ -1,6 +1,7 @@
import React, {useContext, useState, useSyncExternalStore} from 'react'
import {AppState} from 'react-native'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
import {Convo} from '#/state/messages/convo/agent'
import {
@@ -13,6 +14,8 @@ import {
import {isConvoActive} from '#/state/messages/convo/util'
import {useMessagesEventBus} from '#/state/messages/events'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
+import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-converations'
+import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
export * from '#/state/messages/convo/util'
@@ -52,6 +55,7 @@ export function ConvoProvider({
children,
convoId,
}: Pick & {children: React.ReactNode}) {
+ const queryClient = useQueryClient()
const isScreenFocused = useIsFocused()
const {getAgent} = useAgent()
const events = useMessagesEventBus()
@@ -78,6 +82,23 @@ export function ConvoProvider({
}, [convo, convoId, markAsRead]),
)
+ React.useEffect(() => {
+ return convo.on(event => {
+ switch (event.type) {
+ case 'invalidate-block-state': {
+ for (const did of event.accountDids) {
+ queryClient.invalidateQueries({
+ queryKey: createProfileQueryKey(did),
+ })
+ }
+ queryClient.invalidateQueries({
+ queryKey: ListConvosQueryKey,
+ })
+ }
+ }
+ })
+ }, [convo, queryClient])
+
React.useEffect(() => {
const handleAppStateChange = (nextAppState: string) => {
if (isScreenFocused) {
diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts
index 3fb0eb6a..25e79aba 100644
--- a/src/state/messages/convo/types.ts
+++ b/src/state/messages/convo/types.ts
@@ -23,10 +23,6 @@ export enum ConvoStatus {
}
export enum ConvoItemError {
- /**
- * Generic error
- */
- Network = 'network',
/**
* Error connecting to event firehose
*/
@@ -95,6 +91,7 @@ export type ConvoItem =
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
+ failed: boolean
/**
* Retry sending the message. If present, the message is in a failed state.
*/
@@ -110,10 +107,13 @@ export type ConvoItem =
| null
}
| {
- type: 'error-recoverable'
+ type: 'error'
key: string
code: ConvoItemError
- retry: () => void
+ /**
+ * If present, error is recoverable.
+ */
+ retry?: () => void
}
type DeleteMessage = (messageId: string) => Promise
@@ -186,7 +186,7 @@ export type ConvoStateError = {
status: ConvoStatus.Error
items: []
convo: undefined
- error: any
+ error: ConvoError
sender: undefined
recipients: undefined
isFetchingHistory: false
@@ -201,3 +201,8 @@ export type ConvoState =
| ConvoStateBackgrounded
| ConvoStateSuspended
| ConvoStateError
+
+export type ConvoEvent = {
+ type: 'invalidate-block-state'
+ accountDids: string[]
+}