From d0ad7819bf300f1d4c2e92a52f6c329f8f109190 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 29 May 2026 22:46:15 +0300 Subject: [PATCH] [Chat] Add chat disabled banner (#10573) --- src/components/dms/dialogs/NewChatDialog.tsx | 21 +++-- src/screens/Messages/ChatList.tsx | 88 +++++++++++++------ .../Messages/components/ChatDisabled.tsx | 16 ++-- .../splitView/MessagesSplitViewLayout.tsx | 36 ++++++-- src/state/queries/messages/get-status.ts | 27 ++++++ 5 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 src/state/queries/messages/get-status.ts diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index 4ab5a16421..84b1d3346f 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -10,6 +10,7 @@ import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat' 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 {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' @@ -30,6 +31,8 @@ export function NewChat({ const {t: l} = useLingui() const ax = useAnalytics() const requireEmailVerification = useRequireEmailVerification() + const {data: chatStatus} = useChatActorStatusQuery() + const chatDisabled = !!chatStatus?.chatDisabled const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) @@ -153,14 +156,16 @@ export function NewChat({ return ( <> - } - accessibilityRole="button" - accessibilityLabel={l`New chat`} - accessibilityHint="" - /> + {!chatDisabled && ( + } + accessibilityRole="button" + accessibilityLabel={l`New chat`} + accessibilityHint="" + /> + )} @@ -171,8 +180,8 @@ export function MessagesScreenInner({navigation, route}: Props) { return ( -
- +
+ ) @@ -181,9 +190,11 @@ export function MessagesScreenInner({navigation, route}: Props) { export function ChatList({ selectedChat, newChatControl, + chatStatus, }: { selectedChat?: string newChatControl: DialogControlProps + chatStatus: ChatStatus | undefined }) { const t = useTheme() const {t: l} = useLingui() @@ -362,14 +373,18 @@ export function ChatList({ textStyle={t.atoms.text} iconColor={t.atoms.text.color} iconSize="4xl" - button={{ - label: l`New chat`, - text: l`New chat`, - onPress: wrappedOpenChatControl, - size: 'small', - color: 'primary', - icon: MessagePlusIcon, - }} + button={ + chatStatus?.chatDisabled + ? undefined + : { + label: l`New chat`, + text: l`New chat`, + onPress: wrappedOpenChatControl, + size: 'small', + color: 'primary', + icon: MessagePlusIcon, + } + } style={[a.h_full, {paddingTop: '20%'}]} /> )} @@ -388,6 +403,11 @@ export function ChatList({ refreshing={isPTRing} onRefresh={() => void onRefresh()} onEndReached={() => void onEndReached()} + ListHeaderComponent={ + chatStatus?.chatDisabled ? ( + + ) : undefined + } ListFooterComponent={ ) } -export function Header({newChatControl}: {newChatControl: DialogControlProps}) { +export function Header({ + newChatControl, + chatStatus, +}: { + newChatControl: DialogControlProps + chatStatus: ChatStatus | undefined +}) { const {t: l} = useLingui() const {gtMobile} = useBreakpoints() const requireEmailVerification = useRequireEmailVerification() @@ -476,14 +504,16 @@ export function Header({newChatControl}: {newChatControl: DialogControlProps}) { style={[a.justify_center]}> - + {!chatStatus?.chatDisabled && ( + + )} ) : ( diff --git a/src/screens/Messages/components/ChatDisabled.tsx b/src/screens/Messages/components/ChatDisabled.tsx index 866f0029d7..8095c1e0b9 100644 --- a/src/screens/Messages/components/ChatDisabled.tsx +++ b/src/screens/Messages/components/ChatDisabled.tsx @@ -1,5 +1,5 @@ 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 {Trans, useLingui} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' @@ -15,19 +15,23 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' -export function ChatDisabled() { +export function ChatDisabled({ + shape = 'pill', + style, +}: { + shape?: 'pill' | 'banner' + style?: StyleProp +}) { const t = useTheme() return ( - + } -function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) { - const {rightNavVisible, centerColumnOffset} = useLayoutBreakpoints() +function MessagesSplitViewLayout({children, ...props}: LayoutProps) { + const {rightNavVisible} = useLayoutBreakpoints() + const aa = useAgeAssurance() + + if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) { + return children + } + + return ( + + {children} + + ) +} + +function MessagesSplitViewLayoutInner({ + children, + navigation, + route, +}: LayoutProps) { + const {centerColumnOffset} = useLayoutBreakpoints() const newChatControl = useDialogControl() const t = useTheme() - const aa = useAgeAssurance() const isFocused = useIsFocused() + const {data: chatStatus} = useChatActorStatusQuery() const onLeftColumnScroll = useCallback((e: ReanimatedScrollEvent) => { 'worklet' splitViewLeftScroll.current = e.contentOffset.y }, []) - if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) { - return children - } - const onNewChat = (conversation: string) => navigation.navigate('MessagesConversation', {conversation}) @@ -102,11 +118,15 @@ function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) { t.atoms.border_contrast_low, {width: leftColumnWidth}, ]}> - + diff --git a/src/state/queries/messages/get-status.ts b/src/state/queries/messages/get-status.ts new file mode 100644 index 0000000000..2bfb12550a --- /dev/null +++ b/src/state/queries/messages/get-status.ts @@ -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, + }) +}