Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 56 additions & 57 deletions src/components/structures/SpaceHierarchy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
RoomType,
GuestAccess,
HistoryVisibility,
type HierarchyRelation,
type HierarchyRoom,
JoinRule,
} from "matrix-js-sdk/src/matrix";
Expand Down Expand Up @@ -71,6 +70,7 @@ import { getTopic } from "../../hooks/room/useTopic";
import { SdkContextClass } from "../../contexts/SDKContext";
import { getDisplayAliasForAliasSet } from "../../Rooms";
import SettingsStore from "../../settings/SettingsStore";
import { filterBoolean } from "../../utils/arrays.ts";

interface IProps {
space: Room;
Expand Down Expand Up @@ -504,68 +504,67 @@ export const HierarchyLevel: React.FC<IHierarchyLevelProps> = ({
const space = cli.getRoom(root.room_id);
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getSafeUserId());

const sortedChildren = sortBy(root.children_state, (ev) => {
return getChildOrder(ev.content.order, ev.origin_server_ts, ev.state_key);
});

const [subspaces, childRooms] = sortedChildren.reduce(
(result, ev: HierarchyRelation) => {
const room = hierarchy.roomMap.get(ev.state_key);
if (room && roomSet.has(room)) {
result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room, hierarchy));
}
return result;
},
[[] as HierarchyRoom[], [] as HierarchyRoom[]],
const sortedChildren = filterBoolean(
sortBy(root.children_state, (ev) => {
return getChildOrder(ev.content.order, ev.origin_server_ts, ev.state_key);
}).map((ev) => {
const hierarchyRoom = hierarchy.roomMap.get(ev.state_key);
if (!hierarchyRoom || !roomSet.has(hierarchyRoom)) return null;
// Find the most up-to-date info for this room, if it has been upgraded and we know about it.
return toLocalRoom(cli, hierarchyRoom, hierarchy);
}),
);

const newParents = new Set(parents).add(root.room_id);
return (
<React.Fragment>
{uniqBy(childRooms, "room_id").map((room) => (
<Tile
key={room.room_id}
room={room}
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
onViewRoomClick={() => onViewRoomClick(room.room_id, room.room_type as RoomType)}
onJoinRoomClick={() => onJoinRoomClick(room.room_id, newParents)}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
/>
))}

{subspaces
.filter((room) => !newParents.has(room.room_id))
.map((space) => (
<Tile
key={space.room_id}
room={space}
numChildRooms={
space.children_state.filter((ev) => {
const room = hierarchy.roomMap.get(ev.state_key);
return room && roomSet.has(room) && !room.room_type;
}).length
}
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
onViewRoomClick={() => onViewRoomClick(space.room_id, RoomType.Space)}
onJoinRoomClick={() => onJoinRoomClick(space.room_id, newParents)}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
>
<HierarchyLevel
root={space}
roomSet={roomSet}
hierarchy={hierarchy}
parents={newParents}
selectedMap={selectedMap}
onViewRoomClick={onViewRoomClick}
onJoinRoomClick={onJoinRoomClick}
onToggleClick={onToggleClick}
{uniqBy(sortedChildren, "room_id").map((room) => {
if (room.room_type !== RoomType.Space) {
return (
<Tile
key={room.room_id}
room={room}
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
onViewRoomClick={() => onViewRoomClick(room.room_id, room.room_type as RoomType)}
onJoinRoomClick={() => onJoinRoomClick(room.room_id, newParents)}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
/>
</Tile>
))}
);
} else {
if (newParents.has(room.room_id)) return null; // prevent cycles
return (
<Tile
key={room.room_id}
room={room}
numChildRooms={
room.children_state.filter((ev) => {
const child = hierarchy.roomMap.get(ev.state_key);
return child && roomSet.has(child) && !child.room_type;
}).length
}
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
onViewRoomClick={() => onViewRoomClick(room.room_id, RoomType.Space)}
onJoinRoomClick={() => onJoinRoomClick(room.room_id, newParents)}
hasPermissions={hasPermissions}
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
>
<HierarchyLevel
root={room}
roomSet={roomSet}
hierarchy={hierarchy}
parents={newParents}
selectedMap={selectedMap}
onViewRoomClick={onViewRoomClick}
onJoinRoomClick={onJoinRoomClick}
onToggleClick={onToggleClick}
/>
</Tile>
);
}
})}
</React.Fragment>
);
};
Expand Down
36 changes: 35 additions & 1 deletion test/unit-tests/components/structures/SpaceHierarchy-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ describe("SpaceHierarchy", () => {
const room2 = mkStubRoom("room-id-3", "Room 2", client);
const space1 = mkStubRoom("space-id-4", "Space 2", client);
const room3 = mkStubRoom("room-id-5", "Room 3", client);
const space2 = mkStubRoom("space-id-6", "Space 3", client);
mocked(client.getRooms).mockReturnValue([root]);
mocked(client.getRoom).mockImplementation(
(roomId) => client.getRooms().find((room) => room.roomId === roomId) ?? null,
);
[room1, room2, space1, room3].forEach((r) => mocked(r.getMyMembership).mockReturnValue(KnownMembership.Leave));
[room1, room2, space1, room3, space2].forEach((r) =>
mocked(r.getMyMembership).mockReturnValue(KnownMembership.Leave),
);

const hierarchyRoot: HierarchyRoom = {
room_id: root.roomId,
Expand Down Expand Up @@ -324,5 +327,36 @@ describe("SpaceHierarchy", () => {
undefined,
);
});

it("should not render cycles", async () => {
const hierarchySpace2: HierarchyRoom = {
room_id: space2.roomId,
name: "Space with cycle",
num_joined_members: 1,
room_type: "m.space",
children_state: [
{
state_key: root.roomId,
content: { order: "1" },
origin_server_ts: 111,
type: "m.space.child",
sender: "@other:server",
},
],
world_readable: true,
guest_can_join: true,
};

mocked(client.getRoomHierarchy).mockResolvedValue({
rooms: [hierarchyRoot, hierarchyRoom1, hierarchyRoom2, hierarchySpace1, hierarchySpace2],
});

const { getAllByText, queryByText, asFragment } = render(getComponent());
// Wait for spinners to go away
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
expect(getAllByText("Nested space")).toHaveLength(1);
expect(queryByText("Space 1")).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});
});
Loading
Loading