add split view layout to chat
This commit is contained in:
@@ -143,6 +143,7 @@ import {setNavigationMetadata} from '#/analytics/metadata'
|
|||||||
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
import {renderMessagesSplitViewLayout} from './screens/Messages/components/splitView/MessagesSplitViewLayout'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
@@ -564,6 +565,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => TopicScreen}
|
getComponent={() => TopicScreen}
|
||||||
options={{title: title(msg`Topic`)}}
|
options={{title: title(msg`Topic`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Group screenLayout={renderMessagesSplitViewLayout}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="MessagesConversation"
|
name="MessagesConversation"
|
||||||
getComponent={() => MessagesConversationScreen}
|
getComponent={() => MessagesConversationScreen}
|
||||||
@@ -584,6 +586,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => MessagesInboxScreen}
|
getComponent={() => MessagesInboxScreen}
|
||||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
</Stack.Group>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="NotificationsActivityList"
|
name="NotificationsActivityList"
|
||||||
getComponent={() => NotificationsActivityListScreen}
|
getComponent={() => NotificationsActivityListScreen}
|
||||||
@@ -832,6 +835,7 @@ const FlatNavigator = ({
|
|||||||
name="Messages"
|
name="Messages"
|
||||||
getComponent={() => MessagesScreen}
|
getComponent={() => MessagesScreen}
|
||||||
options={{title: title(msg`Messages`), requireAuth: true}}
|
options={{title: title(msg`Messages`), requireAuth: true}}
|
||||||
|
layout={renderMessagesSplitViewLayout}
|
||||||
/>
|
/>
|
||||||
<Flat.Screen
|
<Flat.Screen
|
||||||
name="Start"
|
name="Start"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
platform,
|
platform,
|
||||||
@@ -46,6 +47,7 @@ export function Outer({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -64,7 +66,8 @@ export function Outer({
|
|||||||
}),
|
}),
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||||
!isWithinOffsetView && {
|
!isWithinOffsetView &&
|
||||||
|
!isWithinSplitView && {
|
||||||
transform: [
|
transform: [
|
||||||
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
|
||||||
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
@@ -108,6 +111,7 @@ export function Slot({children}: {children?: React.ReactNode}) {
|
|||||||
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {isWithinRightPanel} = useIsWithinSplitView()
|
||||||
|
|
||||||
const onPressBack = useCallback(
|
const onPressBack = useCallback(
|
||||||
(evt: GestureResponderEvent) => {
|
(evt: GestureResponderEvent) => {
|
||||||
@@ -122,6 +126,10 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
|||||||
[onPress, navigation],
|
[onPress, navigation],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isWithinRightPanel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot>
|
<Slot>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
|
|
||||||
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
@@ -49,12 +50,13 @@ export const Screen = memo(function Screen({
|
|||||||
...props
|
...props
|
||||||
}: ScreenProps) {
|
}: ScreenProps) {
|
||||||
const {top} = useSafeAreaInsets()
|
const {top} = useSafeAreaInsets()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IS_WEB && <WebCenterBorders />}
|
{IS_WEB && !isWithinSplitView && <WebCenterBorders />}
|
||||||
<View
|
<View
|
||||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -174,16 +176,18 @@ export const Center = memo(function LayoutCenter({
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
const {isWithinDialog} = useDialogContext()
|
const {isWithinDialog} = useDialogContext()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.mx_auto,
|
!isWithinSplitView && a.mx_auto,
|
||||||
gtMobile && {
|
gtMobile && {
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
},
|
},
|
||||||
!isWithinOffsetView && {
|
!isWithinOffsetView &&
|
||||||
|
!isWithinSplitView && {
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateX:
|
translateX:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
|
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
@@ -25,6 +26,7 @@ export function NewChat({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
|
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
|
||||||
|
|
||||||
@@ -99,6 +101,8 @@ export function NewChat({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* in split view, header button is always available, so no need for FAB */}
|
||||||
|
{!isWithinSplitView && (
|
||||||
<FAB
|
<FAB
|
||||||
testID="newChatFAB"
|
testID="newChatFAB"
|
||||||
onPress={wrappedOnPress}
|
onPress={wrappedOnPress}
|
||||||
@@ -107,6 +111,7 @@ export function NewChat({
|
|||||||
accessibilityLabel={l`New chat`}
|
accessibilityLabel={l`New chat`}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
testID="newChatDialog"
|
testID="newChatDialog"
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
MyProfileTab: undefined
|
MyProfileTab: undefined
|
||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
|
|||||||
+106
-102
@@ -2,9 +2,7 @@ import {useCallback, useEffect, useMemo, 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 ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} 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'
|
||||||
|
|
||||||
@@ -19,9 +17,10 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
|||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
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 {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -41,14 +40,16 @@ import {useAgeAssurance} from '#/ageAssurance'
|
|||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
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'
|
||||||
|
|
||||||
type ListItem = {
|
type ListItem = {
|
||||||
type: 'CONVERSATION'
|
type: 'CONVERSATION'
|
||||||
conversation: ChatBskyConvoDefs.ConvoView
|
conversation: ChatBskyConvoDefs.ConvoView
|
||||||
|
selected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({item}: {item: ListItem}) {
|
function renderItem({item}: {item: ListItem}) {
|
||||||
return <ChatListItem convo={item.conversation} />
|
return <ChatListItem convo={item.conversation} selected={item.selected} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyExtractor(item: ListItem) {
|
function keyExtractor(item: ListItem) {
|
||||||
@@ -58,19 +59,19 @@ function keyExtractor(item: ListItem) {
|
|||||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
||||||
|
|
||||||
export function MessagesScreen(props: Props) {
|
export function MessagesScreen(props: Props) {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const aaCopy = useAgeAssuranceCopy()
|
const aaCopy = useAgeAssuranceCopy()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgeRestrictedScreen
|
<AgeRestrictedScreen
|
||||||
screenTitle={_(msg`Chats`)}
|
screenTitle={l`Chats`}
|
||||||
infoText={aaCopy.chatsInfoText}
|
infoText={aaCopy.chatsInfoText}
|
||||||
rightHeaderSlot={
|
rightHeaderSlot={
|
||||||
aa.flags.chatDisabled ? null : (
|
aa.flags.chatDisabled ? null : (
|
||||||
<Link
|
<Link
|
||||||
to="/messages/settings"
|
to="/messages/settings"
|
||||||
label={_(msg`Chat settings`)}
|
label={l`Chat settings`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary">
|
color="secondary">
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
@@ -85,10 +86,9 @@ export function MessagesScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessagesScreenInner({navigation, route}: Props) {
|
export function MessagesScreenInner({navigation, route}: Props) {
|
||||||
const {_} = useLingui()
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
const t = useTheme()
|
const {t: l} = useLingui()
|
||||||
const newChatControl = useDialogControl()
|
const newChatControl = useDialogControl()
|
||||||
const scrollElRef: ListRef = useAnimatedRef()
|
|
||||||
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
|
||||||
@@ -120,6 +120,48 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
}, [messagesBus, isActive]),
|
}, [messagesBus, isActive]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onNewChat = useCallback(
|
||||||
|
(conversation: string) =>
|
||||||
|
navigation.navigate('MessagesConversation', {conversation}),
|
||||||
|
[navigation],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isWithinSplitView) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EmptyState
|
||||||
|
message={l`Start a conversation`}
|
||||||
|
icon={MessageIcon}
|
||||||
|
iconSize="3xl"
|
||||||
|
button={{
|
||||||
|
label: l`New chat`,
|
||||||
|
text: l`New chat`,
|
||||||
|
onPress: newChatControl.open,
|
||||||
|
size: 'small',
|
||||||
|
color: 'secondary_inverted',
|
||||||
|
}}
|
||||||
|
style={[a.h_full, a.justify_center, a.pb_5xl]}
|
||||||
|
/>
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="messagesScreen">
|
||||||
|
<Header newChatControl={newChatControl} />
|
||||||
|
<ChatList />
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatList({selectedChat}: {selectedChat?: string}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const scrollElRef: ListRef = useAnimatedRef()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
|
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
|
||||||
@@ -134,11 +176,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useListConvosQuery({status: 'accepted'})
|
} = useListConvosQuery({status: 'accepted'})
|
||||||
|
|
||||||
const {
|
const {refetch: refetchInbox} = useListConvosQuery({
|
||||||
data: inboxData,
|
|
||||||
refetch: refetchInbox,
|
|
||||||
hasNextPage: hasMoreRequests,
|
|
||||||
} = useListConvosQuery({
|
|
||||||
status: 'request',
|
status: 'request',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,16 +185,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
|
|
||||||
const leftConvos = useLeftConvos()
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
const inboxAllConvos =
|
|
||||||
inboxData?.pages
|
|
||||||
.flatMap(page => page.convos)
|
|
||||||
.filter(
|
|
||||||
convo =>
|
|
||||||
!leftConvos.includes(convo.id) &&
|
|
||||||
!convo.muted &&
|
|
||||||
convo.members.every(member => member.handle !== 'missing.invalid'),
|
|
||||||
) ?? []
|
|
||||||
|
|
||||||
const conversations = useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
const conversations = data.pages
|
const conversations = data.pages
|
||||||
@@ -164,18 +192,17 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
// filter out convos that are actively being left
|
// filter out convos that are actively being left
|
||||||
.filter(convo => !leftConvos.includes(convo.id))
|
.filter(convo => !leftConvos.includes(convo.id))
|
||||||
|
|
||||||
return [
|
return conversations.map(
|
||||||
...conversations.map(
|
|
||||||
convo =>
|
convo =>
|
||||||
({
|
({
|
||||||
type: 'CONVERSATION',
|
type: 'CONVERSATION',
|
||||||
conversation: convo,
|
conversation: convo,
|
||||||
|
selected: convo.id === selectedChat,
|
||||||
}) as const,
|
}) as const,
|
||||||
),
|
) satisfies ListItem[]
|
||||||
] satisfies ListItem[]
|
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data, leftConvos])
|
}, [data, leftConvos, selectedChat])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
@@ -196,12 +223,6 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
}
|
}
|
||||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||||
|
|
||||||
const onNewChat = useCallback(
|
|
||||||
(conversation: string) =>
|
|
||||||
navigation.navigate('MessagesConversation', {conversation}),
|
|
||||||
[navigation],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSoftReset = useCallback(async () => {
|
const onSoftReset = useCallback(async () => {
|
||||||
scrollElRef.current?.scrollToOffset({
|
scrollElRef.current?.scrollToOffset({
|
||||||
animated: IS_NATIVE,
|
animated: IS_NATIVE,
|
||||||
@@ -222,20 +243,8 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
return listenSoftReset(() => void onSoftReset())
|
return listenSoftReset(() => void onSoftReset())
|
||||||
}, [onSoftReset, isScreenFocused])
|
}, [onSoftReset, isScreenFocused])
|
||||||
|
|
||||||
// NOTE(APiligrim)
|
if (conversations.length === 0) {
|
||||||
// Show empty state only if there are no conversations at all
|
|
||||||
const activeConversations = conversations.filter(
|
|
||||||
item => item.type === 'CONVERSATION',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (activeConversations.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
|
||||||
<Header
|
|
||||||
newChatControl={newChatControl}
|
|
||||||
requestsCount={inboxAllConvos.length}
|
|
||||||
hasMoreRequests={hasMoreRequests}
|
|
||||||
/>
|
|
||||||
<Layout.Center>
|
<Layout.Center>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ChatListLoadingPlaceholder />
|
<ChatListLoadingPlaceholder />
|
||||||
@@ -261,63 +270,40 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
{maxWidth: 360},
|
{maxWidth: 360},
|
||||||
]}>
|
]}>
|
||||||
{cleanError(error) ||
|
{cleanError(error) || l`Failed to load conversations`}
|
||||||
_(msg`Failed to load conversations`)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Reload conversations`)}
|
label={l`Reload conversations`}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
variant="solid"
|
|
||||||
onPress={() => void refetch()}>
|
onPress={() => void refetch()}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={RetryIcon} position="right" />
|
<ButtonIcon icon={RetryIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
) : isWithinSplitView ? (
|
||||||
|
<EmptyState
|
||||||
|
message={l`Your conversations will appear here`}
|
||||||
|
icon={MessageIcon}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<EmptyState
|
||||||
<View style={[a.pt_3xl, a.align_center]}>
|
message={l`No chats yet`}
|
||||||
<MessageIcon width={48} fill={t.palette.primary_500} />
|
icon={MessageIcon}
|
||||||
<Text
|
iconSize="3xl"
|
||||||
style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}>
|
/>
|
||||||
<Trans>Nothing here</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.pb_xl,
|
|
||||||
a.text_center,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>You have no conversations yet. Start one!</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Center>
|
</Layout.Center>
|
||||||
|
|
||||||
{!isLoading && !isError && (
|
|
||||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
|
||||||
)}
|
|
||||||
</Layout.Screen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="messagesScreen">
|
|
||||||
<Header
|
|
||||||
newChatControl={newChatControl}
|
|
||||||
requestsCount={inboxAllConvos.length}
|
|
||||||
hasMoreRequests={hasMoreRequests}
|
|
||||||
/>
|
|
||||||
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
|
||||||
<List
|
<List
|
||||||
ref={scrollElRef}
|
ref={scrollElRef}
|
||||||
data={conversations}
|
data={conversations}
|
||||||
@@ -340,23 +326,39 @@ export function MessagesScreenInner({navigation, route}: Props) {
|
|||||||
windowSize={11}
|
windowSize={11}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
|
disableFullWindowScroll={isWithinSplitView}
|
||||||
|
style={
|
||||||
|
isWithinSplitView && [
|
||||||
|
a.w_full,
|
||||||
|
web({
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Layout.Screen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header({
|
export function Header({newChatControl}: {newChatControl: DialogControlProps}) {
|
||||||
newChatControl,
|
const {t: l} = useLingui()
|
||||||
requestsCount,
|
|
||||||
hasMoreRequests,
|
|
||||||
}: {
|
|
||||||
newChatControl: DialogControlProps
|
|
||||||
requestsCount: number
|
|
||||||
hasMoreRequests: boolean
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
|
const {data: inboxData, hasNextPage: hasMoreRequests} = useListConvosQuery({
|
||||||
|
status: 'request',
|
||||||
|
})
|
||||||
|
|
||||||
|
const inboxAllConvos =
|
||||||
|
inboxData?.pages
|
||||||
|
.flatMap(page => page.convos)
|
||||||
|
.filter(
|
||||||
|
convo =>
|
||||||
|
!leftConvos.includes(convo.id) &&
|
||||||
|
!convo.muted &&
|
||||||
|
convo.members.every(member => member.handle !== 'missing.invalid'),
|
||||||
|
) ?? []
|
||||||
|
|
||||||
const openChatControl = useCallback(() => {
|
const openChatControl = useCallback(() => {
|
||||||
newChatControl.open()
|
newChatControl.open()
|
||||||
@@ -370,13 +372,16 @@ function Header({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const requestsLink = (
|
const requestsLink = (
|
||||||
<InboxRequests count={requestsCount} more={hasMoreRequests} />
|
<InboxRequests
|
||||||
|
count={inboxAllConvos.length}
|
||||||
|
more={hasMoreRequests ?? false}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const settingsLink = (
|
const settingsLink = (
|
||||||
<Link
|
<Link
|
||||||
to="/messages/settings"
|
to="/messages/settings"
|
||||||
label={_(msg`Chat settings`)}
|
label={l`Chat settings`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -400,12 +405,11 @@ function Header({
|
|||||||
{requestsLink}
|
{requestsLink}
|
||||||
{settingsLink}
|
{settingsLink}
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`New chat`)}
|
label={l`New chat`}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
|
||||||
onPress={wrappedOpenChatControl}>
|
onPress={wrappedOpenChatControl}>
|
||||||
<ButtonIcon icon={PlusIcon} position="left" />
|
<ButtonIcon icon={PlusIcon} />
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>New chat</Trans>
|
<Trans>New chat</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import * as Toast from '#/components/Toast'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {RequestListItem} from './components/RequestListItem'
|
import {RequestListItem} from './components/RequestListItem'
|
||||||
|
import {useIsWithinSplitView} from './components/splitView/context'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
||||||
|
|
||||||
@@ -127,6 +128,7 @@ function RequestList({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {isWithinSplitView} = useIsWithinSplitView()
|
||||||
|
|
||||||
// Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future)
|
// 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
|
// but only when the screen is active
|
||||||
@@ -217,7 +219,7 @@ function RequestList({
|
|||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Retry</Trans>
|
<Trans>Retry</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={RetryIcon} position="right" />
|
<ButtonIcon icon={RetryIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@@ -243,6 +245,7 @@ function RequestList({
|
|||||||
You don't have any chat requests at the moment.
|
You don't have any chat requests at the moment.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
{!isWithinSplitView && (
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -260,6 +263,7 @@ function RequestList({
|
|||||||
<Trans>Back to Chats</Trans>
|
<Trans>Back to Chats</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ export const ChatListItemPortal = createPortalGroup()
|
|||||||
export function ChatListItem({
|
export function ChatListItem({
|
||||||
convo: convoView,
|
convo: convoView,
|
||||||
showMenu = true,
|
showMenu = true,
|
||||||
|
selected = false,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -78,7 +80,8 @@ export function ChatListItem({
|
|||||||
<DirectChatItem
|
<DirectChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}
|
||||||
|
selected={selected}>
|
||||||
{children}
|
{children}
|
||||||
</DirectChatItem>
|
</DirectChatItem>
|
||||||
)
|
)
|
||||||
@@ -88,7 +91,8 @@ export function ChatListItem({
|
|||||||
<GroupChatItem
|
<GroupChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}
|
||||||
|
selected={selected}>
|
||||||
{children}
|
{children}
|
||||||
</GroupChatItem>
|
</GroupChatItem>
|
||||||
)
|
)
|
||||||
@@ -103,11 +107,13 @@ function DirectChatItem({
|
|||||||
convo,
|
convo,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -145,6 +151,7 @@ function DirectChatItem({
|
|||||||
: l`This conversation is with a deleted or a deactivated account. Press for options`
|
: l`This conversation is with a deleted or a deactivated account. Press for options`
|
||||||
}
|
}
|
||||||
showMenu={showMenu}
|
showMenu={showMenu}
|
||||||
|
selected={selected}
|
||||||
isDeletedAccount={isDeletedAccount}
|
isDeletedAccount={isDeletedAccount}
|
||||||
isBlockedAccount={moderation.blocked}
|
isBlockedAccount={moderation.blocked}
|
||||||
showProfileBadges
|
showProfileBadges
|
||||||
@@ -164,11 +171,13 @@ function GroupChatItem({
|
|||||||
convo,
|
convo,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -193,6 +202,7 @@ function GroupChatItem({
|
|||||||
isBlockedAccount={false}
|
isBlockedAccount={false}
|
||||||
isDeletedAccount={false}
|
isDeletedAccount={false}
|
||||||
showProfileBadges={false}
|
showProfileBadges={false}
|
||||||
|
selected={selected}
|
||||||
showMenu={showMenu}>
|
showMenu={showMenu}>
|
||||||
{children}
|
{children}
|
||||||
</BaseChatItem>
|
</BaseChatItem>
|
||||||
@@ -210,6 +220,7 @@ function BaseChatItem({
|
|||||||
primaryProfile,
|
primaryProfile,
|
||||||
primaryProfileModeration,
|
primaryProfileModeration,
|
||||||
showMenu,
|
showMenu,
|
||||||
|
selected,
|
||||||
showProfileBadges,
|
showProfileBadges,
|
||||||
postAlerts,
|
postAlerts,
|
||||||
children,
|
children,
|
||||||
@@ -224,6 +235,7 @@ function BaseChatItem({
|
|||||||
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
||||||
primaryProfileModeration?: ModerationDecision
|
primaryProfileModeration?: ModerationDecision
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
|
selected?: boolean
|
||||||
showProfileBadges: boolean
|
showProfileBadges: boolean
|
||||||
postAlerts?: React.ReactNode
|
postAlerts?: React.ReactNode
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -457,7 +469,8 @@ function BaseChatItem({
|
|||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.py_md,
|
a.py_md,
|
||||||
a.gap_md,
|
a.gap_md,
|
||||||
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
|
selected && t.atoms.bg_contrast_25,
|
||||||
|
(hovered || pressed || focused) && t.atoms.bg_contrast_50,
|
||||||
]}>
|
]}>
|
||||||
{/* Avatar goes here */}
|
{/* Avatar goes here */}
|
||||||
<View style={{width: 52, height: 52}} />
|
<View style={{width: 52, height: 52}} />
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {type ScreenLayoutArgs, useIsFocused} from '@react-navigation/native'
|
||||||
|
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||||
|
|
||||||
|
import {type AllNavigatorParams, type NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {atoms as a, useLayoutBreakpoints, useTheme, web} from '#/alf'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {NewChat} from '#/components/dms/dialogs/NewChatDialog'
|
||||||
|
import {SCROLLBAR_OFFSET} from '#/components/Layout'
|
||||||
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
|
import {IS_WEB} from '#/env'
|
||||||
|
import {ChatList, Header as ChatListHeader} from '../../ChatList'
|
||||||
|
import {SplitViewProvider} from './context'
|
||||||
|
|
||||||
|
const LEFT_NAV_MINIMAL_WIDTH = 86
|
||||||
|
const RIGHT_NAV_WIDTH = 330 + 28
|
||||||
|
|
||||||
|
type LayoutProps = ScreenLayoutArgs<
|
||||||
|
AllNavigatorParams,
|
||||||
|
'MessagesConversation',
|
||||||
|
{},
|
||||||
|
NavigationProp
|
||||||
|
>
|
||||||
|
export function renderMessagesSplitViewLayout(props: LayoutProps) {
|
||||||
|
return <MessagesSplitViewLayout {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) {
|
||||||
|
const {rightNavVisible, centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
const newChatControl = useDialogControl()
|
||||||
|
const t = useTheme()
|
||||||
|
const aa = useAgeAssurance()
|
||||||
|
const isFocused = useIsFocused()
|
||||||
|
|
||||||
|
if (!IS_WEB || !rightNavVisible || aa.state.access !== aa.Access.Full) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
const onNewChat = (conversation: string) =>
|
||||||
|
navigation.navigate('MessagesConversation', {conversation})
|
||||||
|
|
||||||
|
const selectedChat =
|
||||||
|
route.name === 'MessagesConversation'
|
||||||
|
? route?.params?.conversation
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.mx_auto,
|
||||||
|
{maxWidth: centerColumnOffset ? 900 : 950},
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX: centerColumnOffset
|
||||||
|
? LEFT_NAV_MINIMAL_WIDTH / 2
|
||||||
|
: RIGHT_NAV_WIDTH / 2,
|
||||||
|
},
|
||||||
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{isFocused && <RemoveScrollBar />}
|
||||||
|
<SplitViewProvider side="left">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border_l,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{width: centerColumnOffset ? 300 : 350},
|
||||||
|
]}>
|
||||||
|
<ChatListHeader newChatControl={newChatControl} />
|
||||||
|
<ChatList selectedChat={selectedChat} />
|
||||||
|
<NewChat onNewChat={onNewChat} control={newChatControl} />
|
||||||
|
</View>
|
||||||
|
</SplitViewProvider>
|
||||||
|
<SplitViewProvider side="right">
|
||||||
|
<View style={[a.border_x, t.atoms.border_contrast_low, {width: 600}]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</SplitViewProvider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {createContext, useContext, useMemo} from 'react'
|
||||||
|
|
||||||
|
const SplitViewContext = createContext<{
|
||||||
|
isWithinSplitView: boolean
|
||||||
|
isWithinLeftPanel: boolean
|
||||||
|
isWithinRightPanel: boolean
|
||||||
|
}>({
|
||||||
|
isWithinSplitView: false,
|
||||||
|
isWithinLeftPanel: false,
|
||||||
|
isWithinRightPanel: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function SplitViewProvider({
|
||||||
|
children,
|
||||||
|
side,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
side: 'left' | 'right'
|
||||||
|
}) {
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
isWithinSplitView: true,
|
||||||
|
isWithinLeftPanel: side === 'left',
|
||||||
|
isWithinRightPanel: side === 'right',
|
||||||
|
}),
|
||||||
|
[side],
|
||||||
|
)
|
||||||
|
return <SplitViewContext value={value}>{children}</SplitViewContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useIsWithinSplitView = () => useContext(SplitViewContext)
|
||||||
@@ -2,8 +2,6 @@ import {isValidElement} from 'react'
|
|||||||
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, type ButtonProps, ButtonText} from '#/components/Button'
|
import {Button, type ButtonProps, ButtonText} from '#/components/Button'
|
||||||
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
||||||
@@ -24,23 +22,25 @@ export function EmptyState({
|
|||||||
button,
|
button,
|
||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
icon?: React.ComponentType<any> | React.ReactElement
|
icon?: React.ComponentType<any> | React.ReactElement | null
|
||||||
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
||||||
message: string
|
message: string
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
textStyle?: StyleProp<TextStyle>
|
textStyle?: StyleProp<TextStyle>
|
||||||
button?: EmptyStateButtonProps
|
button?: EmptyStateButtonProps
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile, gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
const placeholderIcon = (
|
const placeholderIcon = (
|
||||||
<EditIcon size="2xl" fill={t.atoms.text_contrast_medium.color} />
|
<EditIcon size="2xl" fill={t.atoms.text_contrast_medium.color} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
|
if (icon === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
return placeholderIcon
|
return placeholderIcon
|
||||||
}
|
}
|
||||||
@@ -79,16 +79,14 @@ export function EmptyState({
|
|||||||
{height: 64, width: 64},
|
{height: 64, width: 64},
|
||||||
isValidElement(icon)
|
isValidElement(icon)
|
||||||
? a.bg_transparent
|
? a.bg_transparent
|
||||||
: [isTabletOrDesktop && {marginTop: 50}],
|
: [gtTablet && {marginTop: 50}],
|
||||||
]}>
|
]}>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
{
|
t.atoms.text_contrast_high,
|
||||||
color: pal.colors.textLight,
|
{maxWidth: gtMobile ? '40%' : '60%'},
|
||||||
maxWidth: gtMobile ? '40%' : '60%',
|
|
||||||
},
|
|
||||||
a.pt_xs,
|
a.pt_xs,
|
||||||
a.font_medium,
|
a.font_medium,
|
||||||
a.text_md,
|
a.text_md,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
const kawaii = useKawaiiMode()
|
const kawaii = useKawaiiMode()
|
||||||
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
const gutters = useGutters(['base', 0, 'base', 'wide'])
|
||||||
const isSearchScreen = routeName === 'Search'
|
const isSearchScreen = routeName === 'Search'
|
||||||
|
const isMessagesRelatedScreen = routeName.startsWith('Messages')
|
||||||
const webqueryParams = useWebQueryParams()
|
const webqueryParams = useWebQueryParams()
|
||||||
const searchQuery = webqueryParams?.q
|
const searchQuery = webqueryParams?.q
|
||||||
const showExploreScreenDuplicatedContent =
|
const showExploreScreenDuplicatedContent =
|
||||||
@@ -56,7 +57,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
|
const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
|
||||||
useLayoutBreakpoints()
|
useLayoutBreakpoints()
|
||||||
|
|
||||||
if (!rightNavVisible) {
|
if (!rightNavVisible || isMessagesRelatedScreen) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user