about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-05-14 11:59:53 -0500
committerGitHub <noreply@github.com>2024-05-14 11:59:53 -0500
commit1c51a48764e4145679198f68368713410e28c8da (patch)
tree6f2840665b10dfbb3591f0a694c9d5165f383be3
parentbffb9b590672c1e636083bdf9873f5cd8ab97b57 (diff)
downloadvoidsky-1c51a48764e4145679198f68368713410e28c8da.tar.zst
[🐴] Make status checks easier, fix load state (#4010)
* Make status checks easier, fix load state

* Make naming more clear

* Split up types for easier re-use

* Replace hacky usage
-rw-r--r--src/components/dms/MessageMenu.tsx7
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx18
-rw-r--r--src/screens/Messages/Conversation/index.tsx14
-rw-r--r--src/state/messages/convo/index.tsx32
-rw-r--r--src/state/messages/convo/types.ts162
-rw-r--r--src/state/messages/convo/util.ts22
6 files changed, 154 insertions, 101 deletions
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 3349b2ff8..55c3ac21b 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -7,8 +7,7 @@ import {useLingui} from '@lingui/react'
 
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
 import {isWeb} from 'platform/detection'
-import {useConvo} from 'state/messages/convo'
-import {ConvoStatus} from 'state/messages/convo/types'
+import {useConvoActive} from 'state/messages/convo'
 import {useSession} from 'state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
@@ -34,7 +33,7 @@ export let MessageMenu = ({
   const {_} = useLingui()
   const t = useTheme()
   const {currentAccount} = useSession()
-  const convo = useConvo()
+  const convo = useConvoActive()
   const deleteControl = usePromptControl()
   const retryDeleteControl = usePromptControl()
   const reportControl = usePromptControl()
@@ -55,8 +54,6 @@ export let MessageMenu = ({
   }, [_, message.text, message.facets])
 
   const onDelete = React.useCallback(() => {
-    if (convo.status !== ConvoStatus.Ready) return
-
     LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
     convo
       .deleteMessage(message.id)
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index 5ba82eeff..dac534cd4 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -7,8 +7,8 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api'
 
 import {shortenLinks} from '#/lib/strings/rich-text-manip'
 import {isNative} from '#/platform/detection'
-import {useConvo} from '#/state/messages/convo'
-import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
+import {useConvoActive} from '#/state/messages/convo'
+import {ConvoItem} from '#/state/messages/convo/types'
 import {useAgent} from '#/state/session'
 import {ScrollProvider} from 'lib/ScrollContext'
 import {isWeb} from 'platform/detection'
@@ -60,7 +60,7 @@ function onScrollToIndexFailed() {
 }
 
 export function MessagesList() {
-  const convo = useConvo()
+  const convo = useConvoActive()
   const {getAgent} = useAgent()
   const flatListRef = useRef<FlatList>(null)
 
@@ -128,7 +128,7 @@ export function MessagesList() {
   // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached`
   // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls.
   const onStartReached = useCallback(() => {
-    if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled.value) {
+    if (hasInitiallyScrolled.value) {
       convo.fetchMessageHistory()
     }
   }, [convo, hasInitiallyScrolled])
@@ -150,12 +150,10 @@ export function MessagesList() {
         return true
       })
 
-      if (convo.status === ConvoStatus.Ready) {
-        convo.sendMessage({
-          text: rt.text,
-          facets: rt.facets,
-        })
-      }
+      convo.sendMessage({
+        text: rt.text,
+        facets: rt.facets,
+      })
     },
     [convo, getAgent],
   )
diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx
index a783a0bd6..01c205ac8 100644
--- a/src/screens/Messages/Conversation/index.tsx
+++ b/src/screens/Messages/Conversation/index.tsx
@@ -15,7 +15,7 @@ import {useGate} from '#/lib/statsig/statsig'
 import {useCurrentConvoId} from '#/state/messages/current-convo-id'
 import {BACK_HITSLOP} from 'lib/constants'
 import {isIOS, isWeb} from 'platform/detection'
-import {ConvoProvider, useConvo} from 'state/messages/convo'
+import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
 import {ConvoStatus} from 'state/messages/convo/types'
 import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
 import {CenteredView} from 'view/com/util/Views'
@@ -72,14 +72,14 @@ function Inner() {
   React.useEffect(() => {
     if (
       !hasInitiallyRendered &&
-      convoState.status === ConvoStatus.Ready &&
+      isConvoActive(convoState) &&
       !convoState.isFetchingHistory
     ) {
       setTimeout(() => {
         setHasInitiallyRendered(true)
       }, 15)
     }
-  }, [convoState.isFetchingHistory, convoState.status, hasInitiallyRendered])
+  }, [convoState, hasInitiallyRendered])
 
   if (convoState.status === ConvoStatus.Error) {
     return (
@@ -108,10 +108,10 @@ function Inner() {
         <CenteredView style={a.flex_1} sideBorders>
           <Header profile={convoState.recipients?.[0]} />
           <View style={[a.flex_1]}>
-            {convoState.status !== ConvoStatus.Ready ? (
-              <ListMaybePlaceholder isLoading />
-            ) : (
+            {isConvoActive(convoState) ? (
               <MessagesList />
+            ) : (
+              <ListMaybePlaceholder isLoading />
             )}
             {!hasInitiallyRendered && (
               <View
@@ -230,7 +230,7 @@ let Header = ({
           </>
         )}
       </View>
-      {convoState.status === ConvoStatus.Ready && profile ? (
+      {isConvoActive(convoState) && profile ? (
         <ConvoMenu
           convo={convoState.convo}
           profile={profile}
diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx
index 9c5295832..e955d4118 100644
--- a/src/state/messages/convo/index.tsx
+++ b/src/state/messages/convo/index.tsx
@@ -3,11 +3,20 @@ import {AppState} from 'react-native'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 
 import {Convo} from '#/state/messages/convo/agent'
-import {ConvoParams, ConvoState} from '#/state/messages/convo/types'
+import {
+  ConvoParams,
+  ConvoState,
+  ConvoStateBackgrounded,
+  ConvoStateReady,
+  ConvoStateSuspended,
+} from '#/state/messages/convo/types'
+import {isConvoActive} from '#/state/messages/convo/util'
 import {useMessagesEventBus} from '#/state/messages/events'
 import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
 import {useAgent} from '#/state/session'
 
+export * from '#/state/messages/convo/util'
+
 const ChatContext = React.createContext<ConvoState | null>(null)
 
 export function useConvo() {
@@ -18,6 +27,27 @@ export function useConvo() {
   return ctx
 }
 
+/**
+ * This hook should only be used when the Convo is "active", meaning the chat
+ * is loaded and ready to be used, or its in a suspended or background state,
+ * and ready for resumption.
+ */
+export function useConvoActive() {
+  const ctx = useContext(ChatContext) as
+    | ConvoStateReady
+    | ConvoStateBackgrounded
+    | ConvoStateSuspended
+  if (!ctx) {
+    throw new Error('useConvo must be used within a ConvoProvider')
+  }
+  if (!isConvoActive(ctx)) {
+    throw new Error(
+      `useConvoActive must only be rendered when the Convo is ready.`,
+    )
+  }
+  return ctx
+}
+
 export function ConvoProvider({
   children,
   convoId,
diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts
index 4615acc2d..6ce4d40bd 100644
--- a/src/state/messages/convo/types.ts
+++ b/src/state/messages/convo/types.ts
@@ -107,82 +107,88 @@ export type ConvoItem =
       retry: () => void
     }
 
+type DeleteMessage = (messageId: string) => Promise<void>
+type SendMessage = (
+  message: ChatBskyConvoSendMessage.InputSchema['message'],
+) => Promise<void>
+type FetchMessageHistory = () => Promise<void>
+
+export type ConvoStateUninitialized = {
+  status: ConvoStatus.Uninitialized
+  items: []
+  convo: undefined
+  error: undefined
+  sender: undefined
+  recipients: undefined
+  isFetchingHistory: false
+  deleteMessage: undefined
+  sendMessage: undefined
+  fetchMessageHistory: undefined
+}
+export type ConvoStateInitializing = {
+  status: ConvoStatus.Initializing
+  items: []
+  convo: undefined
+  error: undefined
+  sender: undefined
+  recipients: undefined
+  isFetchingHistory: boolean
+  deleteMessage: undefined
+  sendMessage: undefined
+  fetchMessageHistory: undefined
+}
+export type ConvoStateReady = {
+  status: ConvoStatus.Ready
+  items: ConvoItem[]
+  convo: ChatBskyConvoDefs.ConvoView
+  error: undefined
+  sender: AppBskyActorDefs.ProfileViewBasic
+  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  isFetchingHistory: boolean
+  deleteMessage: DeleteMessage
+  sendMessage: SendMessage
+  fetchMessageHistory: FetchMessageHistory
+}
+export type ConvoStateBackgrounded = {
+  status: ConvoStatus.Backgrounded
+  items: ConvoItem[]
+  convo: ChatBskyConvoDefs.ConvoView
+  error: undefined
+  sender: AppBskyActorDefs.ProfileViewBasic
+  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  isFetchingHistory: boolean
+  deleteMessage: DeleteMessage
+  sendMessage: SendMessage
+  fetchMessageHistory: FetchMessageHistory
+}
+export type ConvoStateSuspended = {
+  status: ConvoStatus.Suspended
+  items: ConvoItem[]
+  convo: ChatBskyConvoDefs.ConvoView
+  error: undefined
+  sender: AppBskyActorDefs.ProfileViewBasic
+  recipients: AppBskyActorDefs.ProfileViewBasic[]
+  isFetchingHistory: boolean
+  deleteMessage: DeleteMessage
+  sendMessage: SendMessage
+  fetchMessageHistory: FetchMessageHistory
+}
+export type ConvoStateError = {
+  status: ConvoStatus.Error
+  items: []
+  convo: undefined
+  error: any
+  sender: undefined
+  recipients: undefined
+  isFetchingHistory: false
+  deleteMessage: undefined
+  sendMessage: undefined
+  fetchMessageHistory: undefined
+}
 export type ConvoState =
-  | {
-      status: ConvoStatus.Uninitialized
-      items: []
-      convo: undefined
-      error: undefined
-      sender: undefined
-      recipients: undefined
-      isFetchingHistory: false
-      deleteMessage: undefined
-      sendMessage: undefined
-      fetchMessageHistory: undefined
-    }
-  | {
-      status: ConvoStatus.Initializing
-      items: []
-      convo: undefined
-      error: undefined
-      sender: undefined
-      recipients: undefined
-      isFetchingHistory: boolean
-      deleteMessage: undefined
-      sendMessage: undefined
-      fetchMessageHistory: undefined
-    }
-  | {
-      status: ConvoStatus.Ready
-      items: ConvoItem[]
-      convo: ChatBskyConvoDefs.ConvoView
-      error: undefined
-      sender: AppBskyActorDefs.ProfileViewBasic
-      recipients: AppBskyActorDefs.ProfileViewBasic[]
-      isFetchingHistory: boolean
-      deleteMessage: (messageId: string) => Promise<void>
-      sendMessage: (
-        message: ChatBskyConvoSendMessage.InputSchema['message'],
-      ) => void
-      fetchMessageHistory: () => void
-    }
-  | {
-      status: ConvoStatus.Suspended
-      items: ConvoItem[]
-      convo: ChatBskyConvoDefs.ConvoView
-      error: undefined
-      sender: AppBskyActorDefs.ProfileViewBasic
-      recipients: AppBskyActorDefs.ProfileViewBasic[]
-      isFetchingHistory: boolean
-      deleteMessage: (messageId: string) => Promise<void>
-      sendMessage: (
-        message: ChatBskyConvoSendMessage.InputSchema['message'],
-      ) => Promise<void>
-      fetchMessageHistory: () => Promise<void>
-    }
-  | {
-      status: ConvoStatus.Backgrounded
-      items: ConvoItem[]
-      convo: ChatBskyConvoDefs.ConvoView
-      error: undefined
-      sender: AppBskyActorDefs.ProfileViewBasic
-      recipients: AppBskyActorDefs.ProfileViewBasic[]
-      isFetchingHistory: boolean
-      deleteMessage: (messageId: string) => Promise<void>
-      sendMessage: (
-        message: ChatBskyConvoSendMessage.InputSchema['message'],
-      ) => Promise<void>
-      fetchMessageHistory: () => Promise<void>
-    }
-  | {
-      status: ConvoStatus.Error
-      items: []
-      convo: undefined
-      error: any
-      sender: undefined
-      recipients: undefined
-      isFetchingHistory: false
-      deleteMessage: undefined
-      sendMessage: undefined
-      fetchMessageHistory: undefined
-    }
+  | ConvoStateUninitialized
+  | ConvoStateInitializing
+  | ConvoStateReady
+  | ConvoStateBackgrounded
+  | ConvoStateSuspended
+  | ConvoStateError
diff --git a/src/state/messages/convo/util.ts b/src/state/messages/convo/util.ts
new file mode 100644
index 000000000..ffaa4104a
--- /dev/null
+++ b/src/state/messages/convo/util.ts
@@ -0,0 +1,22 @@
+import {
+  ConvoState,
+  ConvoStateBackgrounded,
+  ConvoStateReady,
+  ConvoStateSuspended,
+  ConvoStatus,
+} from './types'
+
+/**
+ * Checks if a `Convo` has a `status` that is "active", meaning the chat is
+ * loaded and ready to be used, or its in a suspended or background state, and
+ * ready for resumption.
+ */
+export function isConvoActive(
+  convo: ConvoState,
+): convo is ConvoStateReady | ConvoStateBackgrounded | ConvoStateSuspended {
+  return (
+    convo.status === ConvoStatus.Ready ||
+    convo.status === ConvoStatus.Backgrounded ||
+    convo.status === ConvoStatus.Suspended
+  )
+}