[Chat] Add chat disabled banner (#10573)
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user