Add Shared Media drawer and harden same-room navigation.

Widen the media panel, support skinny full-page mode, keep jump-to-latest and return-to-previous reliable, and stop same-room event jumps from remounting the outlet or yanking the sidebar.
This commit is contained in:
2026-07-23 14:52:01 +10:00
parent 61d41900cc
commit 9509a9705e
30 changed files with 1749 additions and 168 deletions

View File

@@ -41,7 +41,7 @@ import {
} from './pathUtils';
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
import { Direct, DirectCreate, DirectRouteRoomProvider, DirectSearch } from './client/direct';
import { RouteSpaceProvider, SpaceRouteRoomProvider, SpaceSearch, SpaceIndexRedirect } from './client/space';
import { ForumAwareSpaceLayout } from '../features/forum/ForumAwareSpaceLayout';
import { ForumFeedPage } from '../features/forum/ForumFeedPage';
@@ -223,6 +223,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
>
{mobile ? null : <Route index element={<WelcomePage />} />}
<Route path={_CREATE_PATH} element={<DirectCreate />} />
<Route path={_SEARCH_PATH} element={<DirectSearch />} />
<Route path={_NOTIFICATIONS_PATH} element={<Notifications />} />
<Route path={_INVITES_PATH} element={<Invites />} />
<Route

View File

@@ -17,7 +17,7 @@ import {
NavItemContent,
NavLink,
} from '../../../components/nav';
import { getDirectCreatePath, getDirectRoomPath, getDirectNotificationsPath, getDirectInvitesPath } from '../../pathUtils';
import { getDirectCreatePath, getDirectRoomPath, getDirectNotificationsPath, getDirectInvitesPath, getDirectSearchPath } from '../../pathUtils';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { VirtualTile } from '../../../components/virtualizer';
@@ -41,10 +41,9 @@ import {
getRoomNotificationMode,
useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences';
import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected';
import { useDirectCreateSelected, useDirectSearchSelected } from '../../../hooks/router/useDirectSelected';
import { allInvitesAtom } from '../../../state/room-list/inviteList';
import { UnreadBadge } from '../../../components/unread-badge';
import { UnreadBadge } from '../../../components/unread-badge';
type DirectMenuProps = {
requestClose: () => void;
};
@@ -228,6 +227,7 @@ export function Direct() {
const navigate = useNavigate();
const createDirectSelected = useDirectCreateSelected();
const searchSelected = useDirectSearchSelected();
const selectedRoomId = useSelectedRoom();
const noRoomToDisplay = directs.length === 0;
@@ -283,6 +283,22 @@ export function Direct() {
</NavButton>
</NavItem>
<PluginNavSlot location="direct-messages" />
<NavItem variant="Background" radii="400" aria-selected={searchSelected}>
<NavLink to={getDirectSearchPath()}>
<NavItemContent>
<Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400">
<Icon src={Icons.Search} size="100" filled={searchSelected} />
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Message Search
</Text>
</Box>
</Box>
</NavItemContent>
</NavLink>
</NavItem>
</NavCategory>
<NavCategory>
<NotificationsNavItem />

View File

@@ -0,0 +1,55 @@
import React, { useRef } from 'react';
import { Box, Text, Scroll, IconButton } from 'folds';
import { Icon, Icons } from '../../../components/icons';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { MessageSearch } from '../../../features/message-search';
import { useDirectRooms } from './useDirectRooms';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function DirectSearch() {
const scrollRef = useRef<HTMLDivElement>(null);
const rooms = useDirectRooms();
const screenSize = useScreenSizeContext();
return (
<Page>
<PageHeader balance>
<Box grow="Yes" alignItems="Center" gap="200">
<Box grow="Yes" basis="No">
{screenSize === ScreenSize.Mobile && (
<BackRouteHandler>
{(onBack) => (
<IconButton onClick={onBack}>
<Icon src={Icons.ArrowLeft} />
</IconButton>
)}
</BackRouteHandler>
)}
</Box>
<Box justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
<Text size="H3" truncate>
Message Search
</Text>
</Box>
<Box grow="Yes" basis="No" />
</Box>
</PageHeader>
<Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<MessageSearch
defaultRoomsFilterName="Direct Messages"
allowGlobal
rooms={rooms}
scrollRef={scrollRef}
/>
</PageContentCenter>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View File

@@ -1,3 +1,4 @@
export * from './Direct';
export * from './RoomProvider';
export * from './DirectCreate';
export * from './Search';

View File

@@ -3,6 +3,7 @@ import {
DIRECT_CREATE_PATH,
DIRECT_PATH,
DIRECT_ROOM_PATH,
DIRECT_SEARCH_PATH,
DIRECT_NOTIFICATIONS_PATH,
DIRECT_INVITES_PATH,
EXPLORE_FEATURED_PATH,
@@ -121,6 +122,7 @@ export const getHomeRoomPath = (roomIdOrAlias: string, eventId?: string): string
export const getDirectPath = (): string => DIRECT_PATH;
export const getDirectCreatePath = (): string => DIRECT_CREATE_PATH;
export const getDirectSearchPath = (): string => DIRECT_SEARCH_PATH;
export const getDirectRoomPath = (roomIdOrAlias: string, eventId?: string): string => {
const params = {
roomIdOrAlias,
@@ -180,6 +182,28 @@ export const getSpaceRoomPath = (
return generatePath(SPACE_ROOM_PATH, params);
};
/**
* Keep the current room URL (home / direct / space) and only add, replace, or clear
* the optional `$eventId` segment. Used so same-room jumps (media, permalinks) don't
* re-home the room into a different sidebar section.
*/
export const withRoomEventId = (pathname: string, eventId?: string): string => {
const parts = pathname.split('/').filter(Boolean);
if (parts.length > 0) {
const lastDecoded = decodeRouteParam(parts[parts.length - 1]);
if (lastDecoded?.startsWith('$')) {
parts.pop();
}
}
if (eventId) {
parts.push(encodeURIComponent(eventId));
}
return `/${parts.join('/')}/`;
};
export const getExplorePath = (): string => EXPLORE_PATH;
export const getExploreFeaturedPath = (): string => EXPLORE_FEATURED_PATH;
export const getExploreServerPath = (server: string): string => {

View File

@@ -54,6 +54,7 @@ export type DirectCreateSearchParams = {
userId?: string;
};
export const DIRECT_CREATE_PATH = `/direct/${_CREATE_PATH}`;
export const DIRECT_SEARCH_PATH = `/direct/${_SEARCH_PATH}`;
export const DIRECT_ROOM_PATH = `/direct/${_ROOM_PATH}`;
export const SPACE_PATH = '/:spaceIdOrAlias/';