From c0799142d6f667c782c0551e3625e6eca1512ae9 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Thu, 19 Jun 2025 22:36:52 +0200 Subject: [PATCH 01/16] Fix room header members icon not filled when enabled --- src/app/features/room/RoomViewHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 352ae4b5..edc3170e 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -34,7 +34,7 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { StateEvent } from '../../../types/matrix/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoom } from '../../hooks/useRoom'; -import { useSetSetting, useSetting } from '../../state/hooks/settings'; +import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils'; @@ -230,7 +230,7 @@ export function RoomViewHeader() { ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; - const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); + const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const handleSearchClick = () => { const searchParams: _SearchPathSearchParams = { @@ -410,7 +410,7 @@ export function RoomViewHeader() { > {(triggerRef) => ( setPeopleDrawer((drawer) => !drawer)}> - + )} From 2fae418132d27e576ef7e801402fd85306597648 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Fri, 27 Jun 2025 10:05:56 +0200 Subject: [PATCH 02/16] fix hierarchy indenting and order --- src/app/hooks/useSpaceHierarchy.ts | 57 ++++++++++++++++------------ src/app/pages/client/space/Space.tsx | 47 +++++++++++++++++------ 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index ad34e3f4..34f5e123 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -1,6 +1,6 @@ import { atom, useAtom, useAtomValue } from 'jotai'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { MatrixError, Room } from 'matrix-js-sdk'; +import { MatrixError, MatrixEvent, Room } from 'matrix-js-sdk'; import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces'; import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query'; import { useMatrixClient } from './useMatrixClient'; @@ -18,6 +18,7 @@ export type HierarchyItemSpace = { ts: number; space: true; parentId?: string; + depth: number; }; export type HierarchyItemRoom = { @@ -25,6 +26,7 @@ export type HierarchyItemRoom = { content: MSpaceChildContent; ts: number; parentId: string; + depth: number; }; export type HierarchyItem = HierarchyItemSpace | HierarchyItemRoom; @@ -35,6 +37,10 @@ const hierarchyItemTs: SortFunc = (a, b) => byTsOldToNew(a.ts, b. const hierarchyItemByOrder: SortFunc = (a, b) => byOrderKey(a.content.order, b.content.order); +const childEventTs: SortFunc = (a, b) => byTsOldToNew(a.getTs(), b.getTs()); +const childEventByOrder: SortFunc = (a, b) => + byOrderKey(a.getContent().order, b.getContent().order); + const getHierarchySpaces = ( rootSpaceId: string, getRoom: GetRoomCallback, @@ -45,8 +51,9 @@ const getHierarchySpaces = ( content: { via: [] }, ts: 0, space: true, + depth: 0, }; - let spaceItems: HierarchyItemSpace[] = []; + const spaceItems: HierarchyItemSpace[] = []; const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => { if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return; @@ -55,37 +62,37 @@ const getHierarchySpaces = ( if (!space) return; const childEvents = getStateEvents(space, StateEvent.SpaceChild); + childEvents + .filter((childEvent) => { + if (!isValidChild(childEvent)) return false; + const childId = childEvent.getStateKey(); + if (!childId || !isRoomId(childId)) return false; + + // because we can not find if a childId is space without joining + // or requesting room summary, we will look it into spaceRooms local + // cache which we maintain as we load summary in UI. + return getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId); + }) + .sort(childEventTs) + .sort(childEventByOrder); childEvents.forEach((childEvent) => { - if (!isValidChild(childEvent)) return; const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return; - // because we can not find if a childId is space without joining - // or requesting room summary, we will look it into spaceRooms local - // cache which we maintain as we load summary in UI. - if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) { - const childItem: HierarchyItemSpace = { - roomId: childId, - content: childEvent.getContent(), - ts: childEvent.getTs(), - space: true, - parentId: spaceItem.roomId, - }; - findAndCollectHierarchySpaces(childItem); - } + const childItem: HierarchyItemSpace = { + roomId: childId, + content: childEvent.getContent(), + ts: childEvent.getTs(), + space: true, + parentId: spaceItem.roomId, + depth: spaceItem.depth + 1, + }; + findAndCollectHierarchySpaces(childItem); }); }; findAndCollectHierarchySpaces(rootSpaceItem); - spaceItems = [ - rootSpaceItem, - ...spaceItems - .filter((item) => item.roomId !== rootSpaceId) - .sort(hierarchyItemTs) - .sort(hierarchyItemByOrder), - ]; - return spaceItems; }; @@ -121,6 +128,7 @@ const getSpaceHierarchy = ( content: childEvent.getContent(), ts: childEvent.getTs(), parentId: spaceItem.roomId, + depth: spaceItem.depth, }; childItems.push(childItem); }); @@ -208,6 +216,7 @@ const getSpaceJoinedHierarchy = ( content: childEvent.getContent(), ts: childEvent.getTs(), parentId: spaceItem.roomId, + depth: spaceItem.depth, }; childItems.push(childItem); }); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index d1009464..2868701f 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -318,7 +318,8 @@ export function Space() { useCallback( (parentId, roomId) => { if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { - return false; + // REWORK HOW THIS WORKS? + return false; // This does not account for sub-subspaces, best way to do? - first fix useSpaceHie... } const showRoom = roomToUnread.has(roomId) || roomId === selectedRoomId; if (showRoom) return false; @@ -346,6 +347,12 @@ export function Space() { const getToLink = (roomId: string) => getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId)); + const getCategoryPadding = (depth: number): string | undefined => { + if (depth === 0) return undefined; + if (depth === 1) return config.space.S400; + return config.space.S200; + }; + return ( @@ -392,12 +399,17 @@ export function Space() { }} > {virtualizer.getVirtualItems().map((vItem) => { - const { roomId } = hierarchy[vItem.index] ?? {}; + const { roomId, depth } = hierarchy[vItem.index] ?? {}; const room = mx.getRoom(roomId); if (!room) return null; + const paddingLeft = `calc((${depth} - 1) * ${config.space.S200})`; + if (room.isSpaceRoom()) { const categoryId = makeNavCategoryId(space.roomId, roomId); + const closed = closedCategories.has(categoryId); + + const paddingTop = getCategoryPadding(depth); return ( -
+
{roomId === space.roomId ? 'Rooms' : room?.name} @@ -422,14 +440,19 @@ export function Space() { return ( - +
+ +
); })} From 6b3841cd2bd741fe002995efdd21d85a2527b707 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sat, 28 Jun 2025 02:31:41 +0200 Subject: [PATCH 03/16] Partially fix collapse behaviour --- src/app/hooks/useSpaceHierarchy.ts | 16 +++++- src/app/pages/client/space/Space.tsx | 86 +++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 34f5e123..81fa8172 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -44,6 +44,7 @@ const childEventByOrder: SortFunc = (a, b) => const getHierarchySpaces = ( rootSpaceId: string, getRoom: GetRoomCallback, + excludeRoom: (parentId: string, roomId: string) => boolean, spaceRooms: Set ): HierarchyItemSpace[] => { const rootSpaceItem: HierarchyItemSpace = { @@ -79,6 +80,7 @@ const getHierarchySpaces = ( childEvents.forEach((childEvent) => { const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return; + if (excludeRoom(spaceItem.roomId, childId)) return; const childItem: HierarchyItemSpace = { roomId: childId, @@ -106,7 +108,12 @@ const getSpaceHierarchy = ( getRoom: (roomId: string) => Room | undefined, closedCategory: (spaceId: string) => boolean ): SpaceHierarchy[] => { - const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms); + const spaceItems: HierarchyItemSpace[] = getHierarchySpaces( + rootSpaceId, + getRoom, + () => false, + spaceRooms + ); const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => { const space = getRoom(spaceItem.roomId); @@ -185,7 +192,12 @@ const getSpaceJoinedHierarchy = ( excludeRoom: (parentId: string, roomId: string) => boolean, sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[] ): HierarchyItem[] => { - const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, new Set()); + const spaceItems: HierarchyItemSpace[] = getHierarchySpaces( + rootSpaceId, + getRoom, + excludeRoom, + new Set() + ); const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => { const space = getRoom(spaceItem.roomId); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 2868701f..076484ca 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -292,6 +292,7 @@ export function Space() { const scrollRef = useRef(null); const mDirects = useAtomValue(mDirectAtom); const roomToUnread = useAtomValue(roomToUnreadAtom); + const roomToParents = useAtomValue(roomToParentsAtom); const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); @@ -312,24 +313,93 @@ export function Space() { [mx, allJoinedRooms] ); + /** + * Recursively checks if a given parentId (and its ancestors) is in a closed category. + * + * @param spaceId - The root space ID to check against. + * @param parentId - The parent room or space ID to start the check from. + * @returns True if parentId or any ancestor is in a closed category. + */ + const getInClosedCategories = useCallback( + (spaceId: string, parentId: string): boolean => { + if (closedCategories.has(makeNavCategoryId(spaceId, parentId))) { + return true; + } + + const parentParentIds = roomToParents.get(parentId); + if (!parentParentIds || parentParentIds.size === 0) { + return false; + } + parentParentIds.forEach((id) => getInClosedCategories(spaceId, id)); + + return false; + }, + [closedCategories, roomToParents] + ); + + // There are a lot better ways to do this + const roomToChildren = useMemo(() => { + const map = new Map>(); + roomToParents.forEach((parentSet, childId) => { + parentSet.forEach((parentId) => { + if (!map.has(parentId)) map.set(parentId, new Set()); + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + map.get(parentId)!.add(childId); + }); + }); + return map; + }, [roomToParents]); + + /** + * Recursively checks if the given room or any of its descendants should be visible. + * + * @param roomId - The room ID to check. + * @returns True if the room or any descendant should be visible. + */ + const getContainsShowRoom = useCallback( + (roomId: string): boolean => { + if (roomToUnread.has(roomId) || roomId === selectedRoomId) { + return true; + } + + const childIds = roomToChildren.get(roomId); + console.log('CHILDREN'); + console.log(childIds?.forEach((childId) => getRoom(childId)?.name)); + if (!childIds || childIds.size === 0) { + return false; + } + + // CHILD CATEGORY SHOULD COLLAPSE IF PARENT IS COLLAPSED (but retain their set state when expanded?) + // WHY ARE CHILDREN DISPLAYED? + let visible = false; + childIds.forEach((id) => { + if (getContainsShowRoom(id)) { + visible = true; + } + }); + + return visible; + }, + [roomToUnread, selectedRoomId, roomToChildren] + ); + const hierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, useCallback( (parentId, roomId) => { - if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { - // REWORK HOW THIS WORKS? - return false; // This does not account for sub-subspaces, best way to do? - first fix useSpaceHie... + if (!getInClosedCategories(space.roomId, parentId)) { + return false; } - const showRoom = roomToUnread.has(roomId) || roomId === selectedRoomId; - if (showRoom) return false; + if (getContainsShowRoom(roomId)) return false; return true; }, - [space.roomId, closedCategories, roomToUnread, selectedRoomId] + [getContainsShowRoom, getInClosedCategories, space.roomId] ), useCallback( - (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)), - [closedCategories, space.roomId] + //IS CATEGORY CLOSED - SHOULD BE CLOSED IF PARENT IS, add new param?? HOW IS IT HANDLED + (sId) => getInClosedCategories(space.roomId, sId), + [getInClosedCategories, space.roomId] ) ); From a9fa1aada800800d170ea1f0654a217bb6079a74 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sat, 28 Jun 2025 14:14:44 +0200 Subject: [PATCH 04/16] remove getInCollapedCategories --- src/app/pages/client/space/Space.tsx | 38 +++++----------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 076484ca..1999120c 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -313,30 +313,6 @@ export function Space() { [mx, allJoinedRooms] ); - /** - * Recursively checks if a given parentId (and its ancestors) is in a closed category. - * - * @param spaceId - The root space ID to check against. - * @param parentId - The parent room or space ID to start the check from. - * @returns True if parentId or any ancestor is in a closed category. - */ - const getInClosedCategories = useCallback( - (spaceId: string, parentId: string): boolean => { - if (closedCategories.has(makeNavCategoryId(spaceId, parentId))) { - return true; - } - - const parentParentIds = roomToParents.get(parentId); - if (!parentParentIds || parentParentIds.size === 0) { - return false; - } - parentParentIds.forEach((id) => getInClosedCategories(spaceId, id)); - - return false; - }, - [closedCategories, roomToParents] - ); - // There are a lot better ways to do this const roomToChildren = useMemo(() => { const map = new Map>(); @@ -363,14 +339,11 @@ export function Space() { } const childIds = roomToChildren.get(roomId); - console.log('CHILDREN'); - console.log(childIds?.forEach((childId) => getRoom(childId)?.name)); if (!childIds || childIds.size === 0) { return false; } // CHILD CATEGORY SHOULD COLLAPSE IF PARENT IS COLLAPSED (but retain their set state when expanded?) - // WHY ARE CHILDREN DISPLAYED? let visible = false; childIds.forEach((id) => { if (getContainsShowRoom(id)) { @@ -388,18 +361,19 @@ export function Space() { getRoom, useCallback( (parentId, roomId) => { - if (!getInClosedCategories(space.roomId, parentId)) { + // closedCategories.has(makeNavCategoryId(spaceId, parentId)) + // NOT SURE THIS IS NEEDED - children of hidden spaces are not displayed + if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { return false; } if (getContainsShowRoom(roomId)) return false; return true; }, - [getContainsShowRoom, getInClosedCategories, space.roomId] + [closedCategories, getContainsShowRoom, space.roomId] ), useCallback( - //IS CATEGORY CLOSED - SHOULD BE CLOSED IF PARENT IS, add new param?? HOW IS IT HANDLED - (sId) => getInClosedCategories(space.roomId, sId), - [getInClosedCategories, space.roomId] + (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)), + [closedCategories, space.roomId] ) ); From 6ff0260e8d67f1d85d79ff0528c41f213442c916 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sat, 28 Jun 2025 23:59:00 +0200 Subject: [PATCH 05/16] fix collapse behaviour --- src/app/pages/client/space/Space.tsx | 86 ++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 1999120c..a84c0e30 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -313,7 +313,37 @@ export function Space() { [mx, allJoinedRooms] ); - // There are a lot better ways to do this + /** + * Recursively checks if a given parentId (and its ancestors) is in a closed category. + * + * @param spaceId - The root space ID. + * @param parentId - The parent space ID to start the check from. + * @returns True if parentId or any ancestor is in a closed category. + */ + const getInClosedCategories = useCallback( + (spaceId: string, parentId: string): boolean => { + if (closedCategories.has(makeNavCategoryId(spaceId, parentId))) { + return true; + } + + const parentParentIds = roomToParents.get(parentId); + if (!parentParentIds || parentParentIds.size === 0) { + return false; + } + + let closed = false; + parentParentIds.forEach((id) => { + if (getInClosedCategories(spaceId, id)) { + closed = true; + } + }); + + return closed; + }, + [closedCategories, roomToParents] + ); + + // There are better ways to do this const roomToChildren = useMemo(() => { const map = new Map>(); roomToParents.forEach((parentSet, childId) => { @@ -343,7 +373,6 @@ export function Space() { return false; } - // CHILD CATEGORY SHOULD COLLAPSE IF PARENT IS COLLAPSED (but retain their set state when expanded?) let visible = false; childIds.forEach((id) => { if (getContainsShowRoom(id)) { @@ -356,24 +385,46 @@ export function Space() { [roomToUnread, selectedRoomId, roomToChildren] ); + /** + * Determines whether all parent categories are collapsed. + * + * @param spaceId - The root space ID. + * @param roomId - The room ID to start the check from. + * @returns True if every parent category is collapsed; false otherwise. + */ + + const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { + const parentIds = roomToParents.get(roomId); + + if (!parentIds || parentIds.size === 0) { + return false; + } + + let allCollapsed = true; + parentIds.forEach((id) => { + if (!getInClosedCategories(spaceId, id)) { + allCollapsed = false; + } + }); + return allCollapsed; + }; + const hierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, useCallback( (parentId, roomId) => { - // closedCategories.has(makeNavCategoryId(spaceId, parentId)) - // NOT SURE THIS IS NEEDED - children of hidden spaces are not displayed - if (!closedCategories.has(makeNavCategoryId(space.roomId, parentId))) { + if (!getInClosedCategories(space.roomId, parentId)) { return false; } if (getContainsShowRoom(roomId)) return false; return true; }, - [closedCategories, getContainsShowRoom, space.roomId] + [getContainsShowRoom, getInClosedCategories, space.roomId] ), useCallback( - (sId) => closedCategories.has(makeNavCategoryId(space.roomId, sId)), - [closedCategories, space.roomId] + (sId) => getInClosedCategories(space.roomId, sId), + [getInClosedCategories, space.roomId] ) ); @@ -384,9 +435,18 @@ export function Space() { overscan: 10, }); - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => - closedCategories.has(categoryId) - ); + const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { + const collapsed = closedCategories.has(categoryId); + const [spaceId, roomId] = categoryId.split('|').slice(-2); + + // Only prevent collapsing if all parents are collapsed + const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); + + if (toggleable) { + return collapsed; + } + return !collapsed; + }); const getToLink = (roomId: string) => getSpaceRoomPath(spaceIdOrAlias, getCanonicalAliasOrRoomId(mx, roomId)); @@ -451,7 +511,8 @@ export function Space() { if (room.isSpaceRoom()) { const categoryId = makeNavCategoryId(space.roomId, roomId); - const closed = closedCategories.has(categoryId); + const closed = getInClosedCategories(space.roomId, roomId); + const toggleable = !getAllAncestorsCollapsed(space.roomId, roomId); const paddingTop = getCategoryPadding(depth); @@ -473,6 +534,7 @@ export function Space() { onClick={handleCategoryClick} closed={closed} aria-expanded={!closed} + aria-disabled={!toggleable} > {roomId === space.roomId ? 'Rooms' : room?.name} From a55065b1de1733fb42a29905f005dda33e911c77 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sun, 29 Jun 2025 13:58:05 +0200 Subject: [PATCH 06/16] bugfix --- src/app/hooks/useSpaceHierarchy.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 81fa8172..54478505 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -62,12 +62,12 @@ const getHierarchySpaces = ( spaceItems.push(spaceItem); if (!space) return; - const childEvents = getStateEvents(space, StateEvent.SpaceChild); - childEvents + const childEvents = getStateEvents(space, StateEvent.SpaceChild) .filter((childEvent) => { if (!isValidChild(childEvent)) return false; const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return false; + if (excludeRoom(spaceItem.roomId, childId)) return false; // because we can not find if a childId is space without joining // or requesting room summary, we will look it into spaceRooms local @@ -80,7 +80,6 @@ const getHierarchySpaces = ( childEvents.forEach((childEvent) => { const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return; - if (excludeRoom(spaceItem.roomId, childId)) return; const childItem: HierarchyItemSpace = { roomId: childId, From 64fc3066a1f09892349722850f26a6ddf2746cbb Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sun, 29 Jun 2025 20:01:25 +0200 Subject: [PATCH 07/16] improve lobby --- src/app/features/lobby/Lobby.tsx | 46 ++++++++++++++++++++++--- src/app/features/lobby/SpaceItem.tsx | 50 ++++++++++++++++++++++------ src/app/pages/client/space/Space.tsx | 12 +++---- 3 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 069e925e..6851873f 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -162,6 +162,7 @@ export function Lobby() { const screenSize = useScreenSizeContext(); const [onTop, setOnTop] = useState(true); const [closedCategories, setClosedCategories] = useAtom(useClosedLobbyCategoriesAtom()); + const roomToParents = useAtomValue(roomToParentsAtom); const [sidebarItems] = useSidebarItems( useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom)) ); @@ -193,6 +194,36 @@ export function Lobby() { [mx] ); + /** + * Recursively checks if a given parentId (or all its ancestors) is in a closed category. + * + * @param spaceId - The root space ID. + * @param parentId - The parent space ID to start the check from. + * @returns True if parentId or all ancestors is in a closed category. + */ + const getInClosedCategories = useCallback( + (spaceId: string, parentId: string): boolean => { + if (closedCategories.has(makeLobbyCategoryId(spaceId, parentId))) { + return true; + } + + const parentParentIds = roomToParents.get(parentId); + if (!parentParentIds || parentParentIds.size === 0) { + return false; + } + + let anyOpen = false; + parentParentIds.forEach((id) => { + if (!getInClosedCategories(spaceId, id)) { + anyOpen = true; + } + }); + + return !anyOpen; + }, + [closedCategories, roomToParents] + ); + const [draggingItem, setDraggingItem] = useState(); const hierarchy = useSpaceHierarchy( space.roomId, @@ -200,9 +231,9 @@ export function Lobby() { getRoom, useCallback( (childId) => - closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) || + getInClosedCategories(space.roomId, childId) || (draggingItem ? 'space' in draggingItem : false), - [closedCategories, space.roomId, draggingItem] + [draggingItem, getInClosedCategories, space.roomId] ) ); @@ -476,14 +507,20 @@ export function Lobby() { const item = hierarchy[vItem.index]; if (!item) return null; const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId; - const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId); + const inClosedCategory = getInClosedCategories( + space.roomId, + item.space.roomId + ); + + const paddingLeft = `calc((${item.space.depth} - 1) * ${config.space.S200})`; return ( } > - } - onClick={handleAddSpace} - aria-pressed={!!cords} - > - Add Space - + {item.parentId === undefined ? ( + } + onClick={handleAddSpace} + aria-pressed={!!cords} + > + Add Space + + ) : ( + + Add Space + + } + > + {(triggerRef) => ( + + + + )} + + )} ); } @@ -473,7 +503,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>( {canEditChild && ( - {item.parentId === undefined && } + )} diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index a84c0e30..bb79dcac 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -314,11 +314,11 @@ export function Space() { ); /** - * Recursively checks if a given parentId (and its ancestors) is in a closed category. + * Recursively checks if a given parentId (or all its ancestors) is in a closed category. * * @param spaceId - The root space ID. * @param parentId - The parent space ID to start the check from. - * @returns True if parentId or any ancestor is in a closed category. + * @returns True if parentId or all ancestors is in a closed category. */ const getInClosedCategories = useCallback( (spaceId: string, parentId: string): boolean => { @@ -331,14 +331,14 @@ export function Space() { return false; } - let closed = false; + let anyOpen = false; parentParentIds.forEach((id) => { - if (getInClosedCategories(spaceId, id)) { - closed = true; + if (!getInClosedCategories(spaceId, id)) { + anyOpen = true; } }); - return closed; + return !anyOpen; }, [closedCategories, roomToParents] ); From 56dfc6a497574b51ff259942cc9d7f4be8d12226 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Mon, 30 Jun 2025 14:17:32 +0200 Subject: [PATCH 08/16] Undo breaking change --- src/app/features/lobby/Lobby.tsx | 53 ++++++++++++++++++++++++---- src/app/pages/client/space/Space.tsx | 20 +++++++---- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 6851873f..709334a5 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -199,10 +199,18 @@ export function Lobby() { * * @param spaceId - The root space ID. * @param parentId - The parent space ID to start the check from. + * @param previousId - The last ID checked, only used to ignore root collapse state. * @returns True if parentId or all ancestors is in a closed category. */ const getInClosedCategories = useCallback( - (spaceId: string, parentId: string): boolean => { + (spaceId: string, parentId: string, previousId?: string): boolean => { + // Ignore root space being collapsed if in a subspace, + // this is due to many spaces dumping all rooms in the top-level space. + if (parentId === spaceId) { + if (previousId) { + if (getRoom(previousId)?.isSpaceRoom() || spaceRooms.has(previousId)) return false; + } + } if (closedCategories.has(makeLobbyCategoryId(spaceId, parentId))) { return true; } @@ -214,16 +222,40 @@ export function Lobby() { let anyOpen = false; parentParentIds.forEach((id) => { - if (!getInClosedCategories(spaceId, id)) { + if (!getInClosedCategories(spaceId, id, parentId)) { anyOpen = true; } }); return !anyOpen; }, - [closedCategories, roomToParents] + [closedCategories, getRoom, roomToParents, spaceRooms] ); + /** + * Determines whether all parent categories are collapsed. + * + * @param spaceId - The root space ID. + * @param roomId - The room ID to start the check from. + * @returns True if every parent category is collapsed; false otherwise. + */ + + const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { + const parentIds = roomToParents.get(roomId); + + if (!parentIds || parentIds.size === 0) { + return false; + } + + let allCollapsed = true; + parentIds.forEach((id) => { + if (!getInClosedCategories(spaceId, id, roomId)) { + allCollapsed = false; + } + }); + return allCollapsed; + }; + const [draggingItem, setDraggingItem] = useState(); const hierarchy = useSpaceHierarchy( space.roomId, @@ -443,9 +475,18 @@ export function Lobby() { [setSpaceRooms] ); - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => - closedCategories.has(categoryId) - ); + const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { + const collapsed = closedCategories.has(categoryId); + const [spaceId, roomId] = categoryId.split('|').slice(-2); + + // Only prevent collapsing if all parents are collapsed + const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); + + if (toggleable) { + return collapsed; + } + return !collapsed; + }); const handleOpenRoom: MouseEventHandler = (evt) => { const rId = evt.currentTarget.getAttribute('data-room-id'); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index bb79dcac..c99ed4bc 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -321,7 +321,15 @@ export function Space() { * @returns True if parentId or all ancestors is in a closed category. */ const getInClosedCategories = useCallback( - (spaceId: string, parentId: string): boolean => { + (spaceId: string, parentId: string, previousId?: string): boolean => { + // Ignore root space being collapsed if in a subspace, + // this is due to many spaces dumping all rooms in the top-level space. + if (parentId === spaceId) { + if (previousId) { + if (getRoom(previousId)?.isSpaceRoom()) return false; + } + } + if (closedCategories.has(makeNavCategoryId(spaceId, parentId))) { return true; } @@ -333,14 +341,14 @@ export function Space() { let anyOpen = false; parentParentIds.forEach((id) => { - if (!getInClosedCategories(spaceId, id)) { + if (!getInClosedCategories(spaceId, id, parentId)) { anyOpen = true; } }); return !anyOpen; }, - [closedCategories, roomToParents] + [closedCategories, getRoom, roomToParents] ); // There are better ways to do this @@ -402,7 +410,7 @@ export function Space() { let allCollapsed = true; parentIds.forEach((id) => { - if (!getInClosedCategories(spaceId, id)) { + if (!getInClosedCategories(spaceId, id, roomId)) { allCollapsed = false; } }); @@ -414,7 +422,7 @@ export function Space() { getRoom, useCallback( (parentId, roomId) => { - if (!getInClosedCategories(space.roomId, parentId)) { + if (!getInClosedCategories(space.roomId, parentId, roomId)) { return false; } if (getContainsShowRoom(roomId)) return false; @@ -423,7 +431,7 @@ export function Space() { [getContainsShowRoom, getInClosedCategories, space.roomId] ), useCallback( - (sId) => getInClosedCategories(space.roomId, sId), + (sId) => getInClosedCategories(space.roomId, sId, sId), [getInClosedCategories, space.roomId] ) ); From e417b18835311a828a4a360853f707d55ec90903 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Mon, 30 Jun 2025 16:31:40 +0200 Subject: [PATCH 09/16] minor changes --- src/app/features/lobby/Lobby.tsx | 1 - src/app/pages/client/space/Space.tsx | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 709334a5..ede695b4 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -239,7 +239,6 @@ export function Lobby() { * @param roomId - The room ID to start the check from. * @returns True if every parent category is collapsed; false otherwise. */ - const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { const parentIds = roomToParents.get(roomId); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index c99ed4bc..cb94b8af 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -318,6 +318,7 @@ export function Space() { * * @param spaceId - The root space ID. * @param parentId - The parent space ID to start the check from. + * @param previousId - The last ID checked, only used to ignore root collapse state. * @returns True if parentId or all ancestors is in a closed category. */ const getInClosedCategories = useCallback( @@ -354,13 +355,14 @@ export function Space() { // There are better ways to do this const roomToChildren = useMemo(() => { const map = new Map>(); + roomToParents.forEach((parentSet, childId) => { parentSet.forEach((parentId) => { if (!map.has(parentId)) map.set(parentId, new Set()); - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - map.get(parentId)!.add(childId); + map.get(parentId)?.add(childId); }); }); + return map; }, [roomToParents]); @@ -400,10 +402,8 @@ export function Space() { * @param roomId - The room ID to start the check from. * @returns True if every parent category is collapsed; false otherwise. */ - const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { const parentIds = roomToParents.get(roomId); - if (!parentIds || parentIds.size === 0) { return false; } @@ -431,7 +431,7 @@ export function Space() { [getContainsShowRoom, getInClosedCategories, space.roomId] ), useCallback( - (sId) => getInClosedCategories(space.roomId, sId, sId), + (sId) => getInClosedCategories(space.roomId, sId), [getInClosedCategories, space.roomId] ) ); From 8580c2d3049cd8e7da500c1117c7fa6fdea1f4b1 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Mon, 30 Jun 2025 16:39:01 +0200 Subject: [PATCH 10/16] Revert "Fix room header members icon not filled when enabled" This reverts commit c0799142d6f667c782c0551e3625e6eca1512ae9. --- src/app/features/room/RoomViewHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index edc3170e..352ae4b5 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -34,7 +34,7 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { StateEvent } from '../../../types/matrix/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoom } from '../../hooks/useRoom'; -import { useSetting } from '../../state/hooks/settings'; +import { useSetSetting, useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils'; @@ -230,7 +230,7 @@ export function RoomViewHeader() { ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; - const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); + const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const handleSearchClick = () => { const searchParams: _SearchPathSearchParams = { @@ -410,7 +410,7 @@ export function RoomViewHeader() { > {(triggerRef) => ( setPeopleDrawer((drawer) => !drawer)}> - + )} From 9b5ce37743b6195f1eec5fb8826cf7da710ac41d Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Thu, 3 Jul 2025 22:51:26 +0200 Subject: [PATCH 11/16] bugfix and polishing --- src/app/features/lobby/Lobby.tsx | 13 +++++++++---- src/app/pages/client/space/Space.tsx | 20 ++++---------------- src/app/state/closedLobbyCategories.ts | 2 ++ src/app/state/closedNavCategories.ts | 2 ++ src/app/state/room/roomToChildren.ts | 16 ++++++++++++++++ 5 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/app/state/room/roomToChildren.ts diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index ede695b4..5dd54fa0 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -32,7 +32,7 @@ import { useRoomsPowerLevels, } from '../../hooks/usePowerLevels'; import { mDirectAtom } from '../../state/mDirectList'; -import { makeLobbyCategoryId } from '../../state/closedLobbyCategories'; +import { makeLobbyCategoryId, getLobbyCategoryIdParts } from '../../state/closedLobbyCategories'; import { useCategoryHandler } from '../../hooks/useCategoryHandler'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { allRoomsAtom } from '../../state/room-list/roomList'; @@ -74,6 +74,11 @@ const useCanDropLobbyItem = ( const containerSpaceId = space.roomId; + // only allow to be dropped in parent space + if (item.parentId !== container.item.roomId && item.parentId !== container.item.parentId) { + return false; + } + if ( getRoom(containerSpaceId) === undefined || !canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {}) @@ -368,7 +373,7 @@ export function Lobby() { // remove from current space if (item.parentId !== containerParentId) { - mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId); + await mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId); } if ( @@ -388,7 +393,7 @@ export function Lobby() { joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? []; allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId }); - mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, { + await mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, { ...joinRuleContent, allow, }); @@ -476,7 +481,7 @@ export function Lobby() { const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { const collapsed = closedCategories.has(categoryId); - const [spaceId, roomId] = categoryId.split('|').slice(-2); + const [spaceId, roomId] = getLobbyCategoryIdParts(categoryId); // Only prevent collapsing if all parents are collapsed const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index cb94b8af..015e014e 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -45,7 +45,7 @@ import { import { useSpace } from '../../../hooks/useSpace'; import { VirtualTile } from '../../../components/virtualizer'; import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav'; -import { makeNavCategoryId } from '../../../state/closedNavCategories'; +import { makeNavCategoryId, getNavCategoryIdParts } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; @@ -57,6 +57,7 @@ import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels import { openInviteUser } from '../../../../client/action/navigation'; import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; +import { roomToChildrenAtom } from '../../../state/room/roomToChildren'; import { markAsRead } from '../../../../client/action/notifications'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { UseStateProvider } from '../../../components/UseStateProvider'; @@ -293,6 +294,7 @@ export function Space() { const mDirects = useAtomValue(mDirectAtom); const roomToUnread = useAtomValue(roomToUnreadAtom); const roomToParents = useAtomValue(roomToParentsAtom); + const roomToChildren = useAtomValue(roomToChildrenAtom); const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); @@ -352,20 +354,6 @@ export function Space() { [closedCategories, getRoom, roomToParents] ); - // There are better ways to do this - const roomToChildren = useMemo(() => { - const map = new Map>(); - - roomToParents.forEach((parentSet, childId) => { - parentSet.forEach((parentId) => { - if (!map.has(parentId)) map.set(parentId, new Set()); - map.get(parentId)?.add(childId); - }); - }); - - return map; - }, [roomToParents]); - /** * Recursively checks if the given room or any of its descendants should be visible. * @@ -445,7 +433,7 @@ export function Space() { const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => { const collapsed = closedCategories.has(categoryId); - const [spaceId, roomId] = categoryId.split('|').slice(-2); + const [spaceId, roomId] = getNavCategoryIdParts(categoryId); // Only prevent collapsing if all parents are collapsed const toggleable = !getAllAncestorsCollapsed(spaceId, roomId); diff --git a/src/app/state/closedLobbyCategories.ts b/src/app/state/closedLobbyCategories.ts index 40ecd163..22bf2d76 100644 --- a/src/app/state/closedLobbyCategories.ts +++ b/src/app/state/closedLobbyCategories.ts @@ -66,3 +66,5 @@ export const makeClosedLobbyCategoriesAtom = (userId: string): ClosedLobbyCatego }; export const makeLobbyCategoryId = (...args: string[]): string => args.join('|'); + +export const getLobbyCategoryIdParts = (categoryId: string): string[] => categoryId.split('|'); diff --git a/src/app/state/closedNavCategories.ts b/src/app/state/closedNavCategories.ts index ea61cb2e..f2e39a27 100644 --- a/src/app/state/closedNavCategories.ts +++ b/src/app/state/closedNavCategories.ts @@ -66,3 +66,5 @@ export const makeClosedNavCategoriesAtom = (userId: string): ClosedNavCategories }; export const makeNavCategoryId = (...args: string[]): string => args.join('|'); + +export const getNavCategoryIdParts = (categoryId: string): string[] => categoryId.split('|'); diff --git a/src/app/state/room/roomToChildren.ts b/src/app/state/room/roomToChildren.ts new file mode 100644 index 00000000..ae0f4f24 --- /dev/null +++ b/src/app/state/room/roomToChildren.ts @@ -0,0 +1,16 @@ +import { atom } from 'jotai'; +import { roomToParentsAtom } from './roomToParents'; + +export const roomToChildrenAtom = atom((get) => { + const roomToParents = get(roomToParentsAtom); + const map = new Map>(); + + roomToParents.forEach((parentSet, childId) => { + parentSet.forEach((parentId) => { + if (!map.has(parentId)) map.set(parentId, new Set()); + map.get(parentId)?.add(childId); + }); + }); + + return map; +}); From 18082ff400b833b2b30a342580e9c3c35e296403 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Fri, 25 Jul 2025 16:38:44 +0200 Subject: [PATCH 12/16] show space header if subspaces contain rooms --- src/app/hooks/useSpaceHierarchy.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 54478505..0baba282 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -198,6 +198,29 @@ const getSpaceJoinedHierarchy = ( new Set() ); + /** + * Recursively checks if the given space or any of its descendants contain non-space rooms. + * + * @param roomId - The space ID to check. + * @returns True if the space or any descendant contains non-space rooms. + */ + const containsRoom = (spaceId: string) => { + const space = getRoom(spaceId); + if (!space) return false; + + const childEvents = getStateEvents(space, StateEvent.SpaceChild).filter(isValidChild); + + return childEvents.some((childEvent): boolean => { + const childId = childEvent.getStateKey(); + if (!childId || !isRoomId(childId)) return false; + const room = getRoom(childId); + if (!room) return false; + + if (!room.isSpaceRoom()) return true; + return containsRoom(childId); + }); + }; + const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => { const space = getRoom(spaceItem.roomId); if (!space) { @@ -213,7 +236,7 @@ const getSpaceJoinedHierarchy = ( return true; }); - if (joinedRoomEvents.length === 0) return []; + if (!containsRoom(spaceItem.roomId)) return []; const childItems: HierarchyItemRoom[] = []; joinedRoomEvents.forEach((childEvent) => { From b4d7f520c5abb7c35390cc76da5d6a188de1415c Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Fri, 25 Jul 2025 16:51:56 +0200 Subject: [PATCH 13/16] fix docstring --- src/app/hooks/useSpaceHierarchy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 0baba282..bec7a035 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -201,7 +201,7 @@ const getSpaceJoinedHierarchy = ( /** * Recursively checks if the given space or any of its descendants contain non-space rooms. * - * @param roomId - The space ID to check. + * @param spaceId - The space ID to check. * @returns True if the space or any descendant contains non-space rooms. */ const containsRoom = (spaceId: string) => { From df8ba32725a3d03508558eb2ce43bd8dce31b89d Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Sun, 27 Jul 2025 16:33:49 +0200 Subject: [PATCH 14/16] improve function naming --- src/app/hooks/useSpaceHierarchy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index bec7a035..62cca99b 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -204,7 +204,7 @@ const getSpaceJoinedHierarchy = ( * @param spaceId - The space ID to check. * @returns True if the space or any descendant contains non-space rooms. */ - const containsRoom = (spaceId: string) => { + const getContainsRoom = (spaceId: string) => { const space = getRoom(spaceId); if (!space) return false; @@ -217,7 +217,7 @@ const getSpaceJoinedHierarchy = ( if (!room) return false; if (!room.isSpaceRoom()) return true; - return containsRoom(childId); + return getContainsRoom(childId); }); }; @@ -236,7 +236,7 @@ const getSpaceJoinedHierarchy = ( return true; }); - if (!containsRoom(spaceItem.roomId)) return []; + if (!getContainsRoom(spaceItem.roomId)) return []; const childItems: HierarchyItemRoom[] = []; joinedRoomEvents.forEach((childEvent) => { From 4947efe2744bf224e89cea644fd9df58ea421687 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Mon, 28 Jul 2025 16:42:50 +0200 Subject: [PATCH 15/16] improve performance --- src/app/features/lobby/Lobby.tsx | 22 +++++++++++++++++--- src/app/hooks/useSpaceHierarchy.ts | 3 ++- src/app/pages/client/space/Space.tsx | 31 ++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 5dd54fa0..ef6ea8ef 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react'; +import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useAtom, useAtomValue } from 'jotai'; @@ -199,6 +199,11 @@ export function Lobby() { [mx] ); + const closedCategoriesCache = useRef(new Map()); + useEffect(() => { + closedCategoriesCache.current.clear(); + }, [closedCategories, roomToParents, getRoom]); + /** * Recursively checks if a given parentId (or all its ancestors) is in a closed category. * @@ -209,19 +214,29 @@ export function Lobby() { */ const getInClosedCategories = useCallback( (spaceId: string, parentId: string, previousId?: string): boolean => { + const categoryId = makeLobbyCategoryId(spaceId, parentId); + if (closedCategoriesCache.current.has(categoryId)) { + return closedCategoriesCache.current.get(categoryId); + } + // Ignore root space being collapsed if in a subspace, // this is due to many spaces dumping all rooms in the top-level space. if (parentId === spaceId) { if (previousId) { - if (getRoom(previousId)?.isSpaceRoom() || spaceRooms.has(previousId)) return false; + if (getRoom(previousId)?.isSpaceRoom() || spaceRooms.has(previousId)) { + closedCategoriesCache.current.set(categoryId, false); + return false; + } } } - if (closedCategories.has(makeLobbyCategoryId(spaceId, parentId))) { + if (closedCategories.has(categoryId)) { + closedCategoriesCache.current.set(categoryId, true); return true; } const parentParentIds = roomToParents.get(parentId); if (!parentParentIds || parentParentIds.size === 0) { + closedCategoriesCache.current.set(categoryId, false); return false; } @@ -232,6 +247,7 @@ export function Lobby() { } }); + closedCategoriesCache.current.set(categoryId, !anyOpen); return !anyOpen; }, [closedCategories, getRoom, roomToParents, spaceRooms] diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts index 62cca99b..52dd12b7 100644 --- a/src/app/hooks/useSpaceHierarchy.ts +++ b/src/app/hooks/useSpaceHierarchy.ts @@ -208,9 +208,10 @@ const getSpaceJoinedHierarchy = ( const space = getRoom(spaceId); if (!space) return false; - const childEvents = getStateEvents(space, StateEvent.SpaceChild).filter(isValidChild); + const childEvents = getStateEvents(space, StateEvent.SpaceChild); return childEvents.some((childEvent): boolean => { + if (!isValidChild(childEvent)) return false; const childId = childEvent.getStateKey(); if (!childId || !isRoomId(childId)) return false; const room = getRoom(childId); diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 5ad9cf68..6e8c51b8 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -2,6 +2,7 @@ import React, { MouseEventHandler, forwardRef, useCallback, + useEffect, useMemo, useRef, useState, @@ -315,6 +316,13 @@ export function Space() { [mx, allJoinedRooms] ); + const closedCategoriesCache = useRef(new Map()); + const ancestorsCollapsedCache = useRef(new Map()); + useEffect(() => { + closedCategoriesCache.current.clear(); + ancestorsCollapsedCache.current.clear(); + }, [closedCategories, roomToParents, getRoom]); + /** * Recursively checks if a given parentId (or all its ancestors) is in a closed category. * @@ -325,20 +333,30 @@ export function Space() { */ const getInClosedCategories = useCallback( (spaceId: string, parentId: string, previousId?: string): boolean => { + const categoryId = makeNavCategoryId(spaceId, parentId); + if (closedCategoriesCache.current.has(categoryId)) { + return closedCategoriesCache.current.get(categoryId); + } + // Ignore root space being collapsed if in a subspace, // this is due to many spaces dumping all rooms in the top-level space. if (parentId === spaceId) { if (previousId) { - if (getRoom(previousId)?.isSpaceRoom()) return false; + if (getRoom(previousId)?.isSpaceRoom()) { + closedCategoriesCache.current.set(categoryId, false); + return false; + } } } - if (closedCategories.has(makeNavCategoryId(spaceId, parentId))) { + if (closedCategories.has(categoryId)) { + closedCategoriesCache.current.set(categoryId, true); return true; } const parentParentIds = roomToParents.get(parentId); if (!parentParentIds || parentParentIds.size === 0) { + closedCategoriesCache.current.set(categoryId, false); return false; } @@ -349,6 +367,7 @@ export function Space() { } }); + closedCategoriesCache.current.set(categoryId, !anyOpen); return !anyOpen; }, [closedCategories, getRoom, roomToParents] @@ -391,8 +410,14 @@ export function Space() { * @returns True if every parent category is collapsed; false otherwise. */ const getAllAncestorsCollapsed = (spaceId: string, roomId: string): boolean => { + const categoryId = makeNavCategoryId(spaceId, roomId); + if (ancestorsCollapsedCache.current.has(categoryId)) { + return ancestorsCollapsedCache.current.get(categoryId); + } + const parentIds = roomToParents.get(roomId); if (!parentIds || parentIds.size === 0) { + ancestorsCollapsedCache.current.set(categoryId, false); return false; } @@ -402,6 +427,8 @@ export function Space() { allCollapsed = false; } }); + + ancestorsCollapsedCache.current.set(categoryId, allCollapsed); return allCollapsed; }; From 24b3b9cf52814d72b790703f30a9a5ae12cdbc61 Mon Sep 17 00:00:00 2001 From: Gimle Larpes Date: Wed, 17 Sep 2025 00:06:36 +0200 Subject: [PATCH 16/16] clean up conflicts --- src/app/features/lobby/Lobby.tsx | 10 ---------- src/app/features/lobby/SpaceItem.tsx | 7 +++---- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 3a3cd0b6..2589f21a 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -194,16 +194,6 @@ export function Lobby() { const getRoom = useGetRoom(allJoinedRooms); - const canEditSpaceChild = useCallback( - (powerLevels: IPowerLevels) => - powerLevelAPI.canSendStateEvent( - powerLevels, - StateEvent.SpaceChild, - powerLevelAPI.getPowerLevel(powerLevels, mx.getUserId() ?? undefined) - ), - [mx] - ); - const closedCategoriesCache = useRef(new Map()); useEffect(() => { closedCategoriesCache.current.clear(); diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index 8177c4af..77b349bd 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -362,7 +362,6 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { } > - {item.parentId === undefined ? ( Add Space - {addExisting && ( - setAddExisting(false)} /> - )} ) : ( )} + {addExisting && ( + setAddExisting(false)} /> + )} ); }