From 9dd837ceabcc645f9859a24daebcdc42908b03cc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 15 May 2024 16:43:46 -0500 Subject: [PATCH] Handle block state when sending messages --- src/components/dms/MessageItem.tsx | 25 +++--- .../Conversation/MessageListError.tsx | 43 +++++----- .../Messages/Conversation/MessagesList.tsx | 2 +- src/state/messages/convo/agent.ts | 80 +++++++++++++++---- src/state/messages/convo/const.ts | 4 + src/state/messages/convo/index.tsx | 10 +++ src/state/messages/convo/types.ts | 18 ++++- 7 files changed, 131 insertions(+), 51 deletions(-) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index cafd7ca5a5..f456fa4748 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 c6e246a3fb..1e0dc34747 100644 --- a/src/screens/Messages/Conversation/MessageListError.tsx +++ b/src/screens/Messages/Conversation/MessageListError.tsx @@ -10,22 +10,21 @@ import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from '#/ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 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(() => { return { - [ConvoItemError.Network]: _( - msg`There was an issue connecting to the chat.`, + [ConvoItemError.Unknown]: _( + msg`An unknown error occurred. If the issue persists, contact support.`, ), [ConvoItemError.FirehoseFailed]: _( msg`This chat was disconnected due to a network error.`, ), [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`), + [ConvoItemError.UserBlocked]: _( + msg`The other user in this chat has blocked you.`, + ), }[item.code] }, [_, item.code]) @@ -49,24 +48,26 @@ export function MessageListError({ fill={t.palette.negative_400} style={[{top: 3}]} /> - + {message} - + {item.retry && ( + + )} ) diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 9c7774e578..bb65567a27 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -39,7 +39,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 94bb8dda44..8946a786c6 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,13 @@ import { ACTIVE_POLL_INTERVAL, BACKGROUND_POLL_INTERVAL, INACTIVE_TIMEOUT, + NETWORK_FAILURE_STATUSES, } from '#/state/messages/convo/const' import { ConvoDispatch, ConvoDispatchEvent, ConvoErrorCode, + ConvoEvent, ConvoItem, ConvoItemError, ConvoParams, @@ -82,6 +86,8 @@ export class Convo { private lastActiveTimestamp: number | undefined + private emitter = new EventEmitter<{event: [ConvoEvent]}>() + convoId: string convo: ChatBskyConvoDefs.ConvoView | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined @@ -587,7 +593,7 @@ export class Convo { logger.error('Convo: failed to fetch message history') this.headerItems.set(ConvoItemError.HistoryFailed, { - type: 'error-recoverable', + type: 'error', key: ConvoItemError.HistoryFailed, code: ConvoItemError.HistoryFailed, retry: () => { @@ -635,7 +641,7 @@ export class Convo { onFirehoseError(error?: MessagesEventBusError) { this.footerItems.set(ConvoItemError.FirehoseFailed, { - type: 'error-recoverable', + type: 'error', key: ConvoItemError.FirehoseFailed, code: ConvoItemError.FirehoseFailed, retry: () => { @@ -724,7 +730,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 @@ -740,7 +746,7 @@ export class Convo { }) this.commit() - if (!this.isProcessingPendingMessages && !this.pendingFailed) { + if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) { this.processPendingMessages() } } @@ -765,7 +771,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,13 +799,47 @@ 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: 'sync-convo-state'}) + break + default: + logger.warn( + `Convo handleSendMessageFailure could not handle error`, + { + status: e.status, + message: e.message, + }, + ) + break + } + } + } else { + logger.warn(`Convo handleSendMessageFailure received unknown error`, { + message: e.message, + }) + this.footerItems.set(ConvoItemError.Unknown, { + type: 'error', + key: ConvoItemError.Unknown, + code: ConvoItemError.Unknown, + }) + } + + this.commit() + } + async batchRetryPendingMessages() { logger.debug( `Convo: retrying ${this.pendingMessages.size} pending messages`, @@ -848,8 +887,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 +915,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 */ @@ -940,13 +986,15 @@ 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.pendingMessageFailure = null + this.commit() + this.batchRetryPendingMessages() + } + : undefined, }) }) diff --git a/src/state/messages/convo/const.ts b/src/state/messages/convo/const.ts index abea5205eb..6ce100d11e 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 e955d41183..1e8cef2e88 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -78,6 +78,16 @@ export function ConvoProvider({ }, [convo, convoId, markAsRead]), ) + React.useEffect(() => { + return convo.on(event => { + switch (event.type) { + case 'sync-convo-state': { + console.log('SYNC') + } + } + }) + }, [convo]) + 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 3fb0eb6ad3..d7490acbe6 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -26,7 +26,7 @@ export enum ConvoItemError { /** * Generic error */ - Network = 'network', + Unknown = 'unknown', /** * Error connecting to event firehose */ @@ -35,6 +35,10 @@ export enum ConvoItemError { * Error fetching past messages */ HistoryFailed = 'historyFailed', + /** + * Recipient is blocking the user + */ + UserBlocked = 'userBlocked', } export enum ConvoErrorCode { @@ -95,6 +99,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 +115,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 @@ -201,3 +209,7 @@ export type ConvoState = | ConvoStateBackgrounded | ConvoStateSuspended | ConvoStateError + +export type ConvoEvent = { + type: 'sync-convo-state' +}