element-portable/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
Michael Telatynski d8ebc68aa8
Remove abandoned Voice Broadcasts labs flag (#28548)
* Remove abandoned Voice Broadcasts labs flag

Any existing voice broadcasts will be shown as a series of voice messages which will sequence play as normal

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove dead code

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 10:53:27 +00:00

493 lines
20 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
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 { EventType, RoomMember, RoomState, RoomStateEvent, Room, IContent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { throttle, get } from "lodash";
import { KnownMembership, RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
import { _t, _td, TranslationKey } from "../../../../../languageHandler";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import PowerSelector from "../../../elements/PowerSelector";
import SettingsFieldset from "../../SettingsFieldset";
import SettingsStore from "../../../../../settings/SettingsStore";
import { ElementCall } from "../../../../../models/Call";
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import { PowerLevelSelector } from "../../PowerLevelSelector";
interface IEventShowOpts {
isState?: boolean;
hideForSpace?: boolean;
hideForRoom?: boolean;
}
interface IPowerLevelDescriptor {
desc: string;
defaultValue: number;
hideForSpace?: boolean;
}
const plEventsToShow: Record<string, IEventShowOpts> = {
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
[EventType.RoomAvatar]: { isState: true },
[EventType.RoomName]: { isState: true },
[EventType.RoomCanonicalAlias]: { isState: true },
[EventType.SpaceChild]: { isState: true, hideForRoom: true },
[EventType.RoomHistoryVisibility]: { isState: true, hideForSpace: true },
[EventType.RoomPowerLevels]: { isState: true },
[EventType.RoomTopic]: { isState: true },
[EventType.RoomTombstone]: { isState: true, hideForSpace: true },
[EventType.RoomEncryption]: { isState: true, hideForSpace: true },
[EventType.RoomServerAcl]: { isState: true, hideForSpace: true },
[EventType.RoomPinnedEvents]: { isState: true, hideForSpace: true },
[EventType.Reaction]: { isState: false, hideForSpace: true },
[EventType.RoomRedaction]: { isState: false, hideForSpace: true },
// MSC3401: Native Group VoIP signaling
[ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
[ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
};
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val: string, def: number): number {
const res = parseInt(val);
return isNaN(res) ? def : res;
}
interface IBannedUserProps {
canUnban?: boolean;
member: RoomMember;
by: string;
reason?: string;
}
export class BannedUser extends React.Component<IBannedUserProps> {
public static contextType = MatrixClientContext;
declare public context: React.ContextType<typeof MatrixClientContext>;
private onUnbanClick = (): void => {
this.context.unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
logger.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("common|error"),
description: _t("room_settings|permissions|error_unbanning"),
});
});
};
public render(): React.ReactNode {
let unbanButton;
if (this.props.canUnban) {
unbanButton = (
<AccessibleButton
className="mx_RolesRoomSettingsTab_unbanBtn"
kind="danger_sm"
onClick={this.onUnbanClick}
>
{_t("action|unban")}
</AccessibleButton>
);
}
const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
return (
<li>
{unbanButton}
<span title={_t("room_settings|permissions|banned_by", { displayName: this.props.by })}>
<strong>{this.props.member.name}</strong> {userId}
{this.props.reason
? " " + _t("room_settings|permissions|ban_reason") + ": " + this.props.reason
: ""}
</span>
</li>
);
}
}
interface IProps {
room: Room;
}
interface RolesRoomSettingsTabState {
isRoomEncrypted: boolean;
isReady: boolean;
}
export default class RolesRoomSettingsTab extends React.Component<IProps, RolesRoomSettingsTabState> {
public static contextType = MatrixClientContext;
declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps) {
super(props);
this.state = {
isReady: false,
isRoomEncrypted: false,
};
}
public async componentDidMount(): Promise<void> {
this.context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
this.setState({
isRoomEncrypted:
(await this.context.getCrypto()?.isEncryptionEnabledInRoom(this.props.room.roomId)) || false,
isReady: true,
});
}
public componentWillUnmount(): void {
const client = this.context;
if (client) {
client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
}
}
private onRoomStateUpdate = (state: RoomState): void => {
if (state.roomId !== this.props.room.roomId) return;
this.onThisRoomMembership();
};
private onThisRoomMembership = throttle(
() => {
this.forceUpdate();
},
200,
{ leading: true, trailing: true },
);
private populateDefaultPlEvents(
eventsSection: Record<string, number>,
stateLevel: number,
eventsLevel: number,
): void {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel;
}
}
}
private onPowerLevelsChanged = async (value: number, powerLevelKey: string): Promise<void> => {
const client = this.context;
const room = this.props.room;
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent<RoomPowerLevelsEventContent>() ?? {};
// Clone the power levels just in case
plContent = Object.assign({}, plContent);
const eventsLevelPrefix = "event_levels_";
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy
plContent["events"] = Object.assign({}, plContent["events"] || {});
plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else {
const keyPath = powerLevelKey.split(".");
let parentObj: IContent = {};
let currentObj: IContent = plContent;
for (const key of keyPath) {
if (!currentObj[key]) {
currentObj[key] = {};
}
parentObj = currentObj;
currentObj = currentObj[key];
}
parentObj[keyPath[keyPath.length - 1]] = value;
}
try {
await client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent);
} catch (e) {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("room_settings|permissions|error_changing_pl_reqs_title"),
description: _t("room_settings|permissions|error_changing_pl_reqs_description"),
});
// Rethrow so that the PowerSelector can roll back
throw e;
}
};
private onUserPowerLevelChanged = async (value: number, powerLevelKey: string): Promise<void> => {
const client = this.context;
const room = this.props.room;
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent<RoomPowerLevelsEventContent>() ?? {};
// Clone the power levels just in case
plContent = Object.assign({}, plContent);
// powerLevelKey should be a user ID
if (!plContent["users"]) plContent["users"] = {};
plContent["users"][powerLevelKey] = value;
try {
await client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent);
} catch (e) {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("room_settings|permissions|error_changing_pl_title"),
description: _t("room_settings|permissions|error_changing_pl_description"),
});
}
};
public render(): React.ReactNode {
const client = this.context;
const room = this.props.room;
const isSpaceRoom = room.isSpaceRoom();
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const plContent = plEvent ? plEvent.getContent() || {} : {};
const canChangeLevels = room.currentState.mayClientSendStateEvent(EventType.RoomPowerLevels, client);
const plEventsToLabels: Record<EventType | string, TranslationKey | null> = {
// These will be translated for us later.
[EventType.RoomAvatar]: isSpaceRoom
? _td("room_settings|permissions|m.room.avatar_space")
: _td("room_settings|permissions|m.room.avatar"),
[EventType.RoomName]: isSpaceRoom
? _td("room_settings|permissions|m.room.name_space")
: _td("room_settings|permissions|m.room.name"),
[EventType.RoomCanonicalAlias]: isSpaceRoom
? _td("room_settings|permissions|m.room.canonical_alias_space")
: _td("room_settings|permissions|m.room.canonical_alias"),
[EventType.SpaceChild]: _td("room_settings|permissions|m.space.child"),
[EventType.RoomHistoryVisibility]: _td("room_settings|permissions|m.room.history_visibility"),
[EventType.RoomPowerLevels]: _td("room_settings|permissions|m.room.power_levels"),
[EventType.RoomTopic]: isSpaceRoom
? _td("room_settings|permissions|m.room.topic_space")
: _td("room_settings|permissions|m.room.topic"),
[EventType.RoomTombstone]: _td("room_settings|permissions|m.room.tombstone"),
[EventType.RoomEncryption]: _td("room_settings|permissions|m.room.encryption"),
[EventType.RoomServerAcl]: _td("room_settings|permissions|m.room.server_acl"),
[EventType.Reaction]: _td("room_settings|permissions|m.reaction"),
[EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"),
[EventType.RoomPinnedEvents]: _td("room_settings|permissions|m.room.pinned_events"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"),
};
// MSC3401: Native Group VoIP signaling
if (SettingsStore.getValue("feature_group_calls")) {
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("room_settings|permissions|m.call");
plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("room_settings|permissions|m.call.member");
}
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
"users_default": {
desc: _t("room_settings|permissions|users_default"),
defaultValue: 0,
},
"events_default": {
desc: _t("room_settings|permissions|events_default"),
defaultValue: 0,
hideForSpace: true,
},
"invite": {
desc: _t("room_settings|permissions|invite"),
defaultValue: 0,
},
"state_default": {
desc: _t("room_settings|permissions|state_default"),
defaultValue: 50,
},
"kick": {
desc: _t("room_settings|permissions|kick"),
defaultValue: 50,
},
"ban": {
desc: _t("room_settings|permissions|ban"),
defaultValue: 50,
},
"redact": {
desc: _t("room_settings|permissions|redact"),
defaultValue: 50,
hideForSpace: true,
},
"notifications.room": {
desc: _t("room_settings|permissions|notifications.room"),
defaultValue: 50,
hideForSpace: true,
},
};
const eventsLevels = plContent.events || {};
const userLevels = plContent.users || {};
const banLevel = parseIntWithDefault(plContent.ban, powerLevelDescriptors.ban.defaultValue);
const defaultUserLevel = parseIntWithDefault(
plContent.users_default,
powerLevelDescriptors.users_default.defaultValue,
);
let currentUserLevel = userLevels[client.getUserId()!];
if (currentUserLevel === undefined) {
currentUserLevel = defaultUserLevel;
}
this.populateDefaultPlEvents(
eventsLevels,
parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue),
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let privilegedUsersSection = <div>{_t("room_settings|permissions|no_privileged_users")}</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
privilegedUsersSection = (
<PowerLevelSelector
title={_t("room_settings|permissions|privileged_users_section")}
userLevels={userLevels}
canChangeLevels={canChangeLevels}
currentUserLevel={currentUserLevel}
onClick={this.onUserPowerLevelChanged}
filter={(user) => userLevels[user] > defaultUserLevel}
>
<div>{_t("room_settings|permissions|no_privileged_users")}</div>
</PowerLevelSelector>
);
mutedUsersSection = (
<PowerLevelSelector
title={_t("room_settings|permissions|muted_users_section")}
userLevels={userLevels}
canChangeLevels={canChangeLevels}
currentUserLevel={currentUserLevel}
onClick={this.onUserPowerLevelChanged}
filter={(user) => userLevels[user] < defaultUserLevel}
/>
);
}
const banned = room.getMembersWithMembership(KnownMembership.Ban);
let bannedUsersSection: JSX.Element | undefined;
if (banned?.length) {
const canBanUsers = currentUserLevel >= banLevel;
bannedUsersSection = (
<SettingsFieldset legend={_t("room_settings|permissions|banned_users_section")}>
<ul className="mx_RolesRoomSettingsTab_bannedList">
{banned.map((member) => {
const banEvent = member.events.member?.getContent();
const bannedById = member.events.member?.getSender();
const sender = bannedById ? room.getMember(bannedById) : undefined;
const bannedBy = sender?.name || bannedById; // fallback to mxid
return (
<BannedUser
key={member.userId}
canUnban={canBanUsers}
member={member}
reason={banEvent?.reason}
by={bannedBy!}
/>
);
})}
</ul>
</SettingsFieldset>
);
}
const powerSelectors = Object.keys(powerLevelDescriptors)
.map((key, index) => {
const descriptor = powerLevelDescriptors[key];
if (isSpaceRoom && descriptor.hideForSpace) {
return null;
}
const value = parseIntWithDefault(get(plContent, key), descriptor.defaultValue);
return (
<div key={index} className="">
<PowerSelector
label={descriptor.desc}
value={value}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this.onPowerLevelsChanged}
/>
</div>
);
})
.filter(Boolean);
// hide the power level selector for enabling E2EE if it the room is already encrypted
if (this.state.isRoomEncrypted) {
delete eventsLevels[EventType.RoomEncryption];
}
const eventPowerSelectors = Object.keys(eventsLevels)
.map((eventType, i) => {
if (isSpaceRoom && plEventsToShow[eventType]?.hideForSpace) {
return null;
} else if (!isSpaceRoom && plEventsToShow[eventType]?.hideForRoom) {
return null;
}
const translationKeyForEvent = plEventsToLabels[eventType];
let label: string;
if (translationKeyForEvent) {
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
label = _t(translationKeyForEvent, { brand });
} else {
label = _t("room_settings|permissions|send_event_type", { eventType });
}
return (
<div key={eventType}>
<PowerSelector
label={label}
value={eventsLevels[eventType]}
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType}
onChange={this.onPowerLevelsChanged}
/>
</div>
);
})
.filter(Boolean);
return (
<SettingsTab>
<SettingsSection heading={_t("room_settings|permissions|title")}>
{privilegedUsersSection}
{canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
{mutedUsersSection}
{bannedUsersSection}
{this.state.isReady && (
<SettingsFieldset
legend={_t("room_settings|permissions|permissions_section")}
description={
isSpaceRoom
? _t("room_settings|permissions|permissions_section_description_space")
: _t("room_settings|permissions|permissions_section_description_room")
}
>
{powerSelectors}
{eventPowerSelectors}
</SettingsFieldset>
)}
</SettingsSection>
</SettingsTab>
);
}
}