collapse left nav, remove fab in tablet mode

This commit is contained in:
Samuel Newman
2026-04-15 16:24:49 +03:00
parent f5a3487566
commit a178cff998
4 changed files with 119 additions and 75 deletions
@@ -12,8 +12,11 @@ import {IS_WEB} from '#/env'
import {ChatList, Header as ChatListHeader} from '../../ChatList' import {ChatList, Header as ChatListHeader} from '../../ChatList'
import {SplitViewProvider} from './context' import {SplitViewProvider} from './context'
const CENTER_COLUMN_WIDTH = 600
const LEFT_NAV_FULL_WIDTH = 245
const LEFT_NAV_MINIMAL_WIDTH = 86 const LEFT_NAV_MINIMAL_WIDTH = 86
const RIGHT_NAV_WIDTH = 330 + 28 const RIGHT_NAV_FULL_WIDTH = 330 + 28
const RIGHT_NAV_MINIMAL_WIDTH = 280 + 28
type LayoutProps = ScreenLayoutArgs< type LayoutProps = ScreenLayoutArgs<
AllNavigatorParams, AllNavigatorParams,
@@ -44,20 +47,26 @@ function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) {
? route?.params?.conversation ? route?.params?.conversation
: undefined : undefined
const leftNavWidth = centerColumnOffset
? LEFT_NAV_MINIMAL_WIDTH
: LEFT_NAV_FULL_WIDTH
const rightNavWidth = centerColumnOffset
? RIGHT_NAV_MINIMAL_WIDTH
: RIGHT_NAV_FULL_WIDTH
const containerWidth =
leftNavWidth - LEFT_NAV_MINIMAL_WIDTH + CENTER_COLUMN_WIDTH + rightNavWidth
return ( return (
<View <View
style={[ style={[
a.flex_1, a.flex_1,
a.flex_row, a.flex_row,
a.mx_auto, a.mx_auto,
{maxWidth: centerColumnOffset ? 900 : 950}, {maxWidth: containerWidth},
{ {
transform: [ transform: [
{ {translateX: LEFT_NAV_MINIMAL_WIDTH / 2},
translateX: centerColumnOffset
? LEFT_NAV_MINIMAL_WIDTH / 2
: RIGHT_NAV_WIDTH / 2,
},
{translateX: web(SCROLLBAR_OFFSET) ?? 0}, {translateX: web(SCROLLBAR_OFFSET) ?? 0},
], ],
}, },
+5 -5
View File
@@ -1,13 +1,13 @@
import {View} from 'react-native' import {View} from 'react-native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useBreakpoints} from '#/alf'
import {FABInner, type FABProps} from './FABInner' import {FABInner, type FABProps} from './FABInner'
export const FAB = (_opts: FABProps) => { export const FAB = (props: FABProps) => {
const {isDesktop} = useWebMediaQueries() const {gtMobile} = useBreakpoints()
if (!isDesktop) { if (!gtMobile) {
return <FABInner {..._opts} /> return <FABInner {...props} />
} }
return <View /> return <View />
@@ -200,7 +200,11 @@ function NativeStackNavigator({
</View> </View>
{IS_WEB && ( {IS_WEB && (
<> <>
{showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />} {showBottomBar ? (
<BottomBarWeb />
) : (
<DesktopLeftNav routeName={activeRoute.name} />
)}
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />} {!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
</> </>
)} )}
+93 -62
View File
@@ -8,8 +8,6 @@ import {useNavigation, useNavigationState} from '@react-navigation/native'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import { import {
@@ -30,7 +28,14 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {PressableWithHover} from '#/view/com/util/PressableWithHover'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {NavSignupCard} from '#/view/shell/NavSignupCard' import {NavSignupCard} from '#/view/shell/NavSignupCard'
import {atoms as a, tokens, useLayoutBreakpoints, useTheme, web} from '#/alf' import {
atoms as a,
tokens,
useBreakpoints,
useLayoutBreakpoints,
useTheme,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {type DialogControlProps} from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
@@ -81,7 +86,7 @@ import {router} from '../../../routes'
const NAV_ICON_WIDTH = 28 const NAV_ICON_WIDTH = 28
function ProfileCard() { function ProfileCard({minimal}: {minimal: boolean}) {
const {currentAccount, accounts} = useSession() const {currentAccount, accounts} = useSession()
const {logoutEveryAccount} = useSessionApi() const {logoutEveryAccount} = useSessionApi()
const {isLoading, data} = useProfilesQuery({ const {isLoading, data} = useProfilesQuery({
@@ -89,7 +94,6 @@ function ProfileCard() {
}) })
const profiles = data?.profiles const profiles = data?.profiles
const signOutPromptControl = Prompt.usePromptControl() const signOutPromptControl = Prompt.usePromptControl()
const {leftNavMinimal} = useLayoutBreakpoints()
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -106,7 +110,7 @@ function ProfileCard() {
const {isActive: live} = useActorStatus(profile) const {isActive: live} = useActorStatus(profile)
return ( return (
<View style={[a.my_md, !leftNavMinimal && [a.w_full, a.align_start]]}> <View style={[a.my_md, !minimal && [a.w_full, a.align_start]]}>
{!isLoading && profile ? ( {!isLoading && profile ? (
<Menu.Root> <Menu.Root>
<Menu.Trigger label={_(msg`Switch accounts`)}> <Menu.Trigger label={_(msg`Switch accounts`)}>
@@ -125,7 +129,7 @@ function ProfileCard() {
a.align_center, a.align_center,
a.flex_row, a.flex_row,
{gap: 6}, {gap: 6},
!leftNavMinimal && [a.pl_lg, a.pr_md], !minimal && [a.pl_lg, a.pr_md],
]}> ]}>
<View <View
style={[ style={[
@@ -138,8 +142,8 @@ function ProfileCard() {
a.z_10, a.z_10,
active && { active && {
transform: [ transform: [
{scale: !leftNavMinimal ? 2 / 3 : 0.8}, {scale: !minimal ? 2 / 3 : 0.8},
{translateX: !leftNavMinimal ? -22 : 0}, {translateX: !minimal ? -22 : 0},
], ],
}, },
]}> ]}>
@@ -150,7 +154,7 @@ function ProfileCard() {
live={live} live={live}
/> />
</View> </View>
{!leftNavMinimal && ( {!minimal && (
<> <>
<View <View
style={[ style={[
@@ -203,7 +207,7 @@ function ProfileCard() {
<LoadingPlaceholder <LoadingPlaceholder
width={size} width={size}
height={size} height={size}
style={[{borderRadius: size}, !leftNavMinimal && a.ml_lg]} style={[{borderRadius: size}, !minimal && a.ml_lg]}
/> />
)} )}
<Prompt.Basic <Prompt.Basic
@@ -374,12 +378,21 @@ interface NavItemProps {
icon: JSX.Element icon: JSX.Element
iconFilled: JSX.Element iconFilled: JSX.Element
label: string label: string
minimal: boolean
} }
function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { function NavItem({
count,
hasNew,
href,
icon,
iconFilled,
label,
minimal,
}: NavItemProps) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {leftNavMinimal} = useLayoutBreakpoints()
const [pathName] = useMemo(() => router.matchPath(href), [href]) const [pathName] = useMemo(() => router.matchPath(href), [href])
const currentRouteInfo = useNavigationState(state => { const currentRouteInfo = useNavigationState(state => {
if (!state) { if (!state) {
@@ -417,7 +430,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.p_md, a.p_md,
a.rounded_sm, a.rounded_full,
a.gap_sm, a.gap_sm,
a.outline_inset_1, a.outline_inset_1,
a.transition_color, a.transition_color,
@@ -438,9 +451,9 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
width: 24, width: 24,
height: 24, height: 24,
}, },
leftNavMinimal && { minimal && {
width: 40, width: 30,
height: 40, height: 30,
}, },
]}> ]}>
{isCurrent ? iconFilled : icon} {isCurrent ? iconFilled : icon}
@@ -479,7 +492,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
paddingVertical: 1, paddingVertical: 1,
minWidth: 16, minWidth: 16,
}, },
leftNavMinimal && [ minimal && [
{ {
top: '10%', top: '10%',
left: count.length === 1 ? 20 : 16, left: count.length === 1 ? 20 : 16,
@@ -502,7 +515,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
right: -2, right: -2,
top: -4, top: -4,
}, },
leftNavMinimal && { minimal && {
right: 4, right: 4,
top: 2, top: 2,
}, },
@@ -510,7 +523,7 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
/> />
) : null} ) : null}
</View> </View>
{!leftNavMinimal && ( {!minimal && (
<Text style={[a.text_xl, isCurrent ? a.font_bold : a.font_normal]}> <Text style={[a.text_xl, isCurrent ? a.font_bold : a.font_normal]}>
{label} {label}
</Text> </Text>
@@ -519,12 +532,11 @@ function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) {
) )
} }
function ComposeBtn() { function ComposeBtn({minimal}: {minimal: boolean}) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {getState} = useNavigation() const {getState} = useNavigation()
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const {_} = useLingui() const {_} = useLingui()
const {leftNavMinimal} = useLayoutBreakpoints()
const [isFetchingHandle, setIsFetchingHandle] = useState(false) const [isFetchingHandle, setIsFetchingHandle] = useState(false)
const fetchHandle = useFetchHandle() const fetchHandle = useFetchHandle()
@@ -564,31 +576,28 @@ function ComposeBtn() {
const onPressCompose = async () => const onPressCompose = async () =>
openComposer({mention: await getProfileHandle(), logContext: 'Fab'}) openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
if (leftNavMinimal) {
return null
}
return ( return (
<View style={[a.flex_row, a.pl_md, a.pt_xl]}> <View style={minimal ? [a.p_sm] : [a.flex_row, a.pl_md, a.pt_xl]}>
<Button <Button
disabled={isFetchingHandle} disabled={isFetchingHandle}
label={_(msg`Compose new post`)} label={_(msg`Compose new post`)}
onPress={() => void onPressCompose()} onPress={() => void onPressCompose()}
size="large" size="large"
variant="solid"
color="primary" color="primary"
style={[a.rounded_full]}> style={[a.rounded_full, minimal && {width: 56, height: 56}]}>
<ButtonIcon icon={EditBig} position="left" /> <ButtonIcon icon={EditBig} size={minimal ? 'lg' : 'sm'} />
<ButtonText> {!minimal && (
<Trans context="action">New post</Trans> <ButtonText>
</ButtonText> <Trans context="action">New post</Trans>
</ButtonText>
)}
</Button> </Button>
</View> </View>
) )
} }
function ChatNavItem() { function ChatNavItem({minimal}: {minimal: boolean}) {
const pal = usePalette('default') const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const numUnreadMessages = useUnreadMessageCount() const numUnreadMessages = useUnreadMessageCount()
const aa = useAgeAssurance() const aa = useAgeAssurance()
@@ -596,14 +605,19 @@ function ChatNavItem() {
return ( return (
<NavItem <NavItem
href="/messages" href="/messages"
minimal={minimal}
count={aa.flags.chatDisabled ? undefined : numUnreadMessages.numUnread} count={aa.flags.chatDisabled ? undefined : numUnreadMessages.numUnread}
hasNew={aa.flags.chatDisabled ? false : numUnreadMessages.hasNew} hasNew={aa.flags.chatDisabled ? false : numUnreadMessages.hasNew}
icon={ icon={
<Message style={pal.text} aria-hidden={true} width={NAV_ICON_WIDTH} /> <Message
style={t.atoms.text}
aria-hidden={true}
width={NAV_ICON_WIDTH}
/>
} }
iconFilled={ iconFilled={
<MessageFilled <MessageFilled
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
@@ -613,15 +627,19 @@ function ChatNavItem() {
) )
} }
export function DesktopLeftNav() { export function DesktopLeftNav({routeName}: {routeName: string}) {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isDesktop} = useWebMediaQueries() const t = useTheme()
const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() const {gtMobile} = useBreakpoints()
const isMessagesRelatedScreen = routeName.startsWith('Messages')
const {leftNavMinimal: leftNavMinimalBreakpoint, centerColumnOffset} =
useLayoutBreakpoints()
const numUnreadNotifications = useUnreadNotifications() const numUnreadNotifications = useUnreadNotifications()
if (!hasSession && !isDesktop) { const leftNavMinimal = isMessagesRelatedScreen || leftNavMinimalBreakpoint
if (!hasSession && !gtMobile) {
return null return null
} }
@@ -637,7 +655,11 @@ export function DesktopLeftNav() {
transform: [ transform: [
{ {
translateX: translateX:
-300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), -300 +
(centerColumnOffset ? CENTER_COLUMN_OFFSET : 0) +
(isMessagesRelatedScreen && !leftNavMinimalBreakpoint
? -130
: 0),
}, },
{translateX: '-100%'}, {translateX: '-100%'},
...a.scrollbar_offset.transform, ...a.scrollbar_offset.transform,
@@ -645,7 +667,7 @@ export function DesktopLeftNav() {
}, },
]}> ]}>
{hasSession ? ( {hasSession ? (
<ProfileCard /> <ProfileCard minimal={leftNavMinimal} />
) : !leftNavMinimal ? ( ) : !leftNavMinimal ? (
<View style={[a.pt_xl]}> <View style={[a.pt_xl]}>
<NavSignupCard /> <NavSignupCard />
@@ -656,34 +678,36 @@ export function DesktopLeftNav() {
<> <>
<NavItem <NavItem
href="/" href="/"
minimal={leftNavMinimal}
icon={ icon={
<Home <Home
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
iconFilled={ iconFilled={
<HomeFilled <HomeFilled
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
label={_(msg`Home`)} label={_(msg`Home`)}
/> />
<NavItem <NavItem
href="/search" href="/search"
minimal={leftNavMinimal}
icon={ icon={
<MagnifyingGlass <MagnifyingGlass
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
} }
iconFilled={ iconFilled={
<MagnifyingGlassFilled <MagnifyingGlassFilled
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
@@ -692,36 +716,38 @@ export function DesktopLeftNav() {
/> />
<NavItem <NavItem
href="/notifications" href="/notifications"
minimal={leftNavMinimal}
count={numUnreadNotifications} count={numUnreadNotifications}
icon={ icon={
<Bell <Bell
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
iconFilled={ iconFilled={
<BellFilled <BellFilled
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
label={_(msg`Notifications`)} label={_(msg`Notifications`)}
/> />
<ChatNavItem /> <ChatNavItem minimal={leftNavMinimal} />
<NavItem <NavItem
href="/feeds" href="/feeds"
minimal={leftNavMinimal}
icon={ icon={
<Hashtag <Hashtag
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
} }
iconFilled={ iconFilled={
<HashtagFilled <HashtagFilled
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
@@ -730,16 +756,17 @@ export function DesktopLeftNav() {
/> />
<NavItem <NavItem
href="/lists" href="/lists"
minimal={leftNavMinimal}
icon={ icon={
<List <List
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
} }
iconFilled={ iconFilled={
<ListFilled <ListFilled
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
@@ -748,16 +775,17 @@ export function DesktopLeftNav() {
/> />
<NavItem <NavItem
href="/saved" href="/saved"
minimal={leftNavMinimal}
icon={ icon={
<Bookmark <Bookmark
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
} }
iconFilled={ iconFilled={
<BookmarkFilled <BookmarkFilled
style={pal.text} style={t.atoms.text}
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
/> />
@@ -771,42 +799,44 @@ export function DesktopLeftNav() {
/> />
<NavItem <NavItem
href={currentAccount ? makeProfileLink(currentAccount) : '/'} href={currentAccount ? makeProfileLink(currentAccount) : '/'}
minimal={leftNavMinimal}
icon={ icon={
<UserCircle <UserCircle
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
iconFilled={ iconFilled={
<UserCircleFilled <UserCircleFilled
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
label={_(msg`Profile`)} label={_(msg`Profile`)}
/> />
<NavItem <NavItem
href="/settings" href="/settings"
minimal={leftNavMinimal}
icon={ icon={
<Settings <Settings
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
iconFilled={ iconFilled={
<SettingsFilled <SettingsFilled
aria-hidden={true} aria-hidden={true}
width={NAV_ICON_WIDTH} width={NAV_ICON_WIDTH}
style={pal.text} style={t.atoms.text}
/> />
} }
label={_(msg`Settings`)} label={_(msg`Settings`)}
/> />
<ComposeBtn /> <ComposeBtn minimal={leftNavMinimal} />
</> </>
)} )}
</View> </View>
@@ -824,6 +854,7 @@ const styles = StyleSheet.create({
// @ts-expect-error web only // @ts-expect-error web only
maxHeight: '100vh', maxHeight: '100vh',
overflowY: 'auto', overflowY: 'auto',
scrollbarWidth: 'thin',
}, },
leftNavWide: { leftNavWide: {
width: 245, width: 245,