fix(channels): collapse /channels/ index into one mobile pane and add Create-community CTA next to Find-community on empty state

This commit is contained in:
v.lagerev 2026-05-16 20:25:01 +03:00
parent bf05696336
commit ff01a045e3
5 changed files with 31 additions and 18 deletions

View file

@ -78,8 +78,8 @@ export function PageRoot({ nav, children }: PageRootProps) {
TL/BL carves expose the outer's void. The explicit TL/BL carves expose the outer's void. The explicit
Background bg on the inner is what keeps the panel's Background bg on the inner is what keeps the panel's
apparent colour unchanged for routes whose content has no apparent colour unchanged for routes whose content has no
opaque bg of its own (e.g. ChannelsLanding) without it opaque bg of its own without it the outer void would
the outer void would bleed through. */} bleed through. */}
<Box grow="Yes" style={{ minWidth: 0, backgroundColor: VOJO_HORSESHOE_VOID_COLOR }}> <Box grow="Yes" style={{ minWidth: 0, backgroundColor: VOJO_HORSESHOE_VOID_COLOR }}>
<Box <Box
grow="Yes" grow="Yes"

View file

@ -54,12 +54,7 @@ import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
import { HomeRouteRoomProvider } from './client/home'; import { HomeRouteRoomProvider } from './client/home';
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct'; import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
import { BotExperienceHost, Bots } from './client/bots'; import { BotExperienceHost, Bots } from './client/bots';
import { import { Channels, ChannelsRootNav, ChannelPickPlaceholder } from './client/channels';
Channels,
ChannelsRootNav,
ChannelPickPlaceholder,
ChannelsLanding,
} from './client/channels';
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space'; import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
import { Explore, FeaturedRooms, PublicRooms } from './client/explore'; import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath'; import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
@ -302,7 +297,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
</MobileFriendlyPageNav> </MobileFriendlyPageNav>
} }
> >
<ChannelsLanding /> {mobile ? null : <WelcomePage />}
</PageRoot> </PageRoot>
} }
/> />

View file

@ -4,24 +4,31 @@ import { PageNav, PageNavContent } from '../../../components/page';
import { StreamHeader } from '../../../components/stream-header'; import { StreamHeader } from '../../../components/stream-header';
import { ChannelsList } from './ChannelsList'; import { ChannelsList } from './ChannelsList';
import { ChannelCreateRow } from './ChannelCreateRow'; import { ChannelCreateRow } from './ChannelCreateRow';
import { ChannelsLanding } from './ChannelsLanding';
import { ChannelsWorkspaceHorseshoe } from './ChannelsWorkspaceHorseshoe'; import { ChannelsWorkspaceHorseshoe } from './ChannelsWorkspaceHorseshoe';
import { WorkspaceFooter } from './WorkspaceFooter'; import { WorkspaceFooter } from './WorkspaceFooter';
import { ACTIVE_SPACE_KEY } from './useActiveSpace'; import { ACTIVE_SPACE_KEY } from './useActiveSpace';
// Index route at /channels/ (no space selected). Renders the shared // Index route at /channels/ (no space selected). Renders the shared
// StreamHeader so the segment switcher is consistent across surfaces, // StreamHeader (segment switcher) plus the resolve-active-space-or-
// but no list/footer — those need a Space context. // show-empty-state body — same pattern as Direct's nav owning its own
// empty state. Keeping the landing inside the nav avoids rendering it
// as a sibling pane: on mobile both would collide into one screen, and
// on desktop the redirect logic would fire twice if mounted twice.
//
// `<ChannelsLanding />` is mounted **directly** as the curtain's flex
// child (NOT wrapped in `PageNavContent`). `NavEmptyCenter` relies on
// `flexGrow:1 + justifyContent:Center` for vertical centering — the
// curtain stage is `display:flex; flex-direction:column`, so direct
// mount gives the empty state the height it needs to center. Wrapping
// it in `PageNavContent` (block layout inside Scroll) would collapse
// the centering to top-aligned. Same idiom as `Direct.tsx::DirectEmpty`.
export function ChannelsRootNav() { export function ChannelsRootNav() {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
return ( return (
<PageNav resizable surface="surfaceVariant"> <PageNav resizable surface="surfaceVariant">
<StreamHeader scrollRef={scrollRef}> <StreamHeader scrollRef={scrollRef}>
<PageNavContent scrollRef={scrollRef}> <ChannelsLanding />
{/* Empty list slot is intentional: StreamHeader needs a real
scroll target on this route for its touch gesture even
though there's no list yet. */}
<div />
</PageNavContent>
</StreamHeader> </StreamHeader>
</PageNav> </PageNav>
); );

View file

@ -6,6 +6,7 @@ import { Box, Button, Icon, Icons, Text } from 'folds';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useOrphanSpaces } from '../../../state/hooks/roomList'; import { useOrphanSpaces } from '../../../state/hooks/roomList';
import { useOpenCreateSpaceModal } from '../../../state/hooks/createSpaceModal';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { getChannelsSpacePath, getExplorePath } from '../../pathUtils'; import { getChannelsSpacePath, getExplorePath } from '../../pathUtils';
@ -19,6 +20,7 @@ export function ChannelsLanding() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const openCreateSpaceModal = useOpenCreateSpaceModal();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents); const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
const activeSpaceId = useActiveSpace(orphanSpaces); const activeSpaceId = useActiveSpace(orphanSpaces);
@ -49,6 +51,16 @@ export function ChannelsLanding() {
{t('Channels.explore_cta')} {t('Channels.explore_cta')}
</Text> </Text>
</Button> </Button>
<Button
variant="Secondary"
fill="Soft"
size="300"
onClick={() => openCreateSpaceModal()}
>
<Text size="B300" truncate>
{t('Channels.workspace_switcher_create_space')}
</Text>
</Button>
</Box> </Box>
} }
/> />

View file

@ -1,5 +1,4 @@
export * from './Channels'; export * from './Channels';
export * from './ChannelsList'; export * from './ChannelsList';
export * from './ChannelsLanding';
export * from './ChannelPickPlaceholder'; export * from './ChannelPickPlaceholder';
export * from './useActiveSpace'; export * from './useActiveSpace';