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,7 +1,6 @@
import { useMatch, useParams } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';
import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix';
import { useMatrixClient } from '../useMatrixClient';
import { getSpaceLobbyPath, getSpaceSearchPath } from '../../pages/pathUtils';
export const useSelectedSpace = (): string | undefined => {
const mx = useMatrixClient();
@@ -16,22 +15,18 @@ export const useSelectedSpace = (): string | undefined => {
return spaceId;
};
export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
path: getSpaceLobbyPath(spaceIdOrAlias),
caseSensitive: true,
end: false,
});
return !!match;
/** True when the current URL is the forum feed route. */
export const useSpaceFeedSelected = (): boolean => {
const { pathname } = useLocation();
return /\/feed\/?$/.test(pathname);
};
export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => {
const match = useMatch({
path: getSpaceSearchPath(spaceIdOrAlias),
caseSensitive: true,
end: false,
});
return !!match;
export const useSpaceLobbySelected = (_spaceIdOrAlias?: string): boolean => {
const { pathname } = useLocation();
return /\/lobby\/?$/.test(pathname);
};
export const useSpaceSearchSelected = (_spaceIdOrAlias?: string): boolean => {
const { pathname } = useLocation();
return /\/search\/?$/.test(pathname);
};

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback } from 'react';
import { useEffect, useCallback } from 'react';
import { MatrixClient, Room, RoomEvent } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { useAtomValue, useSetAtom } from 'jotai';
@@ -10,6 +10,7 @@ import {
RoomNotificationMode,
} from './useRoomsNotificationPreferences';
import { updateJoiningProgressAtom } from '../state/joiningProgress';
import { useMatrixClient } from './useMatrixClient';
const PER_PAGE_COUNT = 100;
const MAX_PAGES = 50;
@@ -18,6 +19,8 @@ const JOIN_DELAY_MS = 500;
/** Join rules that allow automatic joining */
const AUTO_JOINABLE_RULES = ['public', 'restricted', 'knock_restricted'];
const processingSpaces = new Set<string>();
/**
* Extracts the server name from a Matrix room ID
* @param roomId - The room ID (e.g., "!abc123:matrix.org")
@@ -46,12 +49,12 @@ function canAutoJoin(hierarchyRoom: IHierarchyRoom): boolean {
}
/**
* Checks if a hierarchy item is a space
* Checks if a hierarchy item is a space (including m.forum category spaces)
* @param hierarchyRoom - The room info from the space hierarchy
* @returns Whether the item is a space
*/
function isHierarchySpace(hierarchyRoom: IHierarchyRoom): boolean {
return hierarchyRoom.room_type === 'm.space';
return hierarchyRoom.room_type === 'm.space' || hierarchyRoom.room_type === 'm.forum';
}
/**
@@ -177,6 +180,29 @@ async function joinRoomsSequentially(
);
}
/**
* Joins all auto-joinable children of a space (and nested spaces/rooms).
*/
export async function autoJoinSpaceChildren(
mx: MatrixClient,
spaceId: string,
onProgress?: ProgressCallback
): Promise<void> {
if (processingSpaces.has(spaceId)) {
return;
}
processingSpaces.add(spaceId);
try {
const hierarchyRooms = await fetchSpaceHierarchy(mx, spaceId);
const childRooms = hierarchyRooms.filter((r) => r.room_id !== spaceId);
await joinRoomsSequentially(mx, childRooms, spaceId, onProgress);
} finally {
processingSpaces.delete(spaceId);
}
}
/**
* Hook that automatically joins all accessible rooms when joining a space
* @param mx - Matrix client instance
@@ -184,7 +210,6 @@ async function joinRoomsSequentially(
export function useAutoJoinSpaceRooms(mx: MatrixClient): void {
const settings = useAtomValue(settingsAtom);
const setJoiningProgress = useSetAtom(updateJoiningProgressAtom);
const processingSpaces = useRef<Set<string>>(new Set());
const handleSpaceJoin = useCallback(
async (room: Room) => {
@@ -192,7 +217,7 @@ export function useAutoJoinSpaceRooms(mx: MatrixClient): void {
return;
}
// Only process spaces
// Only process spaces (includes m.forum containers)
if (!isSpace(room)) {
return;
}
@@ -204,27 +229,11 @@ export function useAutoJoinSpaceRooms(mx: MatrixClient): void {
const spaceId = room.roomId;
// Prevent duplicate processing
if (processingSpaces.current.has(spaceId)) {
return;
}
processingSpaces.current.add(spaceId);
try {
// Fetch the space hierarchy
const hierarchyRooms = await fetchSpaceHierarchy(mx, spaceId);
// Filter out the space itself
const childRooms = hierarchyRooms.filter((r) => r.room_id !== spaceId);
// Join all accessible rooms with progress tracking
await joinRoomsSequentially(mx, childRooms, spaceId, (current, total) => {
await autoJoinSpaceChildren(mx, spaceId, (current, total) => {
setJoiningProgress({ spaceId, progress: { current, total } });
});
} finally {
processingSpaces.current.delete(spaceId);
// Clear progress when done
setJoiningProgress({ spaceId, progress: null });
}
},
@@ -245,3 +254,45 @@ export function useAutoJoinSpaceRooms(mx: MatrixClient): void {
};
}, [mx, handleSpaceJoin]);
}
/**
* Auto-joins space children when visiting an already-joined space (e.g. forum spaces).
*/
export function useAutoJoinOnSpaceVisit(space: Room | undefined): void {
const mx = useMatrixClient();
const settings = useAtomValue(settingsAtom);
const setJoiningProgress = useSetAtom(updateJoiningProgressAtom);
useEffect(() => {
if (!space || !settings.autoJoinSpaceRooms) {
return;
}
if (!isSpace(space)) {
return;
}
if (space.getMyMembership() !== Membership.Join) {
return;
}
const spaceId = space.roomId;
let cancelled = false;
void (async () => {
try {
await autoJoinSpaceChildren(mx, spaceId, (current, total) => {
if (!cancelled) {
setJoiningProgress({ spaceId, progress: { current, total } });
}
});
} finally {
if (!cancelled) {
setJoiningProgress({ spaceId, progress: null });
}
}
})();
return () => {
cancelled = true;
};
}, [space, settings.autoJoinSpaceRooms, mx, setJoiningProgress]);
}