Handle block state when sending messages

This commit is contained in:
Eric Bailey
2024-05-15 16:43:46 -05:00
parent da2bdf5d6f
commit 9dd837ceab
7 changed files with 131 additions and 51 deletions
+15 -10
View File
@@ -202,7 +202,7 @@ let MessageItemMetadata = ({
)} )}
</TimeElapsed> </TimeElapsed>
{item.type === 'pending-message' && item.retry && ( {item.type === 'pending-message' && item.failed && (
<> <>
{' '} {' '}
&middot;{' '} &middot;{' '}
@@ -214,15 +214,20 @@ let MessageItemMetadata = ({
}, },
]}> ]}>
{_(msg`Failed to send`)} {_(msg`Failed to send`)}
</Text>{' '} </Text>
&middot;{' '} {item.retry && (
<InlineLinkText <>
label={_(msg`Click to retry failed message`)} {' '}
to="#" &middot;{' '}
onPress={handleRetry} <InlineLinkText
style={[a.text_xs]}> label={_(msg`Click to retry failed message`)}
{_(msg`Retry`)} to="#"
</InlineLinkText> onPress={handleRetry}
style={[a.text_xs]}>
{_(msg`Retry`)}
</InlineLinkText>
</>
)}
</> </>
)} )}
</Text> </Text>
@@ -10,22 +10,21 @@ import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from '#/
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function MessageListError({ export function MessageListError({item}: {item: ConvoItem & {type: 'error'}}) {
item,
}: {
item: ConvoItem & {type: 'error-recoverable'}
}) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const message = React.useMemo(() => { const message = React.useMemo(() => {
return { return {
[ConvoItemError.Network]: _( [ConvoItemError.Unknown]: _(
msg`There was an issue connecting to the chat.`, msg`An unknown error occurred. If the issue persists, contact support.`,
), ),
[ConvoItemError.FirehoseFailed]: _( [ConvoItemError.FirehoseFailed]: _(
msg`This chat was disconnected due to a network error.`, msg`This chat was disconnected due to a network error.`,
), ),
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`), [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
[ConvoItemError.UserBlocked]: _(
msg`The other user in this chat has blocked you.`,
),
}[item.code] }[item.code]
}, [_, item.code]) }, [_, item.code])
@@ -49,24 +48,26 @@ export function MessageListError({
fill={t.palette.negative_400} fill={t.palette.negative_400}
style={[{top: 3}]} style={[{top: 3}]}
/> />
<View style={[a.flex_1, {maxWidth: 200}]}> <View style={[a.flex_1, {maxWidth: 240}]}>
<Text style={[a.leading_snug]}>{message}</Text> <Text style={[a.leading_snug]}>{message}</Text>
</View> </View>
</View> </View>
<Button {item.retry && (
label={_(msg`Press to retry`)} <Button
size="small" label={_(msg`Press to retry`)}
variant="ghost" size="small"
color="secondary" variant="ghost"
onPress={e => { color="secondary"
e.preventDefault() onPress={e => {
item.retry() e.preventDefault()
return false item.retry?.()
}}> return false
<ButtonText>{_(msg`Retry`)}</ButtonText> }}>
<ButtonIcon icon={Refresh} position="right" /> <ButtonText>{_(msg`Retry`)}</ButtonText>
</Button> <ButtonIcon icon={Refresh} position="right" />
</Button>
)}
</View> </View>
</View> </View>
) )
@@ -39,7 +39,7 @@ function renderItem({item}: {item: ConvoItem}) {
return <MessageItem item={item} /> return <MessageItem item={item} />
} else if (item.type === 'deleted-message') { } else if (item.type === 'deleted-message') {
return <Text>Deleted message</Text> return <Text>Deleted message</Text>
} else if (item.type === 'error-recoverable') { } else if (item.type === 'error') {
return <MessageListError item={item} /> return <MessageListError item={item} />
} }
+64 -16
View File
@@ -5,6 +5,8 @@ import {
ChatBskyConvoGetLog, ChatBskyConvoGetLog,
ChatBskyConvoSendMessage, ChatBskyConvoSendMessage,
} from '@atproto/api' } from '@atproto/api'
import {XRPCError} from '@atproto/xrpc'
import EventEmitter from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
@@ -14,11 +16,13 @@ import {
ACTIVE_POLL_INTERVAL, ACTIVE_POLL_INTERVAL,
BACKGROUND_POLL_INTERVAL, BACKGROUND_POLL_INTERVAL,
INACTIVE_TIMEOUT, INACTIVE_TIMEOUT,
NETWORK_FAILURE_STATUSES,
} from '#/state/messages/convo/const' } from '#/state/messages/convo/const'
import { import {
ConvoDispatch, ConvoDispatch,
ConvoDispatchEvent, ConvoDispatchEvent,
ConvoErrorCode, ConvoErrorCode,
ConvoEvent,
ConvoItem, ConvoItem,
ConvoItemError, ConvoItemError,
ConvoParams, ConvoParams,
@@ -82,6 +86,8 @@ export class Convo {
private lastActiveTimestamp: number | undefined private lastActiveTimestamp: number | undefined
private emitter = new EventEmitter<{event: [ConvoEvent]}>()
convoId: string convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
@@ -587,7 +593,7 @@ export class Convo {
logger.error('Convo: failed to fetch message history') logger.error('Convo: failed to fetch message history')
this.headerItems.set(ConvoItemError.HistoryFailed, { this.headerItems.set(ConvoItemError.HistoryFailed, {
type: 'error-recoverable', type: 'error',
key: ConvoItemError.HistoryFailed, key: ConvoItemError.HistoryFailed,
code: ConvoItemError.HistoryFailed, code: ConvoItemError.HistoryFailed,
retry: () => { retry: () => {
@@ -635,7 +641,7 @@ export class Convo {
onFirehoseError(error?: MessagesEventBusError) { onFirehoseError(error?: MessagesEventBusError) {
this.footerItems.set(ConvoItemError.FirehoseFailed, { this.footerItems.set(ConvoItemError.FirehoseFailed, {
type: 'error-recoverable', type: 'error',
key: ConvoItemError.FirehoseFailed, key: ConvoItemError.FirehoseFailed,
code: ConvoItemError.FirehoseFailed, code: ConvoItemError.FirehoseFailed,
retry: () => { retry: () => {
@@ -724,7 +730,7 @@ export class Convo {
} }
} }
private pendingFailed = false private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
// Ignore empty messages for now since they have no other purpose atm // Ignore empty messages for now since they have no other purpose atm
@@ -740,7 +746,7 @@ export class Convo {
}) })
this.commit() this.commit()
if (!this.isProcessingPendingMessages && !this.pendingFailed) { if (!this.isProcessingPendingMessages && !this.pendingMessageFailure) {
this.processPendingMessages() this.processPendingMessages()
} }
} }
@@ -765,7 +771,6 @@ export class Convo {
try { try {
this.isProcessingPendingMessages = true this.isProcessingPendingMessages = true
// throw new Error('UNCOMMENT TO TEST RETRY')
const {id, message} = pendingMessage const {id, message} = pendingMessage
const response = await networkRetry(2, () => { const response = await networkRetry(2, () => {
@@ -794,13 +799,47 @@ export class Convo {
this.commit() this.commit()
} catch (e: any) { } catch (e: any) {
logger.error(e, {context: `Convo: failed to send message`}) logger.error(e, {context: `Convo: failed to send message`})
this.pendingFailed = true this.handleSendMessageFailure(e)
this.commit()
} finally { } finally {
this.isProcessingPendingMessages = false 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() { async batchRetryPendingMessages() {
logger.debug( logger.debug(
`Convo: retrying ${this.pendingMessages.size} pending messages`, `Convo: retrying ${this.pendingMessages.size} pending messages`,
@@ -848,8 +887,7 @@ export class Convo {
) )
} catch (e: any) { } catch (e: any) {
logger.error(e, {context: `Convo: failed to batch retry messages`}) logger.error(e, {context: `Convo: failed to batch retry messages`})
this.pendingFailed = true this.handleSendMessageFailure(e)
this.commit()
} }
} }
@@ -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 * Items in reverse order, since FlatList inverts
*/ */
@@ -940,13 +986,15 @@ export class Convo {
sender: this.sender!, sender: this.sender!,
}, },
nextMessage: null, nextMessage: null,
retry: this.pendingFailed failed: this.pendingMessageFailure !== null,
? () => { retry:
this.pendingFailed = false this.pendingMessageFailure === 'recoverable'
this.commit() ? () => {
this.batchRetryPendingMessages() this.pendingMessageFailure = null
} this.commit()
: undefined, this.batchRetryPendingMessages()
}
: undefined,
}) })
}) })
+4
View File
@@ -1,3 +1,7 @@
export const ACTIVE_POLL_INTERVAL = 1e3 export const ACTIVE_POLL_INTERVAL = 1e3
export const BACKGROUND_POLL_INTERVAL = 5e3 export const BACKGROUND_POLL_INTERVAL = 5e3
export const INACTIVE_TIMEOUT = 60e3 * 5 export const INACTIVE_TIMEOUT = 60e3 * 5
export const NETWORK_FAILURE_STATUSES = [
1, 408, 425, 429, 500, 502, 503, 504, 522, 524,
]
+10
View File
@@ -78,6 +78,16 @@ export function ConvoProvider({
}, [convo, convoId, markAsRead]), }, [convo, convoId, markAsRead]),
) )
React.useEffect(() => {
return convo.on(event => {
switch (event.type) {
case 'sync-convo-state': {
console.log('SYNC')
}
}
})
}, [convo])
React.useEffect(() => { React.useEffect(() => {
const handleAppStateChange = (nextAppState: string) => { const handleAppStateChange = (nextAppState: string) => {
if (isScreenFocused) { if (isScreenFocused) {
+15 -3
View File
@@ -26,7 +26,7 @@ export enum ConvoItemError {
/** /**
* Generic error * Generic error
*/ */
Network = 'network', Unknown = 'unknown',
/** /**
* Error connecting to event firehose * Error connecting to event firehose
*/ */
@@ -35,6 +35,10 @@ export enum ConvoItemError {
* Error fetching past messages * Error fetching past messages
*/ */
HistoryFailed = 'historyFailed', HistoryFailed = 'historyFailed',
/**
* Recipient is blocking the user
*/
UserBlocked = 'userBlocked',
} }
export enum ConvoErrorCode { export enum ConvoErrorCode {
@@ -95,6 +99,7 @@ export type ConvoItem =
| ChatBskyConvoDefs.MessageView | ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView | ChatBskyConvoDefs.DeletedMessageView
| null | null
failed: boolean
/** /**
* Retry sending the message. If present, the message is in a failed state. * Retry sending the message. If present, the message is in a failed state.
*/ */
@@ -110,10 +115,13 @@ export type ConvoItem =
| null | null
} }
| { | {
type: 'error-recoverable' type: 'error'
key: string key: string
code: ConvoItemError code: ConvoItemError
retry: () => void /**
* If present, error is recoverable.
*/
retry?: () => void
} }
type DeleteMessage = (messageId: string) => Promise<void> type DeleteMessage = (messageId: string) => Promise<void>
@@ -201,3 +209,7 @@ export type ConvoState =
| ConvoStateBackgrounded | ConvoStateBackgrounded
| ConvoStateSuspended | ConvoStateSuspended
| ConvoStateError | ConvoStateError
export type ConvoEvent = {
type: 'sync-convo-state'
}