Skip to content

Commit 6040280

Browse files
dbkrt3chguy
andauthored
Update for compatibility with v12 rooms (#30452)
* Update for compatibility with v12 rooms Stop using powerLevelNorm and reading PL events manually. To support matrix-org/matrix-js-sdk#4937 * Add test for leave space dialog * Don't compute stuff if we don't need it * Use room.client * Use getSafeUserId * Remove client arg * Use getJoinedMembers and add doc * Fix tests * Fix more tests * Fix other test * Clarify comment Co-authored-by: Michael Telatynski <[email protected]> --------- Co-authored-by: Michael Telatynski <[email protected]>
1 parent 12927cc commit 6040280

File tree

5 files changed

+105
-33
lines changed

5 files changed

+105
-33
lines changed

src/components/structures/MatrixChat.tsx

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor
141141
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
142142
import Markdown from "../../Markdown";
143143
import { sanitizeHtmlParams } from "../../Linkify";
144+
import { isOnlyAdmin } from "../../utils/membership";
144145

145146
// legacy export
146147
export { default as Views } from "../../Views";
@@ -1255,29 +1256,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
12551256

12561257
const client = MatrixClientPeg.get();
12571258
if (client && roomToLeave) {
1258-
const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, "");
1259-
const plContent = plEvent ? plEvent.getContent() : {};
1260-
const userLevels = plContent.users || {};
1261-
const currentUserLevel = userLevels[client.getUserId()!];
1262-
const userLevelValues = Object.values(userLevels);
1263-
if (userLevelValues.every((x) => typeof x === "number")) {
1259+
// If the user is the only user with highest power level
1260+
if (isOnlyAdmin(roomToLeave)) {
1261+
const userLevelValues = roomToLeave.getJoinedMembers().map((m) => m.powerLevel);
1262+
12641263
const maxUserLevel = Math.max(...(userLevelValues as number[]));
1265-
// If the user is the only user with highest power level
1266-
if (
1267-
maxUserLevel === currentUserLevel &&
1268-
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
1269-
) {
1270-
const warning =
1271-
maxUserLevel >= 100
1272-
? _t("leave_room_dialog|room_leave_admin_warning")
1273-
: _t("leave_room_dialog|room_leave_mod_warning");
1274-
warnings.push(
1275-
<strong className="warning" key="last_admin_warning">
1276-
{" " /* Whitespace, otherwise the sentences get smashed together */}
1277-
{warning}
1278-
</strong>,
1279-
);
1280-
}
1264+
1265+
const warning =
1266+
maxUserLevel >= 100
1267+
? _t("leave_room_dialog|room_leave_admin_warning")
1268+
: _t("leave_room_dialog|room_leave_mod_warning");
1269+
warnings.push(
1270+
<strong className="warning" key="last_admin_warning">
1271+
{" " /* Whitespace, otherwise the sentences get smashed together */}
1272+
{warning}
1273+
</strong>,
1274+
);
12811275
}
12821276
}
12831277

src/components/views/dialogs/LeaveSpaceDialog.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,13 @@ import BaseDialog from "../dialogs/BaseDialog";
1515
import SpaceStore from "../../../stores/spaces/SpaceStore";
1616
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
1717
import { filterBoolean } from "../../../utils/arrays";
18+
import { isOnlyAdmin } from "../../../utils/membership";
1819

1920
interface IProps {
2021
space: Room;
2122
onFinished(leave: boolean, rooms?: Room[]): void;
2223
}
2324

24-
const isOnlyAdmin = (room: Room): boolean => {
25-
const userId = room.client.getSafeUserId();
26-
if (room.getMember(userId)?.powerLevelNorm !== 100) {
27-
return false; // user is not an admin
28-
}
29-
return room.getJoinedMembers().every((member) => {
30-
// return true if every other member has a lower power level (we are highest)
31-
return member.userId === userId || member.powerLevelNorm < 100;
32-
});
33-
};
34-
3525
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
3626
const spaceChildren = useMemo(() => {
3727
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));

src/utils/membership.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,23 @@ export async function waitForMember(
131131
client.removeListener(RoomStateEvent.NewMember, handler);
132132
});
133133
}
134+
135+
/**
136+
* Check if the user is the only joined admin in the room
137+
* This function will *not* cause lazy loading of room members, so if these should be included then
138+
* the caller needs to make sure members have been loaded.
139+
* @param room The room to check if the user is the only admin.
140+
* @returns True if the user is the only user with the highest power level, false otherwise
141+
*/
142+
export function isOnlyAdmin(room: Room): boolean {
143+
const currentUserLevel = room.getMember(room.client.getSafeUserId())?.powerLevel;
144+
145+
const userLevelValues = room.getJoinedMembers().map((m) => m.powerLevel);
146+
147+
const maxUserLevel = Math.max(...userLevelValues.filter((x) => typeof x === "number"));
148+
// If the user is the only user with highest power level
149+
return (
150+
maxUserLevel === currentUserLevel &&
151+
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
152+
);
153+
}

