[Chat] Split view layout for web (#10258)

This commit is contained in:
Samuel Newman
2026-05-15 20:46:53 +01:00
committed by GitHub
parent 5b7bc56a12
commit 0eb382da03
35 changed files with 849 additions and 588 deletions
@@ -0,0 +1,128 @@
import {View} from 'react-native'
import {type ScreenLayoutArgs, useIsFocused} from '@react-navigation/native'
import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
import {type FlatNavigatorParams} from '#/lib/routes/types'
import {type NativeStackNavigationOptionsWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
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 {LockScroll} from '#/components/LockScroll'
import {useAgeAssurance} from '#/ageAssurance'
import {IS_WEB} from '#/env'
import {ChatList, Header as ChatListHeader} from '../../ChatList'
import {SplitViewProvider} from './context'
const CENTER_COLUMN_WIDTH = 600
const LEFT_NAV_FULL_WIDTH = 245
const LEFT_NAV_MINIMAL_WIDTH = 86
const RIGHT_NAV_FULL_WIDTH = 330
const RIGHT_NAV_MINIMAL_WIDTH = 280
type MessageScreens =
| 'Messages'
| 'MessagesConversation'
| 'MessagesConversationSettings'
| 'MessagesInbox'
| 'MessagesSettings'
type LayoutProps = ScreenLayoutArgs<
FlatNavigatorParams,
MessageScreens,
NativeStackNavigationOptionsWithAuth,
NativeStackNavigationProp<
FlatNavigatorParams,
MessageScreens,
string | undefined
>
>
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.name === 'MessagesConversationSettings') &&
route.params &&
'conversation' in route.params
? route.params.conversation
: undefined
const rightNavWidth = centerColumnOffset
? RIGHT_NAV_MINIMAL_WIDTH
: RIGHT_NAV_FULL_WIDTH
const leftNavWidth = centerColumnOffset
? LEFT_NAV_MINIMAL_WIDTH
: LEFT_NAV_FULL_WIDTH - LEFT_NAV_MINIMAL_WIDTH
// slight reduce width for smaller breakpoint
const centerColumnWidth = centerColumnOffset
? CENTER_COLUMN_WIDTH - 50
: CENTER_COLUMN_WIDTH
// nasty magic numbers here, sorry :(
const offset = centerColumnOffset
? LEFT_NAV_MINIMAL_WIDTH - 34
: LEFT_NAV_MINIMAL_WIDTH + 5
const containerWidth = leftNavWidth + centerColumnWidth + rightNavWidth
return (
<View
style={[
a.flex_1,
a.flex_row,
a.mx_auto,
{maxWidth: containerWidth},
{
transform: [
{translateX: offset},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
]}>
{isFocused && <LockScroll />}
<SplitViewProvider side="left">
<View
style={[
a.border_l,
t.atoms.border_contrast_low,
{width: containerWidth - centerColumnWidth},
]}>
<ChatListHeader newChatControl={newChatControl} />
<ChatList
newChatControl={newChatControl}
selectedChat={selectedChat}
/>
<NewChat onNewChat={onNewChat} control={newChatControl} />
</View>
</SplitViewProvider>
<SplitViewProvider side="right">
<View
style={[
a.border_x,
t.atoms.border_contrast_low,
{width: centerColumnWidth},
]}>
{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)