diff --git a/src/screens/Messages/Conversation/MessageListError.tsx b/src/screens/Messages/Conversation/MessageListError.tsx
new file mode 100644
index 0000000000..82ca48e8b4
--- /dev/null
+++ b/src/screens/Messages/Conversation/MessageListError.tsx
@@ -0,0 +1,54 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {ConvoError, ConvoItem} from '#/state/messages/convo'
+import {atoms as a, useTheme} from '#/alf'
+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'}
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const message = React.useMemo(() => {
+ return {
+ [ConvoError.HistoryFailed]: _(msg`Failed to load past messages.`),
+ }[item.code]
+ }, [_, item.code])
+
+ return (
+
+
+
+
+ {message}{' '}
+ {
+ e.preventDefault()
+ item.retry()
+ return false
+ }}>
+ {_(msg`Retry.`)}
+
+
+
+
+ )
+}
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 3990a1deaa..86a10d8c4f 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -17,6 +17,7 @@ import {useChat} from '#/state/messages'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo'
import {useSetMinimalShellMode} from '#/state/shell'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
+import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {MessageItem} from '#/components/dms/MessageItem'
@@ -63,6 +64,8 @@ function renderItem({item}: {item: ConvoItem}) {
return Deleted message
} else if (item.type === 'pending-retry') {
return
+ } else if (item.type === 'error-recoverable') {
+ return
}
return null
diff --git a/src/state/messages/convo.ts b/src/state/messages/convo.ts
index f687008e5e..fe2095c462 100644
--- a/src/state/messages/convo.ts
+++ b/src/state/messages/convo.ts
@@ -25,6 +25,10 @@ export enum ConvoStatus {
Suspended = 'suspended',
}
+export enum ConvoError {
+ HistoryFailed = 'historyFailed',
+}
+
export type ConvoItem =
| {
type: 'message' | 'pending-message'
@@ -49,6 +53,17 @@ export type ConvoItem =
key: string
retry: () => void
}
+ | {
+ type: 'error-recoverable'
+ key: string
+ code: ConvoError
+ retry: () => void
+ }
+ | {
+ type: 'error-fatal'
+ code: ConvoError
+ key: string
+ }
export type ConvoState =
| {
@@ -169,6 +184,7 @@ export class Convo {
> = new Map()
private deletedMessages: Set = new Set()
private footerItems: Map = new Map()
+ private headerItems: Map = new Map()
private pendingEventIngestion: Promise | undefined
private isProcessingPendingMessages = false
@@ -366,51 +382,72 @@ export class Convo {
*/
if (this.isFetchingHistory) return
- this.isFetchingHistory = true
- this.commit()
-
/*
- * Delay if paginating while scrolled to prevent momentum scrolling from
- * jerking the list around, plus makes it feel a little more human.
+ * If we've rendered a retry state for history fetching, exit. Upon retry,
+ * this will be removed and we'll try again.
*/
- if (this.pastMessages.size > 0) {
- await new Promise(y => setTimeout(y, 500))
- }
+ if (this.headerItems.has(ConvoError.HistoryFailed)) return
- const response = await this.agent.api.chat.bsky.convo.getMessages(
- {
- cursor: this.historyCursor,
- convoId: this.convoId,
- limit: isNative ? 25 : 50,
- },
- {
- headers: {
- Authorization: this.__tempFromUserDid,
- },
- },
- )
- const {cursor, messages} = response.data
+ try {
+ this.isFetchingHistory = true
+ this.commit()
- this.historyCursor = cursor || null
+ /*
+ * Delay if paginating while scrolled to prevent momentum scrolling from
+ * jerking the list around, plus makes it feel a little more human.
+ */
+ if (this.pastMessages.size > 0) {
+ await new Promise(y => setTimeout(y, 500))
+ // throw new Error('UNCOMMENT TO TEST RETRY')
+ }
+
+ const response = await this.agent.api.chat.bsky.convo.getMessages(
+ {
+ cursor: this.historyCursor,
+ convoId: this.convoId,
+ limit: isNative ? 25 : 50,
+ },
+ {
+ headers: {
+ Authorization: this.__tempFromUserDid,
+ },
+ },
+ )
+ const {cursor, messages} = response.data
- for (const message of messages) {
- if (
- ChatBskyConvoDefs.isMessageView(message) ||
- ChatBskyConvoDefs.isDeletedMessageView(message)
- ) {
- this.pastMessages.set(message.id, message)
+ this.historyCursor = cursor ?? null
- // set to latest rev
+ for (const message of messages) {
if (
- message.rev > (this.eventsCursor = this.eventsCursor || message.rev)
+ ChatBskyConvoDefs.isMessageView(message) ||
+ ChatBskyConvoDefs.isDeletedMessageView(message)
) {
- this.eventsCursor = message.rev
+ this.pastMessages.set(message.id, message)
+
+ // set to latest rev
+ if (
+ message.rev > (this.eventsCursor = this.eventsCursor || message.rev)
+ ) {
+ this.eventsCursor = message.rev
+ }
}
}
+ } catch (e: any) {
+ logger.error('Convo: failed to fetch message history')
+
+ this.headerItems.set(ConvoError.HistoryFailed, {
+ type: 'error-recoverable',
+ key: ConvoError.HistoryFailed,
+ code: ConvoError.HistoryFailed,
+ retry: () => {
+ this.headerItems.delete(ConvoError.HistoryFailed)
+ this.fetchMessageHistory()
+ },
+ })
+ } finally {
+ this.isFetchingHistory = false
+ this.commit()
}
-
- this.isFetchingHistory = false
- this.commit()
}
private async pollEvents() {
@@ -730,6 +767,10 @@ export class Convo {
}
})
+ this.headerItems.forEach(item => {
+ items.push(item)
+ })
+
return items
.filter(item => {
if (isConvoItemMessage(item)) {