Mvvm split user info, create powerlevels component (#30005)

* feat: mvvm user info powerlevels

* chore: remove unecesssary comments and add new

* chore: fix lint and rebase

* fix: lint error
This commit is contained in:
Marc
2025-06-30 15:26:37 +02:00
committed by GitHub
parent 4a8b365bf8
commit 58875e5cf2
6 changed files with 552 additions and 185 deletions

View File

@@ -0,0 +1,108 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { useContext, useEffect, useState, useCallback } from "react";
import { logger } from "@sentry/browser";
import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import ErrorDialog from "../../views/dialogs/ErrorDialog";
import QuestionDialog from "../../views/dialogs/QuestionDialog";
import { warnSelfDemote } from "../../views/right_panel/UserInfo";
/**
*
*/
export interface UserInfoPowerLevelState {
/**
* default power level value of the selected user
*/
powerLevelUsersDefault: number;
/**
* The new power level to apply
*/
selectedPowerLevel: number;
/**
* Method to call When power level selection change
*/
onPowerChange: (powerLevel: number) => void;
}
export const useUserInfoPowerlevelViewModel = (user: RoomMember, room: Room): UserInfoPowerLevelState => {
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
useEffect(() => {
setSelectedPowerLevel(user.powerLevel);
}, [user]);
const cli = useContext(MatrixClientContext);
const onPowerChange = useCallback(
async (powerLevel: number) => {
setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise<unknown> => {
return cli.setPowerLevel(roomId, target, powerLevel).then(
function () {
logger.info("Power change success");
},
function (err) {
logger.error("Failed to change power level " + err);
Modal.createDialog(ErrorDialog, {
title: _t("common|error"),
description: _t("error|update_power_level"),
});
},
);
};
const roomId = user.roomId;
const target = user.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
const myUserId = cli.getUserId();
const myPower = powerLevelEvent.getContent().users[myUserId || ""];
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("common|warning"),
description: (
<div>
{_t("user_info|promote_warning")}
<br />
{_t("common|are_you_sure")}
</div>
),
button: _t("action|continue"),
});
const [confirmed] = await finished;
if (!confirmed) return;
} else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
logger.error("Failed to warn about self demotion: " + e);
}
}
await applyPowerChange(roomId, target, powerLevel);
},
[user.roomId, user.userId, cli, room],
);
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
return {
powerLevelUsersDefault,
onPowerChange,
selectedPowerLevel,
};
};

View File

@@ -43,7 +43,6 @@ import { type ButtonEvent } from "../elements/AccessibleButton";
import SdkConfig from "../../../SdkConfig";
import MultiInviter from "../../../utils/MultiInviter";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { textualPowerLevel } from "../../../Roles";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
@@ -54,7 +53,6 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import PowerSelector from "../elements/PowerSelector";
import MemberAvatar from "../avatars/MemberAvatar";
import PresenceLabel from "../rooms/PresenceLabel";
import { ShareDialog } from "../dialogs/ShareDialog";
@@ -76,6 +74,7 @@ import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
export interface IDevice extends Device {
ambiguous?: boolean;
@@ -437,7 +436,7 @@ const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
);
};
interface IRoomPermissions {
export interface IRoomPermissions {
modifyLevelMax: number;
canEdit: boolean;
canInvite: boolean;
@@ -492,112 +491,6 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR
return roomPermissions;
}
const PowerLevelSection: React.FC<{
user: RoomMember;
room: Room;
roomPermissions: IRoomPermissions;
powerLevels: IPowerLevelsContent;
}> = ({ user, room, roomPermissions, powerLevels }) => {
if (roomPermissions.canEdit) {
return <PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />;
} else {
const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = user.powerLevel;
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
return (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_roleDescription">{role}</div>
</div>
);
}
};
export const PowerLevelEditor: React.FC<{
user: RoomMember;
room: Room;
roomPermissions: IRoomPermissions;
}> = ({ user, room, roomPermissions }) => {
const cli = useContext(MatrixClientContext);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
useEffect(() => {
setSelectedPowerLevel(user.powerLevel);
}, [user]);
const onPowerChange = useCallback(
async (powerLevel: number) => {
setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId: string, target: string, powerLevel: number): Promise<unknown> => {
return cli.setPowerLevel(roomId, target, powerLevel).then(
function () {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
logger.log("Power change success");
},
function (err) {
logger.error("Failed to change power level " + err);
Modal.createDialog(ErrorDialog, {
title: _t("common|error"),
description: _t("error|update_power_level"),
});
},
);
};
const roomId = user.roomId;
const target = user.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
const myUserId = cli.getUserId();
const myPower = powerLevelEvent.getContent().users[myUserId || ""];
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("common|warning"),
description: (
<div>
{_t("user_info|promote_warning")}
<br />
{_t("common|are_you_sure")}
</div>
),
button: _t("action|continue"),
});
const [confirmed] = await finished;
if (!confirmed) return;
} else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
logger.error("Failed to warn about self demotion: ", e);
}
}
await applyPowerChange(roomId, target, powerLevel);
},
[user.roomId, user.userId, cli, room],
);
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
return (
<div className="mx_UserInfo_profileField">
<PowerSelector
label={undefined}
value={selectedPowerLevel}
maxValue={roomPermissions.modifyLevelMax}
usersDefault={powerLevelUsersDefault}
onChange={onPowerChange}
/>
</div>
);
};
async function getUserDeviceInfo(
userId: string,
cli: MatrixClient,
@@ -820,12 +713,7 @@ const BasicUserInfo: React.FC<{
// hide the Roles section for DMs as it doesn't make sense there
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
memberDetails = (
<PowerLevelSection
powerLevels={powerLevels}
user={member as RoomMember}
room={room}
roomPermissions={roomPermissions}
/>
<PowerLevelSection user={member as RoomMember} room={room} roomPermissions={roomPermissions} />
);
}

View File

@@ -0,0 +1,53 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type RoomMember, type Room } from "matrix-js-sdk/src/matrix";
import { textualPowerLevel } from "../../../../Roles";
import PowerSelector from "../../elements/PowerSelector";
import { type IRoomPermissions } from "../UserInfo";
import {
type UserInfoPowerLevelState,
useUserInfoPowerlevelViewModel,
} from "../../../viewmodels/right_panel/UserInfoPowerlevelViewModel";
export const PowerLevelSection: React.FC<{
user: RoomMember;
room: Room;
roomPermissions: IRoomPermissions;
}> = ({ user, room, roomPermissions }) => {
const vm = useUserInfoPowerlevelViewModel(user, room);
if (roomPermissions.canEdit) {
return <PowerLevelEditor vm={vm} roomPermissions={roomPermissions} />;
}
const powerLevel = user.powerLevel;
const role = textualPowerLevel(powerLevel, vm.powerLevelUsersDefault);
return (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_roleDescription">{role}</div>
</div>
);
};
export const PowerLevelEditor: React.FC<{
vm: UserInfoPowerLevelState;
roomPermissions: IRoomPermissions;
}> = ({ vm, roomPermissions }) => {
return (
<div className="mx_UserInfo_profileField">
<PowerSelector
label={undefined}
value={vm.selectedPowerLevel}
maxValue={roomPermissions.modifyLevelMax}
usersDefault={vm.powerLevelUsersDefault}
onChange={vm.onPowerChange}
/>
</div>
);
};