Skip to content

Commit 9b905c7

Browse files
committed
feat: user info admin components more split and comments
1 parent 9ac12f9 commit 9b905c7

14 files changed

+1505
-1035
lines changed

src/components/viewmodels/right_panel/UserInfoAdminToolsContainerViewModel.tsx

Lines changed: 0 additions & 424 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import { type Room, type RoomMember, type IPowerLevelsContent } from "matrix-js-sdk/src/matrix";
8+
9+
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
10+
11+
/**
12+
* Interface used by admin tools container subcomponents props
13+
*/
14+
export interface RoomAdminToolsProps {
15+
room: Room;
16+
member: RoomMember;
17+
isUpdating: boolean;
18+
startUpdating: () => void;
19+
stopUpdating: () => void;
20+
}
21+
22+
/**
23+
* Interface used by admin tools container props
24+
*/
25+
export interface RoomAdminToolsContainerProps {
26+
room: Room;
27+
member: RoomMember;
28+
powerLevels: IPowerLevelsContent;
29+
}
30+
31+
32+
interface UserInfoAdminToolsContainerState {
33+
shouldShowKickButton: boolean;
34+
shouldShowBanButton: boolean;
35+
shouldShowMuteButton: boolean;
36+
shouldShowRedactButton: boolean;
37+
isCurrentUserInTheRoom: boolean;
38+
}
39+
40+
41+
/**
42+
* The view model for the user info admin tools container
43+
* @param {RoomAdminToolsContainerProps} props - the object containing the necceray props for the view model
44+
* @param {Room} props.room - the room that display the admin tools
45+
* @param {RoomMember} props.member - the selected member
46+
* @param {IPowerLevelsContent} props.powerLevels - current room power levels
47+
* @returns {UserInfoAdminToolsContainerState} the user info admin tools container state
48+
*/
49+
export const useUserInfoAdminToolsContainerViewModel = (
50+
props: RoomAdminToolsContainerProps,
51+
): UserInfoAdminToolsContainerState => {
52+
const cli = useMatrixClientContext();
53+
const { room, member, powerLevels } = props;
54+
55+
const editPowerLevel =
56+
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default;
57+
58+
// if these do not exist in the event then they should default to 50 as per the spec
59+
const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
60+
61+
const me = room.getMember(cli.getUserId() || "");
62+
const isCurrentUserInTheRoom = me !== null;
63+
64+
if (!isCurrentUserInTheRoom) {
65+
return {
66+
shouldShowKickButton: false,
67+
shouldShowBanButton: false,
68+
shouldShowMuteButton: false,
69+
shouldShowRedactButton: false,
70+
isCurrentUserInTheRoom: false,
71+
};
72+
}
73+
74+
const isMe = me.userId === member.userId;
75+
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
76+
77+
return {
78+
shouldShowKickButton: !isMe && canAffectUser && me.powerLevel >= kickPowerLevel,
79+
shouldShowRedactButton: me.powerLevel >= redactPowerLevel && !room.isSpaceRoom(),
80+
shouldShowBanButton: !isMe && canAffectUser && me.powerLevel >= banPowerLevel,
81+
shouldShowMuteButton: !isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom(),
82+
isCurrentUserInTheRoom,
83+
};
84+
};
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import { logger } from "@sentry/browser";
8+
import { type Room } from "matrix-js-sdk/src/matrix";
9+
import { KnownMembership } from "matrix-js-sdk/src/types";
10+
11+
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
12+
import { _t } from "../../../../../languageHandler";
13+
import Modal from "../../../../../Modal";
14+
import { bulkSpaceBehaviour } from "../../../../../utils/space";
15+
import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog";
16+
import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog";
17+
import ErrorDialog from "../../../../views/dialogs/ErrorDialog";
18+
import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel";
19+
20+
export interface BanButtonState {
21+
/**
22+
* The function to call when the button is clicked
23+
*/
24+
onBanOrUnbanClick: () => Promise<void>;
25+
/**
26+
* The label of the ban button can be ban or unban
27+
*/
28+
banLabel: string;
29+
}
30+
/**
31+
* The view model for the room ban button used in the UserInfoAdminToolsContainer
32+
* @param {RoomAdminToolsProps} props - the object containing the necceray props for banButton the view model
33+
* @param {Room} props.room - the room to ban/unban the user in
34+
* @param {RoomMember} props.member - the member to ban/unban
35+
* @param {boolean} props.isUpdating - whether the operation is currently in progress
36+
* @param {function} props.startUpdating - callback function to start the operation
37+
* @param {function} props.stopUpdating - callback function to stop the operation
38+
* @returns {BanButtonState} the room ban/unban button state
39+
*/
40+
export const useBanButtonViewModel = (props: RoomAdminToolsProps): BanButtonState => {
41+
const { isUpdating, startUpdating, stopUpdating, room, member } = props;
42+
43+
const cli = useMatrixClientContext();
44+
45+
const isBanned = member.membership === KnownMembership.Ban;
46+
47+
let banLabel = room.isSpaceRoom() ? _t("user_info|ban_button_space") : _t("user_info|ban_button_room");
48+
if (isBanned) {
49+
banLabel = room.isSpaceRoom() ? _t("user_info|unban_button_space") : _t("user_info|unban_button_room");
50+
}
51+
52+
const onBanOrUnbanClick = async (): Promise<void> => {
53+
if (isUpdating) return; // only allow one operation at a time
54+
startUpdating();
55+
56+
const commonProps = {
57+
member,
58+
action: room.isSpaceRoom()
59+
? isBanned
60+
? _t("user_info|unban_button_space")
61+
: _t("user_info|ban_button_space")
62+
: isBanned
63+
? _t("user_info|unban_button_room")
64+
: _t("user_info|ban_button_room"),
65+
title: isBanned
66+
? _t("user_info|unban_room_confirm_title", { roomName: room.name })
67+
: _t("user_info|ban_room_confirm_title", { roomName: room.name }),
68+
askReason: !isBanned,
69+
danger: !isBanned,
70+
};
71+
72+
let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
73+
74+
if (room.isSpaceRoom()) {
75+
({ finished } = Modal.createDialog(
76+
ConfirmSpaceUserActionDialog,
77+
{
78+
...commonProps,
79+
space: room,
80+
spaceChildFilter: isBanned
81+
? (child: Room) => {
82+
// Return true if the target member is banned and we have sufficient PL to unban
83+
const myMember = child.getMember(cli.credentials.userId || "");
84+
const theirMember = child.getMember(member.userId);
85+
return (
86+
!!myMember &&
87+
!!theirMember &&
88+
theirMember.membership === KnownMembership.Ban &&
89+
myMember.powerLevel > theirMember.powerLevel &&
90+
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
91+
);
92+
}
93+
: (child: Room) => {
94+
// Return true if the target member isn't banned and we have sufficient PL to ban
95+
const myMember = child.getMember(cli.credentials.userId || "");
96+
const theirMember = child.getMember(member.userId);
97+
return (
98+
!!myMember &&
99+
!!theirMember &&
100+
theirMember.membership !== KnownMembership.Ban &&
101+
myMember.powerLevel > theirMember.powerLevel &&
102+
child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel)
103+
);
104+
},
105+
allLabel: isBanned ? _t("user_info|unban_space_everything") : _t("user_info|ban_space_everything"),
106+
specificLabel: isBanned ? _t("user_info|unban_space_specific") : _t("user_info|ban_space_specific"),
107+
warningMessage: isBanned ? _t("user_info|unban_space_warning") : _t("user_info|kick_space_warning"),
108+
},
109+
"mx_ConfirmSpaceUserActionDialog_wrapper",
110+
));
111+
} else {
112+
({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
113+
}
114+
115+
const [proceed, reason, rooms = []] = await finished;
116+
if (!proceed) {
117+
stopUpdating();
118+
return;
119+
}
120+
121+
const fn = (roomId: string): Promise<unknown> => {
122+
if (isBanned) {
123+
return cli.unban(roomId, member.userId);
124+
} else {
125+
return cli.ban(roomId, member.userId, reason || undefined);
126+
}
127+
};
128+
129+
bulkSpaceBehaviour(room, rooms, (room) => fn(room.roomId))
130+
.then(
131+
() => {
132+
// NO-OP; rely on the m.room.member event coming down else we could
133+
// get out of sync if we force setState here!
134+
logger.info("Ban success");
135+
},
136+
function (err) {
137+
logger.error("Ban error: " + err);
138+
Modal.createDialog(ErrorDialog, {
139+
title: _t("common|error"),
140+
description: _t("user_info|error_ban_user"),
141+
});
142+
},
143+
)
144+
.finally(() => {
145+
stopUpdating();
146+
});
147+
};
148+
149+
return {
150+
onBanOrUnbanClick,
151+
banLabel,
152+
};
153+
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import { logger } from "@sentry/browser";
8+
import { type Room } from "matrix-js-sdk/src/matrix";
9+
import { KnownMembership } from "matrix-js-sdk/src/types";
10+
11+
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
12+
import { _t } from "../../../../../languageHandler";
13+
import Modal from "../../../../../Modal";
14+
import { bulkSpaceBehaviour } from "../../../../../utils/space";
15+
import ConfirmSpaceUserActionDialog from "../../../../views/dialogs/ConfirmSpaceUserActionDialog";
16+
import ConfirmUserActionDialog from "../../../../views/dialogs/ConfirmUserActionDialog";
17+
import ErrorDialog from "../../../../views/dialogs/ErrorDialog";
18+
import { type RoomAdminToolsProps } from "./UserInfoAdminToolsContainerViewModel";
19+
20+
interface RoomKickButtonState {
21+
/**
22+
* The function to call when the button is clicked
23+
*/
24+
onKickClick: () => Promise<void>;
25+
/**
26+
* Whether the user can be kicked based on membership value. If the user already join or was invited, it can be kicked
27+
*/
28+
canUserBeKicked: boolean;
29+
/**
30+
* The label of the kick button can be kick or disinvite
31+
*/
32+
kickLabel: string;
33+
}
34+
35+
/**
36+
* The view model for the room kick button used in the UserInfoAdminToolsContainer
37+
* @param {RoomAdminToolsProps} props - the object containing the necceray props for kickButton the view model
38+
* @param {Room} props.room - the room to kick/disinvite the user from
39+
* @param {RoomMember} props.member - the member to kick/disinvite
40+
* @param {boolean} props.isUpdating - whether the operation is currently in progress
41+
* @param {function} props.startUpdating - callback function to start the operation
42+
* @param {function} props.stopUpdating - callback function to stop the operation
43+
* @returns {KickButtonState} the room kick/disinvite button state
44+
*/
45+
export function useRoomKickButtonViewModel(props: RoomAdminToolsProps): RoomKickButtonState {
46+
const { isUpdating, startUpdating, stopUpdating, room, member } = props;
47+
48+
const cli = useMatrixClientContext();
49+
50+
const onKickClick = async (): Promise<void> => {
51+
if (isUpdating) return; // only allow one operation at a time
52+
startUpdating();
53+
54+
const commonProps = {
55+
member,
56+
action: room.isSpaceRoom()
57+
? member.membership === KnownMembership.Invite
58+
? _t("user_info|disinvite_button_space")
59+
: _t("user_info|kick_button_space")
60+
: member.membership === KnownMembership.Invite
61+
? _t("user_info|disinvite_button_room")
62+
: _t("user_info|kick_button_room"),
63+
title:
64+
member.membership === KnownMembership.Invite
65+
? _t("user_info|disinvite_button_room_name", { roomName: room.name })
66+
: _t("user_info|kick_button_room_name", { roomName: room.name }),
67+
askReason: member.membership === KnownMembership.Join,
68+
danger: true,
69+
};
70+
71+
let finished: Promise<[success?: boolean, reason?: string, rooms?: Room[]]>;
72+
73+
if (room.isSpaceRoom()) {
74+
({ finished } = Modal.createDialog(
75+
ConfirmSpaceUserActionDialog,
76+
{
77+
...commonProps,
78+
space: room,
79+
spaceChildFilter: (child: Room) => {
80+
// Return true if the target member is not banned and we have sufficient PL to ban them
81+
const myMember = child.getMember(cli.credentials.userId || "");
82+
const theirMember = child.getMember(member.userId);
83+
return (
84+
!!myMember &&
85+
!!theirMember &&
86+
theirMember.membership === member.membership &&
87+
myMember.powerLevel > theirMember.powerLevel &&
88+
child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel)
89+
);
90+
},
91+
allLabel: _t("user_info|kick_button_space_everything"),
92+
specificLabel: _t("user_info|kick_space_specific"),
93+
warningMessage: _t("user_info|kick_space_warning"),
94+
},
95+
"mx_ConfirmSpaceUserActionDialog_wrapper",
96+
));
97+
} else {
98+
({ finished } = Modal.createDialog(ConfirmUserActionDialog, commonProps));
99+
}
100+
101+
const [proceed, reason, rooms = []] = await finished;
102+
if (!proceed) {
103+
stopUpdating();
104+
return;
105+
}
106+
107+
bulkSpaceBehaviour(room, rooms, (room) => cli.kick(room.roomId, member.userId, reason || undefined))
108+
.then(
109+
() => {
110+
// NO-OP; rely on the m.room.member event coming down else we could
111+
// get out of sync if we force setState here!
112+
logger.info("Kick success");
113+
},
114+
function (err) {
115+
logger.error("Kick error: " + err);
116+
Modal.createDialog(ErrorDialog, {
117+
title: _t("user_info|error_kicking_user"),
118+
description: err?.message ?? "Operation failed",
119+
});
120+
},
121+
)
122+
.finally(() => {
123+
stopUpdating();
124+
});
125+
};
126+
127+
const canUserBeKicked = member.membership === KnownMembership.Invite || member.membership === KnownMembership.Join;
128+
129+
const kickLabel = room.isSpaceRoom()
130+
? member.membership === KnownMembership.Invite
131+
? _t("user_info|disinvite_button_space")
132+
: _t("user_info|kick_button_space")
133+
: member.membership === KnownMembership.Invite
134+
? _t("user_info|disinvite_button_room")
135+
: _t("user_info|kick_button_room");
136+
137+
return {
138+
onKickClick,
139+
canUserBeKicked,
140+
kickLabel,
141+
};
142+
}

0 commit comments

Comments
 (0)