test/unit-tests/components/structures/MatrixChat-test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,8 @@ describe("<MatrixChat />", () => {
691691
jest.spyOn(spaceRoom, "isSpaceRoom").mockReturnValue(true);
692692

693693
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
694+
(room as any).client = mockClient;
695+
(spaceRoom as any).client = mockClient;
694696
});
695697

696698
describe("forget_room", () => {
@@ -775,6 +777,22 @@ describe("<MatrixChat />", () => {
775777
),
776778
).toBeInTheDocument();
777779
});
780+
it("should warn when user is the last admin", async () => {
781+
jest.spyOn(room, "getJoinedMembers").mockReturnValue([
782+
{ powerLevel: 100 } as unknown as MatrixJs.RoomMember,
783+
{ powerLevel: 0 } as unknown as MatrixJs.RoomMember,
784+
]);
785+
jest.spyOn(room, "getMember").mockReturnValue({
786+
powerLevel: 100,
787+
} as unknown as MatrixJs.RoomMember);
788+
dispatchAction();
789+
await screen.findByRole("dialog");
790+
expect(
791+
screen.getByText(
792+
"You're the only administrator in this room. If you leave, nobody will be able to change room settings or take other important actions.",
793+
),
794+
).toBeInTheDocument();
795+
});
778796
it("should do nothing on cancel", async () => {
779797
dispatchAction();
780798
const dialog = await screen.findByRole("dialog");
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { render, screen } from "jest-matrix-react";
10+
import { MatrixEvent, type RoomMember } from "matrix-js-sdk/src/matrix";
11+
12+
import LeaveSpaceDialog from "../../../../../src/components/views/dialogs/LeaveSpaceDialog";
13+
import { createTestClient, mkStubRoom } from "../../../../test-utils";
14+
15+
describe("LeaveSpaceDialog", () => {
16+
it("should warn about not being able to rejoin non-public space", () => {
17+
const mockClient = createTestClient();
18+
const mockSpace = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
19+
jest.spyOn(mockSpace.currentState, "getStateEvents").mockReturnValue(
20+
new MatrixEvent({
21+
type: "m.room.join_rules",
22+
content: {
23+
join_rule: "invite",
24+
},
25+
}),
26+
);
27+
28+
render(<LeaveSpaceDialog space={mockSpace} onFinished={jest.fn()} />);
29+
30+
expect(screen.getByText(/You won't be able to rejoin unless you are re-invited/)).toBeInTheDocument();
31+
});
32+
33+
it("should warn if user is the only admin", () => {
34+
const mockClient = createTestClient();
35+
const mockSpace = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any;
36+
jest.spyOn(mockSpace, "getJoinedMembers").mockReturnValue([
37+
{ powerLevel: 100 } as unknown as RoomMember,
38+
{ powerLevel: 0 } as unknown as RoomMember,
39+
]);
40+
jest.spyOn(mockSpace, "getMember").mockReturnValue({
41+
powerLevel: 100,
42+
} as unknown as RoomMember);
43+
44+
render(<LeaveSpaceDialog space={mockSpace} onFinished={jest.fn()} />);
45+
46+
expect(
47+
screen.getByText(/You're the only admin of this space. Leaving it will mean no one has control over it./),
48+
).toBeInTheDocument();
49+
});
50+
});

0 commit comments

Comments
 (0)