[Chat] Add chat disabled banner (#10573)

This commit is contained in:
Samuel Newman
2026-05-29 22:46:15 +03:00
committed by GitHub
parent b0708dcefb
commit d0ad7819bf
5 changed files with 137 additions and 51 deletions
+13 -8
View File
@@ -10,6 +10,7 @@ import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat' import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -30,6 +31,8 @@ export function NewChat({
const {t: l} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const requireEmailVerification = useRequireEmailVerification() const requireEmailVerification = useRequireEmailVerification()
const {data: chatStatus} = useChatActorStatusQuery()
const chatDisabled = !!chatStatus?.chatDisabled
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
@@ -153,14 +156,16 @@ export function NewChat({
return ( return (
<> <>
<FAB {!chatDisabled && (
testID="newChatFAB" <FAB
onPress={wrappedOnPress} testID="newChatFAB"
icon={<NewChatIcon size="lg" fill={t.palette.white} />} onPress={wrappedOnPress}
accessibilityRole="button" icon={<NewChatIcon size="lg" fill={t.palette.white} />}
accessibilityLabel={l`New chat`} accessibilityRole="button"
accessibilityHint="" accessibilityLabel={l`New chat`}
/> accessibilityHint=""
/>
)}
<Dialog.Outer <Dialog.Outer
control={control} control={control}
testID="newChatDialog" testID="newChatDialog"
+59 -29
View File
@@ -1,7 +1,7 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated' import {useAnimatedRef} from 'react-native-reanimated'
import {type ChatBskyConvoDefs} from '@atproto/api' import {type ChatBskyActorGetStatus, type ChatBskyConvoDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -15,6 +15,7 @@ import {logger} from '#/logger'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {useLeftConvos} from '#/state/queries/messages/leave-conversation' import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations' import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {EmptyState} from '#/view/com/util/EmptyState' import {EmptyState} from '#/view/com/util/EmptyState'
@@ -42,11 +43,14 @@ import {ListFooter} from '#/components/Lists'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance' import {useAgeAssurance} from '#/ageAssurance'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {ChatDisabled} from './components/ChatDisabled'
import {ChatListItem} from './components/ChatListItem' import {ChatListItem} from './components/ChatListItem'
import {InboxRequests} from './components/InboxRequests' import {InboxRequests} from './components/InboxRequests'
import {useIsWithinSplitView} from './components/splitView/context' import {useIsWithinSplitView} from './components/splitView/context'
import {splitViewLeftScroll} from './components/splitView/leftColumnScroll' import {splitViewLeftScroll} from './components/splitView/leftColumnScroll'
type ChatStatus = ChatBskyActorGetStatus.OutputSchema
type ListItem = { type ListItem = {
type: 'CONVERSATION' type: 'CONVERSATION'
conversation: ChatBskyConvoDefs.ConvoView conversation: ChatBskyConvoDefs.ConvoView
@@ -95,6 +99,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
const {t: l} = useLingui() const {t: l} = useLingui()
const t = useTheme() const t = useTheme()
const newChatControl = useDialogControl() const newChatControl = useDialogControl()
const {data: chatStatus} = useChatActorStatusQuery()
const pushToConversation = route.params?.pushToConversation const pushToConversation = route.params?.pushToConversation
// Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on // Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on
@@ -154,14 +159,18 @@ export function MessagesScreenInner({navigation, route}: Props) {
textStyle={t.atoms.text} textStyle={t.atoms.text}
iconColor={t.atoms.text.color} iconColor={t.atoms.text.color}
iconSize="4xl" iconSize="4xl"
button={{ button={
label: l`New chat`, chatStatus?.chatDisabled
text: l`New chat`, ? undefined
onPress: wrappedOpenChatControl, : {
size: 'small', label: l`New chat`,
color: 'primary', text: l`New chat`,
icon: MessagePlusIcon, onPress: wrappedOpenChatControl,
}} size: 'small',
color: 'primary',
icon: MessagePlusIcon,
}
}
style={[a.h_full, a.justify_center, a.pb_5xl]} style={[a.h_full, a.justify_center, a.pb_5xl]}
/> />
<NewChat onNewChat={onNewChat} control={newChatControl} /> <NewChat onNewChat={onNewChat} control={newChatControl} />
@@ -171,8 +180,8 @@ export function MessagesScreenInner({navigation, route}: Props) {
return ( return (
<Layout.Screen testID="messagesScreen"> <Layout.Screen testID="messagesScreen">
<Header newChatControl={newChatControl} /> <Header newChatControl={newChatControl} chatStatus={chatStatus} />
<ChatList newChatControl={newChatControl} /> <ChatList newChatControl={newChatControl} chatStatus={chatStatus} />
<NewChat onNewChat={onNewChat} control={newChatControl} /> <NewChat onNewChat={onNewChat} control={newChatControl} />
</Layout.Screen> </Layout.Screen>
) )
@@ -181,9 +190,11 @@ export function MessagesScreenInner({navigation, route}: Props) {
export function ChatList({ export function ChatList({
selectedChat, selectedChat,
newChatControl, newChatControl,
chatStatus,
}: { }: {
selectedChat?: string selectedChat?: string
newChatControl: DialogControlProps newChatControl: DialogControlProps
chatStatus: ChatStatus | undefined
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -362,14 +373,18 @@ export function ChatList({
textStyle={t.atoms.text} textStyle={t.atoms.text}
iconColor={t.atoms.text.color} iconColor={t.atoms.text.color}
iconSize="4xl" iconSize="4xl"
button={{ button={
label: l`New chat`, chatStatus?.chatDisabled
text: l`New chat`, ? undefined
onPress: wrappedOpenChatControl, : {
size: 'small', label: l`New chat`,
color: 'primary', text: l`New chat`,
icon: MessagePlusIcon, onPress: wrappedOpenChatControl,
}} size: 'small',
color: 'primary',
icon: MessagePlusIcon,
}
}
style={[a.h_full, {paddingTop: '20%'}]} style={[a.h_full, {paddingTop: '20%'}]}
/> />
)} )}
@@ -388,6 +403,11 @@ export function ChatList({
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={() => void onRefresh()} onRefresh={() => void onRefresh()}
onEndReached={() => void onEndReached()} onEndReached={() => void onEndReached()}
ListHeaderComponent={
chatStatus?.chatDisabled ? (
<ChatDisabled shape="banner" style={[isWithinSplitView && a.mb_sm]} />
) : undefined
}
ListFooterComponent={ ListFooterComponent={
<ListFooter <ListFooter
isFetchingNextPage={isFetchingNextPage} isFetchingNextPage={isFetchingNextPage}
@@ -413,12 +433,20 @@ export function ChatList({
}), }),
] ]
} }
contentContainerStyle={isWithinSplitView && a.py_sm} contentContainerStyle={
isWithinSplitView && !chatStatus?.chatDisabled && a.py_sm
}
/> />
) )
} }
export function Header({newChatControl}: {newChatControl: DialogControlProps}) { export function Header({
newChatControl,
chatStatus,
}: {
newChatControl: DialogControlProps
chatStatus: ChatStatus | undefined
}) {
const {t: l} = useLingui() const {t: l} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const requireEmailVerification = useRequireEmailVerification() const requireEmailVerification = useRequireEmailVerification()
@@ -476,14 +504,16 @@ export function Header({newChatControl}: {newChatControl: DialogControlProps}) {
style={[a.justify_center]}> style={[a.justify_center]}>
<ButtonIcon icon={SettingsIcon} /> <ButtonIcon icon={SettingsIcon} />
</Link> </Link>
<Button {!chatStatus?.chatDisabled && (
label={l`New chat`} <Button
color="primary" label={l`New chat`}
size="small" color="primary"
shape="round" size="small"
onPress={wrappedOpenChatControl}> shape="round"
<ButtonIcon icon={NewChatIcon} /> onPress={wrappedOpenChatControl}>
</Button> <ButtonIcon icon={NewChatIcon} />
</Button>
)}
</View> </View>
</> </>
) : ( ) : (
@@ -1,5 +1,5 @@
import {useCallback, useState} from 'react' import {useCallback, useState} from 'react'
import {View} from 'react-native' import {type StyleProp, View, type ViewStyle} from 'react-native'
import {ToolsOzoneReportDefs} from '@atproto/api' import {ToolsOzoneReportDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useMutation} from '@tanstack/react-query' import {useMutation} from '@tanstack/react-query'
@@ -15,19 +15,23 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function ChatDisabled() { export function ChatDisabled({
shape = 'pill',
style,
}: {
shape?: 'pill' | 'banner'
style?: StyleProp<ViewStyle>
}) {
const t = useTheme() const t = useTheme()
return ( return (
<View style={[a.p_md]}> <View style={[shape === 'pill' && a.p_md, style]}>
<View <View
style={[ style={[
a.align_center, a.align_center,
a.justify_center, a.justify_center,
a.p_lg, a.p_lg,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
{ shape === 'pill' && {borderRadius: 40},
borderRadius: 40,
},
]}> ]}>
<WarningIcon fill={t.atoms.text.color} size="lg" style={[a.mb_xs]} /> <WarningIcon fill={t.atoms.text.color} size="lg" style={[a.mb_xs]} />
<Text <Text
@@ -6,6 +6,7 @@ import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
import {type FlatNavigatorParams} from '#/lib/routes/types' import {type FlatNavigatorParams} from '#/lib/routes/types'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {type NativeStackNavigationOptionsWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth' import {type NativeStackNavigationOptionsWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
import {LEFT_NAV_MINIMAL_WIDTH} from '#/view/shell/desktop/LeftNav' import {LEFT_NAV_MINIMAL_WIDTH} from '#/view/shell/desktop/LeftNav'
import {atoms as a, useLayoutBreakpoints, useTheme, web} from '#/alf' import {atoms as a, useLayoutBreakpoints, useTheme, web} from '#/alf'
@@ -40,22 +41,37 @@ export function renderMessagesSplitViewLayout(props: LayoutProps) {
return <MessagesSplitViewLayout {...props} /> return <MessagesSplitViewLayout {...props} />
} }
function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) { function MessagesSplitViewLayout({children, ...props}: LayoutProps) {
const {rightNavVisible, centerColumnOffset} = useLayoutBreakpoints() const {rightNavVisible} = useLayoutBreakpoints()
const aa = useAgeAssurance()
if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) {
return children
}
return (
<MessagesSplitViewLayoutInner {...props}>
{children}
</MessagesSplitViewLayoutInner>
)
}
function MessagesSplitViewLayoutInner({
children,
navigation,
route,
}: LayoutProps) {
const {centerColumnOffset} = useLayoutBreakpoints()
const newChatControl = useDialogControl() const newChatControl = useDialogControl()
const t = useTheme() const t = useTheme()
const aa = useAgeAssurance()
const isFocused = useIsFocused() const isFocused = useIsFocused()
const {data: chatStatus} = useChatActorStatusQuery()
const onLeftColumnScroll = useCallback((e: ReanimatedScrollEvent) => { const onLeftColumnScroll = useCallback((e: ReanimatedScrollEvent) => {
'worklet' 'worklet'
splitViewLeftScroll.current = e.contentOffset.y splitViewLeftScroll.current = e.contentOffset.y
}, []) }, [])
if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) {
return children
}
const onNewChat = (conversation: string) => const onNewChat = (conversation: string) =>
navigation.navigate('MessagesConversation', {conversation}) navigation.navigate('MessagesConversation', {conversation})
@@ -102,11 +118,15 @@ function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) {
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
{width: leftColumnWidth}, {width: leftColumnWidth},
]}> ]}>
<ChatListHeader newChatControl={newChatControl} /> <ChatListHeader
newChatControl={newChatControl}
chatStatus={chatStatus}
/>
<ScrollProvider onScroll={onLeftColumnScroll}> <ScrollProvider onScroll={onLeftColumnScroll}>
<ChatList <ChatList
newChatControl={newChatControl} newChatControl={newChatControl}
selectedChat={selectedChat} selectedChat={selectedChat}
chatStatus={chatStatus}
/> />
</ScrollProvider> </ScrollProvider>
<NewChat onNewChat={onNewChat} control={newChatControl} /> <NewChat onNewChat={onNewChat} control={newChatControl} />
+27
View File
@@ -0,0 +1,27 @@
import {useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useAgent} from '#/state/session'
import {STALE} from '..'
import {createQueryKey} from '../util'
const chatActorStatusQueryKey = () =>
createQueryKey('chat-actor-status', {}, {persistedVersion: 1})
export function useChatActorStatusQuery() {
const agent = useAgent()
return useQuery({
queryKey: chatActorStatusQueryKey(),
queryFn: async () => {
const {data} = await agent.chat.bsky.actor.getStatus(
{},
{headers: DM_SERVICE_HEADERS},
)
return data
},
staleTime: STALE.INFINITY,
gcTime: STALE.INFINITY,
})
}