From c995eb2f2fa3e73dcc6943078c85cd6a68f5370b Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 4 Mar 2025 13:54:19 +0000
Subject: DMs inbox (#7778)

* improve error screen

* add chat request prompt

* mock up inbox

* bigger button

* use two-button layout

* get inbox working somewhat

* fix type errors

* fetch both pages for badge

* don't include read convos in preview

* in-chat ui for non-accepted convos (part 1)

* add chatstatusinfo

* fix status info not disappearing

* get chat status info working

* change min item height

* move files around

* add updated sdk

* improve badge behaviour

* mock up mark all as read

* update sdk to 0.14.4

* hide chat status info if initiating convo

* fix unread count for deleted accounts

* add toasts after rejection

* add prompt to delete

* adjust badge on desktop

* requests -> chat requests

* fix height flicker

* add mark as read button to header

* add mark all as read APIs

* separate avatarstack into two components (#7845)

* fix messages being hidden behind chatstatusinfo

* show inbox preview on empty state

* fix empty state again

* Use new convo availability API (#7812)

* [Inbox] Accept button on convo screen (#7795)

* accept button on convo screen

* fix types

* fix type error

* improve spacing

* [DMs] Implement new log types (#7835)

* optimise badge state

* add read message log

* add isLogAcceptConvo

* mute/unmute convo logs

* use setqueriesdata

* always show label on button

* optimistically update badge

* change incorrect unread count change

* Update src/screens/Messages/Inbox.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestButtons.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestButtons.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/screens/Messages/components/RequestListItem.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* fix race condition with accepting convo

* fix back button on web

* filter left convos from badge

* update atproto to fix CI

* Add accept override external to convo (#7891)

* Add accept override external to convo

* rm log

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
---
 src/screens/Messages/ChatList.tsx | 102 ++++++++++++++++++++++++++++----------
 1 file changed, 77 insertions(+), 25 deletions(-)

(limited to 'src/screens/Messages/ChatList.tsx')

diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx
index 32b111def..b060b23e5 100644
--- a/src/screens/Messages/ChatList.tsx
+++ b/src/screens/Messages/ChatList.tsx
@@ -1,7 +1,7 @@
 import {useCallback, useEffect, useMemo, useState} from 'react'
 import {View} from 'react-native'
 import {useAnimatedRef} from 'react-native-reanimated'
-import {ChatBskyConvoDefs} from '@atproto/api'
+import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
@@ -18,6 +18,7 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
 import {useMessagesEventBus} from '#/state/messages/events'
 import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
 import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
+import {useSession} from '#/state/session'
 import {List, ListRef} from '#/view/com/util/List'
 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -35,20 +36,37 @@ import {ListFooter} from '#/components/Lists'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 import {ChatListItem} from './components/ChatListItem'
+import {InboxPreview} from './components/InboxPreview'
 
-type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
+type ListItem =
+  | {
+      type: 'INBOX'
+      count: number
+      profiles: ChatBskyActorDefs.ProfileViewBasic[]
+    }
+  | {
+      type: 'CONVERSATION'
+      conversation: ChatBskyConvoDefs.ConvoView
+    }
 
-function renderItem({item}: {item: ChatBskyConvoDefs.ConvoView}) {
-  return <ChatListItem convo={item} />
+function renderItem({item}: {item: ListItem}) {
+  switch (item.type) {
+    case 'INBOX':
+      return <InboxPreview count={item.count} profiles={item.profiles} />
+    case 'CONVERSATION':
+      return <ChatListItem convo={item.conversation} />
+  }
 }
 
-function keyExtractor(item: ChatBskyConvoDefs.ConvoView) {
-  return item.id
+function keyExtractor(item: ListItem) {
+  return item.type === 'INBOX' ? 'INBOX' : item.conversation.id
 }
 
+type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
 export function MessagesScreen({navigation, route}: Props) {
   const {_} = useLingui()
   const t = useTheme()
+  const {currentAccount} = useSession()
   const newChatControl = useDialogControl()
   const scrollElRef: ListRef = useAnimatedRef()
   const pushToConversation = route.params?.pushToConversation
@@ -94,33 +112,63 @@ export function MessagesScreen({navigation, route}: Props) {
     isError,
     error,
     refetch,
-  } = useListConvosQuery()
+  } = useListConvosQuery({status: 'accepted'})
+
+  const {data: inboxData, refetch: refetchInbox} = useListConvosQuery({
+    status: 'request',
+  })
 
   useRefreshOnFocus(refetch)
+  useRefreshOnFocus(refetchInbox)
 
   const leftConvos = useLeftConvos()
 
+  const inboxPreviewConvos = useMemo(() => {
+    const inbox =
+      inboxData?.pages
+        .flatMap(page => page.convos)
+        .filter(
+          convo =>
+            !leftConvos.includes(convo.id) &&
+            !convo.muted &&
+            convo.unreadCount > 0,
+        ) ?? []
+
+    return inbox
+      .map(x => x.members.find(y => y.did !== currentAccount?.did))
+      .filter(x => !!x)
+  }, [inboxData, leftConvos, currentAccount?.did])
+
   const conversations = useMemo(() => {
     if (data?.pages) {
-      return (
-        data.pages
-          .flatMap(page => page.convos)
-          // filter out convos that are actively being left
-          .filter(convo => !leftConvos.includes(convo.id))
-      )
+      const conversations = data.pages
+        .flatMap(page => page.convos)
+        // filter out convos that are actively being left
+        .filter(convo => !leftConvos.includes(convo.id))
+
+      return [
+        {
+          type: 'INBOX',
+          count: inboxPreviewConvos.length,
+          profiles: inboxPreviewConvos.slice(0, 3),
+        },
+        ...conversations.map(
+          convo => ({type: 'CONVERSATION', conversation: convo} as const),
+        ),
+      ] satisfies ListItem[]
     }
     return []
-  }, [data, leftConvos])
+  }, [data, leftConvos, inboxPreviewConvos])
 
   const onRefresh = useCallback(async () => {
     setIsPTRing(true)
     try {
-      await refetch()
+      await Promise.all([refetch(), refetchInbox()])
     } catch (err) {
       logger.error('Failed to refresh conversations', {message: err})
     }
     setIsPTRing(false)
-  }, [refetch, setIsPTRing])
+  }, [refetch, refetchInbox, setIsPTRing])
 
   const onEndReached = useCallback(async () => {
     if (isFetchingNextPage || !hasNextPage || isError) return
@@ -157,7 +205,8 @@ export function MessagesScreen({navigation, route}: Props) {
     return listenSoftReset(onSoftReset)
   }, [onSoftReset, isScreenFocused])
 
-  if (conversations.length < 1) {
+  // Will always have 1 item - the inbox button
+  if (conversations.length < 2) {
     return (
       <Layout.Screen>
         <Header newChatControl={newChatControl} />
@@ -173,7 +222,7 @@ export function MessagesScreen({navigation, route}: Props) {
                   <View style={[a.pt_3xl, a.align_center]}>
                     <CircleInfo
                       width={48}
-                      fill={t.atoms.border_contrast_low.borderColor}
+                      fill={t.atoms.text_contrast_low.color}
                     />
                     <Text style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_bold]}>
                       <Trans>Whoops!</Trans>
@@ -187,13 +236,14 @@ export function MessagesScreen({navigation, route}: Props) {
                         t.atoms.text_contrast_medium,
                         {maxWidth: 360},
                       ]}>
-                      {cleanError(error)}
+                      {cleanError(error) ||
+                        _(msg`Failed to load conversations`)}
                     </Text>
 
                     <Button
                       label={_(msg`Reload conversations`)}
-                      size="large"
-                      color="secondary"
+                      size="small"
+                      color="secondary_inverted"
                       variant="solid"
                       onPress={() => refetch()}>
                       <ButtonText>
@@ -205,6 +255,10 @@ export function MessagesScreen({navigation, route}: Props) {
                 </>
               ) : (
                 <>
+                  <InboxPreview
+                    count={inboxPreviewConvos.length}
+                    profiles={inboxPreviewConvos}
+                  />
                   <View style={[a.pt_3xl, a.align_center]}>
                     <Message width={48} fill={t.palette.primary_500} />
                     <Text style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_bold]}>
@@ -253,8 +307,6 @@ export function MessagesScreen({navigation, route}: Props) {
             onRetry={fetchNextPage}
             style={{borderColor: 'transparent'}}
             hasNextPage={hasNextPage}
-            showEndMessage={true}
-            endMessageText={_(msg`No more conversations to show`)}
           />
         }
         onEndReachedThreshold={isNative ? 1.5 : 0}
@@ -290,7 +342,7 @@ function Header({newChatControl}: {newChatControl: DialogControlProps}) {
         <>
           <Layout.Header.Content>
             <Layout.Header.TitleText>
-              <Trans>Messages</Trans>
+              <Trans>Chats</Trans>
             </Layout.Header.TitleText>
           </Layout.Header.Content>
 
@@ -314,7 +366,7 @@ function Header({newChatControl}: {newChatControl: DialogControlProps}) {
           <Layout.Header.MenuButton />
           <Layout.Header.Content>
             <Layout.Header.TitleText>
-              <Trans>Messages</Trans>
+              <Trans>Chats</Trans>
             </Layout.Header.TitleText>
           </Layout.Header.Content>
           <Layout.Header.Slot>{settingsLink}</Layout.Header.Slot>
-- 
cgit 1.4.1

