[Clipclops] All my clops gone (#3850)

* Handle two common errors, provide more clarity around error states

* Handle failed polling

* Remove unused error type

* format
zio/stable
Eric Bailey 2024-05-06 15:35:05 -05:00 committed by GitHub
parent 2a1dbd2756
commit 0b6ace990e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 201 additions and 107 deletions

View File

@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ConvoError, ConvoItem} from '#/state/messages/convo' import {ConvoItem, ConvoItemError} from '#/state/messages/convo'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
@ -18,7 +18,13 @@ export function MessageListError({
const {_} = useLingui() const {_} = useLingui()
const message = React.useMemo(() => { const message = React.useMemo(() => {
return { return {
[ConvoError.HistoryFailed]: _(msg`Failed to load past messages.`), [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
[ConvoItemError.ResumeFailed]: _(
msg`There was an issue connecting to the chat.`,
),
[ConvoItemError.PollFailed]: _(
msg`This chat was disconnected due to a network error.`,
),
}[item.code] }[item.code]
}, [_, item.code]) }, [_, item.code])

View File

@ -229,7 +229,7 @@ export function MessagesList() {
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
<List <List
ref={flatListRef} ref={flatListRef}
data={chat.status === ConvoStatus.Ready ? chat.items : undefined} data={chat.items}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
disableVirtualization={true} disableVirtualization={true}
@ -248,11 +248,7 @@ export function MessagesList() {
onScrollToIndexFailed={onScrollToIndexFailed} onScrollToIndexFailed={onScrollToIndexFailed}
scrollEventThrottle={100} scrollEventThrottle={100}
ListHeaderComponent={ ListHeaderComponent={
<MaybeLoader <MaybeLoader isLoading={chat.isFetchingHistory} />
isLoading={
chat.status === ConvoStatus.Ready && chat.isFetchingHistory
}
/>
} }
/> />
</ScrollProvider> </ScrollProvider>

View File

@ -14,7 +14,6 @@ import {BACK_HITSLOP} from 'lib/constants'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {ChatProvider, useChat} from 'state/messages' import {ChatProvider, useChat} from 'state/messages'
import {ConvoStatus} from 'state/messages/convo' import {ConvoStatus} from 'state/messages/convo'
import {useSession} from 'state/session'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList' import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
@ -43,28 +42,27 @@ export function MessagesConversationScreen({route}: Props) {
function Inner() { function Inner() {
const chat = useChat() const chat = useChat()
const {currentAccount} = useSession()
const myDid = currentAccount?.did
const otherProfile = React.useMemo(() => { if (
if (chat.status !== ConvoStatus.Ready) return chat.status === ConvoStatus.Uninitialized ||
return chat.convo.members.find(m => m.did !== myDid) chat.status === ConvoStatus.Initializing
}, [chat, myDid]) ) {
return <ListMaybePlaceholder isLoading />
// TODO whenever we have error messages, we should use them in here -hailey
if (chat.status !== ConvoStatus.Ready || !otherProfile) {
return (
<ListMaybePlaceholder
isLoading={true}
isError={chat.status === ConvoStatus.Error}
/>
)
} }
if (chat.status === ConvoStatus.Error) {
// TODO error
return null
}
/*
* Any other chat states (atm) are "ready" states
*/
return ( return (
<KeyboardProvider> <KeyboardProvider>
<CenteredView style={{flex: 1}} sideBorders> <CenteredView style={{flex: 1}} sideBorders>
<Header profile={otherProfile} /> <Header profile={chat.recipients[0]} />
<MessagesList /> <MessagesList />
</CenteredView> </CenteredView>
</KeyboardProvider> </KeyboardProvider>

View File

@ -1,15 +1,19 @@
import {describe, it} from '@jest/globals' import {describe, it} from '@jest/globals'
describe(`#/state/messages/convo`, () => { describe(`#/state/messages/convo`, () => {
describe(`status states`, () => { describe(`init`, () => {
it.todo(`fails if sender and recipients aren't found`)
it.todo(`cannot re-initialize from a non-unintialized state`) it.todo(`cannot re-initialize from a non-unintialized state`)
it.todo(`can re-initialize from a failed state`) it.todo(`can re-initialize from a failed state`)
describe(`destroy`, () => {
it.todo(`cannot be interacted with when destroyed`)
it.todo(`polling is stopped when destroyed`)
it.todo(`events are cleaned up when destroyed`)
}) })
describe(`resume`, () => {
it.todo(`restores previous state if resume fails`)
})
describe(`suspend`, () => {
it.todo(`cannot be interacted with when suspended`)
it.todo(`polling is stopped when suspended`)
}) })
describe(`read states`, () => { describe(`read states`, () => {

View File

@ -25,8 +25,14 @@ export enum ConvoStatus {
Suspended = 'suspended', Suspended = 'suspended',
} }
export enum ConvoError { export enum ConvoItemError {
HistoryFailed = 'historyFailed', HistoryFailed = 'historyFailed',
ResumeFailed = 'resumeFailed',
PollFailed = 'pollFailed',
}
export enum ConvoError {
InitFailed = 'initFailed',
} }
export type ConvoItem = export type ConvoItem =
@ -56,14 +62,9 @@ export type ConvoItem =
| { | {
type: 'error-recoverable' type: 'error-recoverable'
key: string key: string
code: ConvoError code: ConvoItemError
retry: () => void retry: () => void
} }
| {
type: 'error-fatal'
code: ConvoError
key: string
}
export type ConvoState = export type ConvoState =
| { | {
@ -71,6 +72,8 @@ export type ConvoState =
items: [] items: []
convo: undefined convo: undefined
error: undefined error: undefined
sender: undefined
recipients: undefined
isFetchingHistory: false isFetchingHistory: false
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
@ -81,6 +84,8 @@ export type ConvoState =
items: [] items: []
convo: undefined convo: undefined
error: undefined error: undefined
sender: undefined
recipients: undefined
isFetchingHistory: boolean isFetchingHistory: boolean
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
@ -91,6 +96,8 @@ export type ConvoState =
items: ConvoItem[] items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
error: undefined error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean isFetchingHistory: boolean
deleteMessage: (messageId: string) => Promise<void> deleteMessage: (messageId: string) => Promise<void>
sendMessage: ( sendMessage: (
@ -103,6 +110,8 @@ export type ConvoState =
items: ConvoItem[] items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
error: undefined error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean isFetchingHistory: boolean
deleteMessage: (messageId: string) => Promise<void> deleteMessage: (messageId: string) => Promise<void>
sendMessage: ( sendMessage: (
@ -115,6 +124,8 @@ export type ConvoState =
items: ConvoItem[] items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
error: undefined error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean isFetchingHistory: boolean
deleteMessage: (messageId: string) => Promise<void> deleteMessage: (messageId: string) => Promise<void>
sendMessage: ( sendMessage: (
@ -127,6 +138,8 @@ export type ConvoState =
items: ConvoItem[] items: ConvoItem[]
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
error: undefined error: undefined
sender: AppBskyActorDefs.ProfileViewBasic
recipients: AppBskyActorDefs.ProfileViewBasic[]
isFetchingHistory: boolean isFetchingHistory: boolean
deleteMessage: (messageId: string) => Promise<void> deleteMessage: (messageId: string) => Promise<void>
sendMessage: ( sendMessage: (
@ -139,6 +152,8 @@ export type ConvoState =
items: [] items: []
convo: undefined convo: undefined
error: any error: any
sender: undefined
recipients: undefined
isFetchingHistory: false isFetchingHistory: false
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
@ -165,10 +180,17 @@ export class Convo {
private pollInterval = ACTIVE_POLL_INTERVAL private pollInterval = ACTIVE_POLL_INTERVAL
private status: ConvoStatus = ConvoStatus.Uninitialized private status: ConvoStatus = ConvoStatus.Uninitialized
private error: any private error:
| {
code: ConvoError
exception?: Error
retry: () => void
}
| undefined
private historyCursor: string | undefined | null = undefined private historyCursor: string | undefined | null = undefined
private isFetchingHistory = false private isFetchingHistory = false
private eventsCursor: string | undefined = undefined private eventsCursor: string | undefined = undefined
private pollingFailure = false
private pastMessages: Map< private pastMessages: Map<
string, string,
@ -192,6 +214,7 @@ export class Convo {
convoId: string convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined
snapshot: ConvoState | undefined snapshot: ConvoState | undefined
constructor(params: ConvoParams) { constructor(params: ConvoParams) {
@ -226,7 +249,7 @@ export class Convo {
getSnapshot(): ConvoState { getSnapshot(): ConvoState {
if (!this.snapshot) this.snapshot = this.generateSnapshot() if (!this.snapshot) this.snapshot = this.generateSnapshot()
logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo) // logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo)
return this.snapshot return this.snapshot
} }
@ -238,6 +261,8 @@ export class Convo {
items: [], items: [],
convo: undefined, convo: undefined,
error: undefined, error: undefined,
sender: undefined,
recipients: undefined,
isFetchingHistory: this.isFetchingHistory, isFetchingHistory: this.isFetchingHistory,
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
@ -253,6 +278,8 @@ export class Convo {
items: this.getItems(), items: this.getItems(),
convo: this.convo!, convo: this.convo!,
error: undefined, error: undefined,
sender: this.sender!,
recipients: this.recipients!,
isFetchingHistory: this.isFetchingHistory, isFetchingHistory: this.isFetchingHistory,
deleteMessage: this.deleteMessage, deleteMessage: this.deleteMessage,
sendMessage: this.sendMessage, sendMessage: this.sendMessage,
@ -265,6 +292,8 @@ export class Convo {
items: [], items: [],
convo: undefined, convo: undefined,
error: this.error, error: this.error,
sender: undefined,
recipients: undefined,
isFetchingHistory: false, isFetchingHistory: false,
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
@ -277,6 +306,8 @@ export class Convo {
items: [], items: [],
convo: undefined, convo: undefined,
error: undefined, error: undefined,
sender: undefined,
recipients: undefined,
isFetchingHistory: false, isFetchingHistory: false,
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
@ -289,7 +320,10 @@ export class Convo {
async init() { async init() {
logger.debug('Convo: init', {}, logger.DebugContext.convo) logger.debug('Convo: init', {}, logger.DebugContext.convo)
if (this.status === ConvoStatus.Uninitialized) { if (
this.status === ConvoStatus.Uninitialized ||
this.status === ConvoStatus.Error
) {
try { try {
this.status = ConvoStatus.Initializing this.status = ConvoStatus.Initializing
this.commit() this.commit()
@ -301,8 +335,16 @@ export class Convo {
await this.fetchMessageHistory() await this.fetchMessageHistory()
this.pollEvents() this.pollEvents()
} catch (e) { } catch (e: any) {
this.error = e logger.error('Convo: failed to init')
this.error = {
exception: e,
code: ConvoError.InitFailed,
retry: () => {
this.error = undefined
this.init()
},
}
this.status = ConvoStatus.Error this.status = ConvoStatus.Error
this.commit() this.commit()
} }
@ -318,6 +360,8 @@ export class Convo {
this.status === ConvoStatus.Suspended || this.status === ConvoStatus.Suspended ||
this.status === ConvoStatus.Backgrounded this.status === ConvoStatus.Backgrounded
) { ) {
const fromStatus = this.status
try { try {
this.status = ConvoStatus.Resuming this.status = ConvoStatus.Resuming
this.commit() this.commit()
@ -326,14 +370,24 @@ export class Convo {
this.status = ConvoStatus.Ready this.status = ConvoStatus.Ready
this.commit() this.commit()
await this.fetchMessageHistory() // throw new Error('UNCOMMENT TO TEST RESUME FAILURE')
this.pollInterval = ACTIVE_POLL_INTERVAL this.pollInterval = ACTIVE_POLL_INTERVAL
this.pollEvents() this.pollEvents()
} catch (e) { } catch (e) {
// TODO handle errors in one place logger.error('Convo: failed to resume')
this.error = e
this.status = ConvoStatus.Error this.footerItems.set(ConvoItemError.ResumeFailed, {
type: 'error-recoverable',
key: ConvoItemError.ResumeFailed,
code: ConvoItemError.ResumeFailed,
retry: () => {
this.footerItems.delete(ConvoItemError.ResumeFailed)
this.resume()
},
})
this.status = fromStatus
this.commit() this.commit()
} }
} else { } else {
@ -367,6 +421,19 @@ export class Convo {
) )
this.convo = response.data.convo this.convo = response.data.convo
this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid) this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid)
this.recipients = this.convo.members.filter(
m => m.did !== this.__tempFromUserDid,
)
/*
* Prevent invalid states
*/
if (!this.sender) {
throw new Error('Convo: could not find sender in convo')
}
if (!this.recipients) {
throw new Error('Convo: could not find recipients in convo')
}
} }
async fetchMessageHistory() { async fetchMessageHistory() {
@ -386,7 +453,7 @@ export class Convo {
* If we've rendered a retry state for history fetching, exit. Upon retry, * If we've rendered a retry state for history fetching, exit. Upon retry,
* this will be removed and we'll try again. * this will be removed and we'll try again.
*/ */
if (this.headerItems.has(ConvoError.HistoryFailed)) return if (this.headerItems.has(ConvoItemError.HistoryFailed)) return
try { try {
this.isFetchingHistory = true this.isFetchingHistory = true
@ -435,12 +502,12 @@ export class Convo {
} catch (e: any) { } catch (e: any) {
logger.error('Convo: failed to fetch message history') logger.error('Convo: failed to fetch message history')
this.headerItems.set(ConvoError.HistoryFailed, { this.headerItems.set(ConvoItemError.HistoryFailed, {
type: 'error-recoverable', type: 'error-recoverable',
key: ConvoError.HistoryFailed, key: ConvoItemError.HistoryFailed,
code: ConvoError.HistoryFailed, code: ConvoItemError.HistoryFailed,
retry: () => { retry: () => {
this.headerItems.delete(ConvoError.HistoryFailed) this.headerItems.delete(ConvoItemError.HistoryFailed)
this.fetchMessageHistory() this.fetchMessageHistory()
}, },
}) })
@ -457,6 +524,11 @@ export class Convo {
) { ) {
if (this.pendingEventIngestion) return if (this.pendingEventIngestion) return
/*
* Represents a failed state, which is retryable.
*/
if (this.pollingFailure) return
setTimeout(async () => { setTimeout(async () => {
this.pendingEventIngestion = this.ingestLatestEvents() this.pendingEventIngestion = this.ingestLatestEvents()
await this.pendingEventIngestion await this.pendingEventIngestion
@ -467,6 +539,8 @@ export class Convo {
} }
async ingestLatestEvents() { async ingestLatestEvents() {
try {
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
const response = await this.agent.api.chat.bsky.convo.getLog( const response = await this.agent.api.chat.bsky.convo.getLog(
{ {
cursor: this.eventsCursor, cursor: this.eventsCursor,
@ -539,6 +613,22 @@ export class Convo {
if (needsCommit) { if (needsCommit) {
this.commit() this.commit()
} }
} catch (e: any) {
logger.error('Convo: failed to poll events')
this.pollingFailure = true
this.footerItems.set(ConvoItemError.PollFailed, {
type: 'error-recoverable',
key: ConvoItemError.PollFailed,
code: ConvoItemError.PollFailed,
retry: () => {
this.footerItems.delete(ConvoItemError.PollFailed)
this.pollingFailure = false
this.commit()
this.pollEvents()
},
})
this.commit()
}
} }
async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) { async sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {