Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c079914
Fix room header members icon not filled when enabled
GimleLarpes Jun 19, 2025
2fae418
fix hierarchy indenting and order
GimleLarpes Jun 27, 2025
6b3841c
Partially fix collapse behaviour
GimleLarpes Jun 28, 2025
a9fa1aa
remove getInCollapedCategories
GimleLarpes Jun 28, 2025
2570e5e
Merge branch 'cinnyapp:dev' into improve-space
GimleLarpes Jun 28, 2025
6ff0260
fix collapse behaviour
GimleLarpes Jun 28, 2025
a55065b
bugfix
GimleLarpes Jun 29, 2025
64fc306
improve lobby
GimleLarpes Jun 29, 2025
56dfc6a
Undo breaking change
GimleLarpes Jun 30, 2025
e417b18
minor changes
GimleLarpes Jun 30, 2025
8580c2d
Revert "Fix room header members icon not filled when enabled"
GimleLarpes Jun 30, 2025
9b5ce37
bugfix and polishing
GimleLarpes Jul 3, 2025
0f3f4a8
Merge branch 'cinnyapp:dev' into improve-space
GimleLarpes Jul 4, 2025
c0056ea
Merge branch 'dev' into improve-space
GimleLarpes Jul 5, 2025
ae75ee7
Merge branch 'dev' into improve-space
GimleLarpes Jul 17, 2025
18082ff
show space header if subspaces contain rooms
GimleLarpes Jul 25, 2025
10a2f8c
Merge branch 'dev' into improve-space
GimleLarpes Jul 25, 2025
b4d7f52
fix docstring
GimleLarpes Jul 25, 2025
df8ba32
improve function naming
GimleLarpes Jul 27, 2025
71e775f
Merge branch 'dev' into improve-space
GimleLarpes Jul 27, 2025
4947efe
improve performance
GimleLarpes Jul 28, 2025
b9ce251
Merge branch 'dev' into improve-space
GimleLarpes Sep 16, 2025
24b3b9c
clean up conflicts
GimleLarpes Sep 16, 2025
30f809e
Merge branch 'cinnyapp:dev' into improve-space
GimleLarpes Jan 29, 2026
9fc2452
Merge branch 'dev' into improve-space
GimleLarpes Feb 12, 2026
d7f85de
Documentation, fix recursion error
GimleLarpes Feb 12, 2026
dce97cf
minor formatting
GimleLarpes Feb 12, 2026
056adb8
Use spaceRooms in Space, bugfixing, fix Lobby recursion
GimleLarpes Feb 13, 2026
1333ee0
optimizations
GimleLarpes Feb 13, 2026
694ca21
fix cache in Lobby
GimleLarpes Feb 13, 2026
8d93671
support multiple reuse of spaces, fix recursion errors when encounter…
GimleLarpes Feb 14, 2026
61c0453
Merge commit '8d93671b' from GimleLarpes/cinny into improve-space
KaceCottam Mar 13, 2026
bf36587
Update Space sidebar with depth-based link to space lobby
KaceCottam Mar 14, 2026
1445f3f
Merge branch 'dev' into improve-space
KaceCottam Mar 14, 2026
8c4f5e5
Initial modification of Space navbar to have svg thread lines
KaceCottam Mar 15, 2026
515b378
Added setting to control depth limit and cleaned up the code
KaceCottam Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/app/features/add-existing/AddExisting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,39 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
const allRoomsSet = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allRoomsSet);

/**
* Recursively checks if a given sourceId room is an ancestor to the targetId space.
*
* @param sourceId - The room to check.
* @param targetId - The space ID to check against.
* @param visited - Set used to prevent recursion errors.
* @returns True if rId is an ancestor of targetId.
*/
const isAncestor = useCallback(
(sourceId: string, targetId: string, visited: Set<string> = new Set()): boolean => {
// Prevent infinite recursion
if (visited.has(targetId)) return false;
visited.add(targetId);

const parentIds = roomIdToParents.get(targetId);
if (!parentIds) return false;

if (parentIds.has(sourceId)) {
return true;
}

return Array.from(parentIds).some((id) => isAncestor(sourceId, id, visited));
},
[roomIdToParents]
);

const allItems: string[] = useMemo(() => {
const rIds = space ? [...spaces] : [...rooms, ...directs];

return rIds
.filter((rId) => rId !== parentId && !roomIdToParents.get(rId)?.has(parentId))
.filter((rId) => rId !== parentId && !isAncestor(rId, parentId))
.sort(factoryRoomIdByAtoZ(mx));
}, [spaces, rooms, directs, space, parentId, roomIdToParents, mx]);
}, [space, spaces, rooms, directs, mx, parentId, isAncestor]);

const getRoomNameStr: SearchItemStrGetter<string> = useCallback(
(rId) => getRoom(rId)?.name ?? rId,
Expand Down
123 changes: 111 additions & 12 deletions src/app/features/lobby/Lobby.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { 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';
Expand Down Expand Up @@ -28,7 +28,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';
Expand Down Expand Up @@ -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;
}

const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
Expand Down Expand Up @@ -167,6 +172,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))
);
Expand All @@ -188,16 +194,95 @@ export function Lobby() {

const getRoom = useGetRoom(allJoinedRooms);

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.
*
* @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.
* @param visited - Set used to prevent recursion errors.
* @returns True if parentId or all ancestors is in a closed category.
*/
const getInClosedCategories = useCallback(
(
spaceId: string,
parentId: string,
previousId?: string,
visited: Set<string> = new Set()
): 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 && previousId) {
if (spaceRooms.has(previousId) || getRoom(previousId)?.isSpaceRoom()) {
return false;
}
}

const categoryId = makeLobbyCategoryId(spaceId, parentId);

// Prevent infinite recursion
if (visited.has(categoryId)) return false;
visited.add(categoryId);

if (closedCategoriesCache.current.has(categoryId)) {
return closedCategoriesCache.current.get(categoryId);
}

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;
}

// As a subspace can be in multiple spaces,
// only return true if all parent spaces are closed.
const allClosed = !Array.from(parentParentIds).some(
(id) => !getInClosedCategories(spaceId, id, parentId, visited)
);
visited.delete(categoryId);
closedCategoriesCache.current.set(categoryId, allClosed);
return allClosed;
},
[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;
}

return !Array.from(parentIds).some((id) => !getInClosedCategories(spaceId, id, roomId));
};

const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
const hierarchy = useSpaceHierarchy(
space.roomId,
spaceRooms,
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]
)
);

Expand Down Expand Up @@ -298,7 +383,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 (
Expand All @@ -318,7 +403,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,
});
Expand Down Expand Up @@ -404,9 +489,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] = getLobbyCategoryIdParts(categoryId);

// Prevent collapsing if all parents are collapsed
const toggleable = !getAllAncestorsCollapsed(spaceId, roomId);

if (toggleable) {
return collapsed;
}
return !collapsed;
});

const handleOpenRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
const rId = evt.currentTarget.getAttribute('data-room-id');
Expand Down Expand Up @@ -468,14 +562,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 (
<VirtualTile
virtualItem={vItem}
style={{
paddingTop: vItem.index === 0 ? 0 : config.space.S500,
paddingLeft,
}}
ref={virtualizer.measureElement}
key={vItem.index}
Expand All @@ -489,8 +589,7 @@ export function Lobby() {
roomsPowerLevels={roomsPowerLevels}
categoryId={categoryId}
closed={
closedCategories.has(categoryId) ||
(draggingItem ? 'space' in draggingItem : false)
inClosedCategory || (draggingItem ? 'space' in draggingItem : false)
}
handleClose={handleCategoryClick}
draggingItem={draggingItem}
Expand Down
50 changes: 40 additions & 10 deletions src/app/features/lobby/SpaceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
MenuItem,
RectCords,
config,
IconButton,
TooltipProvider,
Tooltip,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
Expand Down Expand Up @@ -370,15 +373,42 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
</FocusTrap>
}
>
<Chip
variant="SurfaceVariant"
radii="Pill"
before={<Icon src={Icons.Plus} size="50" />}
onClick={handleAddSpace}
aria-pressed={!!cords}
>
<Text size="B300">Add Space</Text>
</Chip>
{item.parentId === undefined ? (
<Chip
variant="SurfaceVariant"
radii="Pill"
before={<Icon src={Icons.Plus} size="50" />}
onClick={handleAddSpace}
aria-pressed={!!cords}
>
<Text size="B300">Add Space</Text>
</Chip>
) : (
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>Add Space</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={handleAddSpace}
aria-pressed={!!cords}
aria-label="Add Space"
variant="SurfaceVariant"
fill="None"
size="300"
radii="300"
>
<Icon size="50" src={Icons.SpacePlus} />
</IconButton>
)}
</TooltipProvider>
)}
{addExisting && (
<AddExistingModal space parentId={item.roomId} requestClose={() => setAddExisting(false)} />
)}
Expand Down Expand Up @@ -502,7 +532,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
{space && canEditChild && (
<Box shrink="No" alignItems="Inherit" gap="200">
<AddRoomButton item={item} />
{item.parentId === undefined && <AddSpaceButton item={item} />}
<AddSpaceButton item={item} />
</Box>
)}
</Box>
Expand Down
6 changes: 3 additions & 3 deletions src/app/features/room-nav/RoomNavCategoryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
<Chip
className={classNames(css.CategoryButton, className)}
variant="Background"
radii="Pill"
before={
radii="400"
after={
<Icon
className={css.CategoryButtonIcon}
size="50"
Expand All @@ -18,7 +18,7 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
{...props}
ref={ref}
>
<Text size="O400" priority="300" truncate>
<Text size="B400" priority="300" truncate>
{children}
</Text>
</Chip>
Expand Down
Loading