feat: Implement forum feed layout and refactor space navigation logic to support new routing structure

This commit is contained in:
2026-07-06 15:35:21 +10:00
parent 09db721b7e
commit 3ee95a295d
17 changed files with 296 additions and 135 deletions

View File

@@ -1,15 +1,31 @@
import React from 'react';
import React, { useMemo } from 'react';
import { PageRoot } from '../../components/page';
import { AnimatedOutlet } from '../../components/AnimatedOutlet';
import { MobileFriendlyPageNav } from '../../pages/MobileFriendly';
import { SPACE_PATH } from '../../pages/paths';
import { Space } from '../../pages/client/space/Space';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useSpace } from '../../hooks/useSpace';
import { isForumContainer, shouldShowForumLobby } from '../../utils/room';
import { ForumBoardProvider } from './ForumBoardContext';
/**
* Space sidebar + outlet. Forum spaces render ForumFeedSidebar in Space.tsx.
*/
export function ForumAwareSpaceLayout() {
return (
const mx = useMatrixClient();
const space = useSpace();
const selectedRoomId = useSelectedRoom();
const scope = useMemo(() => {
const room = selectedRoomId ? mx.getRoom(selectedRoomId) : null;
const topicRoomId =
room && room.roomId !== space.roomId && !isForumContainer(room) ? room.roomId : undefined;
return { forumSpaceId: space.roomId, topicRoomId };
}, [mx, selectedRoomId, space.roomId]);
const layout = (
<PageRoot
nav={
<MobileFriendlyPageNav path={SPACE_PATH}>
@@ -20,4 +36,14 @@ export function ForumAwareSpaceLayout() {
<AnimatedOutlet />
</PageRoot>
);
if (!shouldShowForumLobby(space)) {
return layout;
}
return (
<ForumBoardProvider forumSpace={space} scope={scope}>
{layout}
</ForumBoardProvider>
);
}

View File

@@ -0,0 +1,6 @@
import { ForumBoardDetail } from './ForumBoardDetail';
/** Forum post feed in the main pane (sidebar holds filters + post list). */
export function ForumFeedPage() {
return <ForumBoardDetail />;
}

View File

@@ -1,30 +1,8 @@
import React, { ReactNode, useMemo } from 'react';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useSpace } from '../../hooks/useSpace';
import { isForumContainer, shouldShowForumLobby } from '../../utils/room';
import { ForumBoardProvider } from './ForumBoardContext';
import React, { ReactNode } from 'react';
/** Shares forum board state between space sidebar and lobby/room outlet. */
/**
* @deprecated Forum board context now lives in ForumAwareSpaceLayout.
*/
export function ForumSpaceLayout({ children }: { children: ReactNode }) {
const mx = useMatrixClient();
const space = useSpace();
const selectedRoomId = useSelectedRoom();
const scope = useMemo(() => {
const room = selectedRoomId ? mx.getRoom(selectedRoomId) : null;
const topicRoomId =
room && room.roomId !== space.roomId && !isForumContainer(room) ? room.roomId : undefined;
return { forumSpaceId: space.roomId, topicRoomId };
}, [mx, selectedRoomId, space.roomId]);
if (!shouldShowForumLobby(space)) {
return children;
}
return (
<ForumBoardProvider forumSpace={space} scope={scope}>
{children}
</ForumBoardProvider>
);
return children;
}

View File

@@ -3,6 +3,7 @@ export { ForumBoardDetail } from './ForumBoardDetail';
export { ForumSortIcons } from './forumLucideIcons';
export { ForumSpaceShell } from './ForumSpaceShell';
export { ForumThreadDetail } from './ForumThreadDetail';
export { ForumFeedPage } from './ForumFeedPage';
export { ForumAwareSpaceLayout } from './ForumAwareSpaceLayout';
export { ForumSpaceLayout } from './ForumSpaceLayout';
export type { ForumBoardScope } from './useForumBoard';

View File

@@ -41,8 +41,7 @@ import { getSpaceRoomPath } from '../../pages/pathUtils';
import { StateEvent } from '../../../types/matrix/room';
import { CanDropCallback, useDnDMonitor } from './DnD';
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
import { getStateEvent, shouldShowForumLobby } from '../../utils/room';
import { ForumBoardDetail } from '../forum/ForumBoardDetail';
import { getStateEvent } from '../../utils/room';
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
import {
makeCinnySpacesContent,
@@ -151,15 +150,7 @@ const useCanDropLobbyItem = (
return canDrop;
};
function ForumSpaceLobby() {
return <ForumBoardDetail />;
}
export function Lobby() {
const space = useSpace();
if (shouldShowForumLobby(space)) {
return <ForumSpaceLobby />;
}
return <SpaceCardLobby />;
}

View File

@@ -27,8 +27,12 @@ import { AddExistingModal } from '../add-existing';
import { SpeechBubble } from '../../components/speech-bubble/SpeechBubble';
import { SpaceOptionsMenu, type OpenAddExistingOptions } from '../space/SpaceOptionsMenu';
import type { HierarchyItemSpace } from '../../hooks/useSpaceHierarchy';
import { StateEvent } from '../../../types/matrix/room';
import { useNavigate } from 'react-router-dom';
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
import { getSpaceFeedPath } from '../../pages/pathUtils';
import { shouldShowForumLobby } from '../../utils/room';
import { useForumBoardContextOptionally } from '../forum/ForumBoardContext';
import { StateEvent } from '../../../types/matrix/room';
type LobbyMenuProps = {
powerLevels: IPowerLevels;
@@ -37,7 +41,9 @@ type LobbyMenuProps = {
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
({ powerLevels, requestClose }, ref) => {
const mx = useMatrixClient();
const navigate = useNavigate();
const space = useSpace();
const isForum = shouldShowForumLobby(space);
const creators = useRoomCreators(space);
const permissions = useRoomPermissions(creators, powerLevels);
@@ -55,6 +61,11 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
requestClose();
};
const handleOpenFeed = () => {
navigate(getSpaceFeedPath(getCanonicalAliasOrRoomId(mx, space.roomId)));
requestClose();
};
return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
{invitePrompt && (
@@ -67,6 +78,18 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
/>
)}
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{isForum && (
<MenuItem
onClick={handleOpenFeed}
size="300"
after={<Icon size="100" src={Icons.Thread} />}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Feed
</Text>
</MenuItem>
)}
<MenuItem
onClick={handleInvite}
variant="Primary"

View File

@@ -28,6 +28,10 @@ import { copyToClipboard } from '../../utils/dom';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers';
import { useNavigate } from 'react-router-dom';
import { getSpaceFeedPath, getSpaceLobbyPath } from '../../pages/pathUtils';
import { shouldShowForumLobby } from '../../utils/room';
import { useSpaceFeedSelected } from '../../hooks/router/useSelectedSpace';
type ForumAddOptions = {
item: HierarchyItem;
@@ -50,6 +54,9 @@ type SpaceOptionsMenuProps = {
export const SpaceOptionsMenu = forwardRef<HTMLDivElement, SpaceOptionsMenuProps>(
({ room, requestClose, forumAdd, onOpenAddExisting }, ref) => {
const mx = useMatrixClient();
const navigate = useNavigate();
const onFeed = useSpaceFeedSelected();
const isForum = shouldShowForumLobby(room);
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const roomToParents = useAtomValue(roomToParentsAtom);
@@ -101,6 +108,16 @@ export const SpaceOptionsMenu = forwardRef<HTMLDivElement, SpaceOptionsMenuProps
requestClose();
};
const handleOpenFeed = () => {
navigate(getSpaceFeedPath(getCanonicalAliasOrRoomId(mx, room.roomId)));
requestClose();
};
const handleOpenManage = () => {
navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, room.roomId)));
requestClose();
};
const handleAddTopic = () => {
if (!forumAdd) return;
openCreateRoomModal(forumAdd.item.roomId);
@@ -200,6 +217,30 @@ export const SpaceOptionsMenu = forwardRef<HTMLDivElement, SpaceOptionsMenuProps
}}
/>
)}
{isForum && !onFeed && (
<MenuItem
onClick={handleOpenFeed}
size="300"
after={<Icon size="100" src={Icons.Thread} />}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Feed
</Text>
</MenuItem>
)}
{isForum && onFeed && (
<MenuItem
onClick={handleOpenManage}
size="300"
after={<Icon size="100" src={Icons.Flag} />}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Manage
</Text>
</MenuItem>
)}
<MenuItem
onClick={handleMarkAsRead}
size="300"