Skip to content

Mvvm split user info, create powerlevels component #30005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -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,
};
};
118 changes: 3 additions & 115 deletions src/components/views/right_panel/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -437,7 +436,7 @@ const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
);
};

interface IRoomPermissions {
export interface IRoomPermissions {
modifyLevelMax: number;
canEdit: boolean;
canInvite: boolean;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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} />
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Loading
Loading