about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/screens/Messages/Conversation/MessageListError.tsx54
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx3
-rw-r--r--src/state/messages/convo.ts109
3 files changed, 132 insertions, 34 deletions
diff --git a/src/screens/Messages/Conversation/MessageListError.tsx b/src/screens/Messages/Conversation/MessageListError.tsx
new file mode 100644
index 000000000..82ca48e8b
--- /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 (
+    <View style={[a.py_md, a.align_center]}>
+      <View
+        style={[
+          a.align_center,
+          a.pt_md,
+          a.pb_lg,
+          a.px_3xl,
+          a.rounded_md,
+          t.atoms.bg_contrast_25,
+          {maxWidth: 300},
+        ]}>
+        <CircleInfo size="lg" fill={t.palette.negative_400} />
+        <Text style={[a.pt_sm, a.leading_snug]}>
+          {message}{' '}
+          <InlineLinkText
+            to="#"
+            label={_(msg`Press to retry`)}
+            onPress={e => {
+              e.preventDefault()
+              item.retry()
+              return false
+            }}>
+            {_(msg`Retry.`)}
+          </InlineLinkText>
+        </Text>
+      </View>
+    </View>
+  )
+}
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 3990a1dea..86a10d8c4 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 <Text>Deleted message</Text>
   } else if (item.type === 'pending-retry') {
     return <RetryButton onPress={item.retry} />
+  } else if (item.type === 'error-recoverable') {
+    return <MessageListError item={item} />
   }
 
   return null
diff --git a/src/state/messages/convo.ts b/src/state/messages/convo.ts
index f687008e5..fe2095c46 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<string> = new Set()
   private footerItems: Map<string, ConvoItem> = new Map()
+  private headerItems: Map<string, ConvoItem> = new Map()
 
   private pendingEventIngestion: Promise<void> | 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)) {