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>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyConvoDefs, ChatBskyConvoListConvos} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
|
||||
|
||||
import {useAppState} from '#/lib/hooks/useAppState'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {
|
||||
CommonNavigatorParams,
|
||||
NativeStackScreenProps,
|
||||
NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
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 {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {RequestListItem} from './components/RequestListItem'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
||||
export function MessagesInboxScreen({}: Props) {
|
||||
const {gtTablet} = useBreakpoints()
|
||||
|
||||
const listConvosQuery = useListConvosQuery({status: 'request'})
|
||||
const {data} = listConvosQuery
|
||||
|
||||
const leftConvos = useLeftConvos()
|
||||
|
||||
const conversations = useMemo(() => {
|
||||
if (data?.pages) {
|
||||
const convos = data.pages
|
||||
.flatMap(page => page.convos)
|
||||
// filter out convos that are actively being left
|
||||
.filter(convo => !leftConvos.includes(convo.id))
|
||||
|
||||
return convos
|
||||
}
|
||||
return []
|
||||
}, [data, leftConvos])
|
||||
|
||||
const hasUnreadConvos = useMemo(() => {
|
||||
return conversations.some(conversation => conversation.unreadCount > 0)
|
||||
}, [conversations])
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="messagesInboxScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align={gtTablet ? 'left' : 'platform'}>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Chat requests</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
{hasUnreadConvos && gtTablet ? (
|
||||
<MarkAsReadHeaderButton />
|
||||
) : (
|
||||
<Layout.Header.Slot />
|
||||
)}
|
||||
</Layout.Header.Outer>
|
||||
<RequestList
|
||||
listConvosQuery={listConvosQuery}
|
||||
conversations={conversations}
|
||||
hasUnreadConvos={hasUnreadConvos}
|
||||
/>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function RequestList({
|
||||
listConvosQuery,
|
||||
conversations,
|
||||
hasUnreadConvos,
|
||||
}: {
|
||||
listConvosQuery: UseInfiniteQueryResult<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>,
|
||||
Error
|
||||
>
|
||||
conversations: ChatBskyConvoDefs.ConvoView[]
|
||||
hasUnreadConvos: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
// Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future)
|
||||
// but only when the screen is active
|
||||
const messagesBus = useMessagesEventBus()
|
||||
const state = useAppState()
|
||||
const isActive = state === 'active'
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (isActive) {
|
||||
const unsub = messagesBus.requestPollInterval(
|
||||
MESSAGE_SCREEN_POLL_INTERVAL,
|
||||
)
|
||||
return () => unsub()
|
||||
}
|
||||
}, [messagesBus, isActive]),
|
||||
)
|
||||
|
||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 130})
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = listConvosQuery
|
||||
|
||||
useRefreshOnFocus(refetch)
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh conversations', {message: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more conversations', {message: err})
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
if (conversations.length < 1) {
|
||||
return (
|
||||
<Layout.Center>
|
||||
{isLoading ? (
|
||||
<View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isError ? (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<CircleInfoIcon
|
||||
width={48}
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
/>
|
||||
<Text style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_bold]}>
|
||||
<Trans>Whoops!</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
{maxWidth: 360},
|
||||
]}>
|
||||
{cleanError(error) || _(msg`Failed to load conversations`)}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
label={_(msg`Reload conversations`)}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
onPress={() => refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={RetryIcon} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.pt_3xl, a.align_center]}>
|
||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
||||
<Text style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_bold]}>
|
||||
<Trans comment="Title message shown in chat requests inbox when it's empty">
|
||||
Inbox zero!
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.pb_xl,
|
||||
a.text_center,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
You don't have any chat requests at the moment.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
label={_(msg`Go back`)}
|
||||
onPress={() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Messages', {animation: 'pop'})
|
||||
}
|
||||
}}>
|
||||
<ButtonIcon icon={ArrowLeftIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Back to Chats</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Layout.Center>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
data={conversations}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
style={{borderColor: 'transparent'}}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
onEndReachedThreshold={isNative ? 1.5 : 0}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
desktopFixedHeight
|
||||
sideBorders={false}
|
||||
/>
|
||||
{hasUnreadConvos && <MarkAllReadFAB />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function keyExtractor(item: ChatBskyConvoDefs.ConvoView) {
|
||||
return item.id
|
||||
}
|
||||
|
||||
function renderItem({item}: {item: ChatBskyConvoDefs.ConvoView}) {
|
||||
return <RequestListItem convo={item} />
|
||||
}
|
||||
|
||||
function MarkAllReadFAB() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {mutate: markAllRead} = useUpdateAllRead('request', {
|
||||
onMutate: () => {
|
||||
Toast.show(_(msg`Marked all as read`), 'check')
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to mark all requests as read`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<FAB
|
||||
testID="markAllAsReadFAB"
|
||||
onPress={() => markAllRead()}
|
||||
icon={<CheckIcon size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Mark all as read`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MarkAsReadHeaderButton() {
|
||||
const {_} = useLingui()
|
||||
const {mutate: markAllRead} = useUpdateAllRead('request', {
|
||||
onMutate: () => {
|
||||
Toast.show(_(msg`Marked all as read`), 'check')
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to mark all requests as read`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={_(msg`Mark all as read`)}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={() => markAllRead()}>
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Mark all as read</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user