[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
+2 -1
View File
@@ -811,14 +811,15 @@ export function ButtonIcon({
* also so that we can calculate transforms.
*/
const iconSize = {
'2xs': 8,
xs: 12,
sm: 16,
md: 18,
lg: 24,
xl: 28,
'2xs': 8,
'2xl': 32,
'3xl': 40,
'4xl': 48,
}[iconSizeShorthand]
/*
+14 -6
View File
@@ -7,6 +7,7 @@ import {useNavigation} from '@react-navigation/native'
import {HITSLOP_30} from '#/lib/constants'
import {type NavigationProp} from '#/lib/routes/types'
import {useSetDrawerOpen} from '#/state/shell'
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
import {
atoms as a,
platform,
@@ -46,6 +47,7 @@ export function Outer({
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {centerColumnOffset} = useLayoutBreakpoints()
const {isWithinSplitView} = useIsWithinSplitView()
return (
<View
@@ -64,12 +66,13 @@ export function Outer({
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
!isWithinOffsetView && {
transform: [
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
!isWithinOffsetView &&
!isWithinSplitView && {
transform: [
{translateX: centerColumnOffset ? CENTER_COLUMN_OFFSET : 0},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
]}>
{children}
</View>
@@ -108,6 +111,7 @@ export function Slot({children}: {children?: React.ReactNode}) {
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {isWithinRightPanel} = useIsWithinSplitView()
const onPressBack = useCallback(
(evt: GestureResponderEvent) => {
@@ -122,6 +126,10 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
[onPress, navigation],
)
if (isWithinRightPanel) {
return null
}
return (
<Slot>
<Button
+37 -64
View File
@@ -1,15 +1,10 @@
import {forwardRef, memo, useContext, useMemo} from 'react'
import {
type StyleProp,
StyleSheet,
View,
type ViewProps,
type ViewStyle,
} from 'react-native'
import {
KeyboardAwareScrollView,
type KeyboardAwareScrollViewProps,
} from 'react-native-keyboard-controller'
import Animated, {
type AnimatedScrollViewProps,
useAnimatedProps,
@@ -18,6 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
import {
atoms as a,
useBreakpoints,
@@ -49,14 +45,20 @@ export const Screen = memo(function Screen({
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
const {isWithinSplitView} = useIsWithinSplitView()
useEnableMinimalShellModeForScreen({enabled: minimalShell})
return (
<>
{IS_WEB && <WebCenterBorders />}
{IS_WEB && !isWithinSplitView && <WebCenterBorders />}
<View
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
style={[
a.util_screen_outer,
{paddingTop: noInsetTop ? 0 : top},
isWithinSplitView && {maxHeight: '100%'},
style,
]}
{...props}
/>
</>
@@ -85,6 +87,7 @@ export const Content = memo(
) {
const t = useTheme()
const {footerHeight} = useShellLayout()
const {isWithinSplitView} = useIsWithinSplitView()
const animatedProps = useAnimatedProps(() => {
return {
scrollIndicatorInsets: {
@@ -103,11 +106,18 @@ export const Content = memo(
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
// sets the scroll inset to the height of the footer
animatedProps={animatedProps}
style={[scrollViewStyles.common, style]}
contentContainerStyle={[
scrollViewStyles.contentContainer,
contentContainerStyle,
style={[
a.w_full,
isWithinSplitView &&
web({
flex: 1,
overflowY: 'scroll',
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
}),
style,
]}
contentContainerStyle={[contentContainerStyle]}
{...props}>
{IS_WEB ? (
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
@@ -122,45 +132,6 @@ export const Content = memo(
}),
)
const scrollViewStyles = StyleSheet.create({
common: {
width: '100%',
},
contentContainer: {
paddingBottom: 100,
},
})
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
children: React.ReactNode
contentContainerStyle?: StyleProp<ViewStyle>
}
/**
* Default scroll view for simple pages.
*
* BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
*/
export const KeyboardAwareContent = memo(function LayoutKeyboardAwareContent({
children,
style,
contentContainerStyle,
...props
}: KeyboardAwareContentProps) {
return (
<KeyboardAwareScrollView
style={[scrollViewStyles.common, style]}
contentContainerStyle={[
scrollViewStyles.contentContainer,
contentContainerStyle,
]}
keyboardShouldPersistTaps="handled"
{...props}>
{IS_WEB ? <Center>{children}</Center> : children}
</KeyboardAwareScrollView>
)
})
/**
* Utility component to center content within the screen
*/
@@ -174,28 +145,30 @@ export const Center = memo(function LayoutCenter({
const {gtMobile} = useBreakpoints()
const {centerColumnOffset} = useLayoutBreakpoints()
const {isWithinDialog} = useDialogContext()
const {isWithinSplitView} = useIsWithinSplitView()
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
return (
<View
style={[
a.w_full,
a.mx_auto,
!isWithinSplitView && a.mx_auto,
gtMobile && {
maxWidth: 600,
},
!isWithinOffsetView && {
transform: [
{
translateX:
centerColumnOffset &&
!ignoreTabletLayoutOffset &&
!isWithinDialog
? CENTER_COLUMN_OFFSET
: 0,
},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
!isWithinOffsetView &&
!isWithinSplitView && {
transform: [
{
translateX:
centerColumnOffset &&
!ignoreTabletLayoutOffset &&
!isWithinDialog
? CENTER_COLUMN_OFFSET
: 0,
},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
style,
]}
{...props}>
+10 -1
View File
@@ -1,4 +1,4 @@
import {createSinglePathSVG} from './TEMPLATE'
import {createMultiPathSVG, createSinglePathSVG} from './TEMPLATE'
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
@@ -15,3 +15,12 @@ export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({
export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z',
})
export const BubbleSmile_Stroke2_Corner2_Rounded_Large = createMultiPathSVG({
viewBox: '0 0 64 64',
paths: [
'M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51c.094-.384.046-.809-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z',
'M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z',
'M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z',
],
})
+10
View File
@@ -0,0 +1,10 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Inbox_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M19 14h-2.417a5 5 0 0 1-9.166 0H5v4a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4Zm0-8a1 1 0 0 0-.898-.995L18 5H6a1 1 0 0 0-1 1v6h3.126a1 1 0 0 1 .969.751 3.001 3.001 0 0 0 5.81 0l.056-.16a1 1 0 0 1 .913-.591H19V6Zm2 12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12l.154.004A3 3 0 0 1 21 6v12Z',
})
export const Inbox_Stroke2_Corner2_Rounded_Large = createSinglePathSVG({
viewBox: '0 0 64 64',
path: 'M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001c.547.957.834 2.04.834 3.142v11.917Z',
})
+9 -23
View File
@@ -51,7 +51,7 @@ export function createSinglePathSVG({
fill="none"
{...rest}
ref={ref}
viewBox={viewBox || '0 0 24 24'}
viewBox={viewBox ?? '0 0 24 24'}
width={size}
height={size}
style={[style]}>
@@ -71,7 +71,13 @@ export function createSinglePathSVG({
})
}
export function createSinglePathSVG2({path}: {path: string}) {
export function createMultiPathSVG({
paths,
viewBox,
}: {
paths: string[]
viewBox?: string
}) {
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
@@ -80,27 +86,7 @@ export function createSinglePathSVG2({path}: {path: string}) {
fill="none"
{...rest}
ref={ref}
viewBox="0 0 24 24"
width={size}
height={size}
style={style}>
{gradient}
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
</Svg>
)
})
}
export function createMultiPathSVG({paths}: {paths: string[]}) {
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
return (
<Svg
fill="none"
{...rest}
ref={ref}
viewBox="0 0 24 24"
viewBox={viewBox ?? '0 0 24 24'}
width={size}
height={size}
style={[style]}>
+1
View File
@@ -21,6 +21,7 @@ export const sizes = {
xl: 28,
'2xl': 32,
'3xl': 48,
'4xl': 64,
} as const
export function useCommonSVGProps(props: Props) {