Files
element-web/src/components/views/dialogs/CreateRoomDialog.tsx
Will Hunt a2f5c49438 Update settings toggles to use consistent design across app. (#30169)
* Use EditInPlace for identity server picker.

* Update test

* Add a test for setting an ID server.

* fix tests

* Reformat other :not sections

* forgot a comma

* Update Apperance settings to use toggle switches.

* Remove unused checkbox setting.

* Remove unused import.

* Update tests

* lint

* update apperance screenshot

* Begin replacing settings

* Refactor RoomPublishSetting

* Remove LabelledToggleSwitch

* Refactor SettingsFlag to use SettingsToggleInput

* Refactor CreateRoomDialog to use SettingsToggleInput

* Refactor DeclineAndBlockInviteDialog to use SettingsToggleInput

* Update DevtoolsDialog

* Refactor ReportRoomDialog to use SettingsToggle

* Update RoomUpgradeWarningDialog to use SettingsToggleInput

* Update WidgetCapabilitiesPromptDialog to use SettingsToggleInput

* Update trivial switchovers

* Update Notifications settings to use SettingsFlag where possible

* Update RoomPublishSetting and SpaceSettingVisibilityTab to use SettingsToggleInput with a warning

* revert changes to field

* Updated screenshots

* Prevent accidental submits

* Replace test ID tests

* Create new snapshot tests

* Add screenshot test for DeclineAndBlockDialog

* Add screenshot for create room dialog.

* Add devtools test

* Add upgrade rooms test

* Add widget capabilites prompt test

* Fix spec

* Add a test for the live location sharing prompt.

* fix copyright

* Add tests for notification settings

* Add tests for user security tab.

* Add test for room security tab.

* Add test for video settings tab.

* remove .only

* Test creating a video room

* Mask the IM name in the header.

* Add spaces vis tab test.

* Fixup unit tests to check correct attributes.

* Various fixes to components for tests.

* lint

* Update compound

* update setting names

* Cleanup tests

prettier

Updates some more playwright tests

Update more snapshots

Update switch

more fixes

drop .only

last screenshot round

fix video room flake

Remove console.logs

Remove roomId from devtools view.

lint

final screenshot

* Add playwright tests

* import pages/ remove duplicate create-room

* Update screenshots

* Fix accessibility for devtools

* Disable region test

* Fixup headers

* remove extra test

* Fix permissions dialog

* fixup tests

* update snapshot

* Update jest tests

* Clear up playwright tests

* update widget screenshot

* Fix wrong snaps from using wrong compound version

* Revert mistaken s/checkbox/switch/

* lint lint

* Update headings

* fix snap

* remove unused

* update snapshot

* update tab screenshot

* Update snapshots

* Fix margins

* Iterate

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

* Iterate

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

* Iterate

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

* Delint

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Update snapshot

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

* Update snapshot

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-04 15:46:21 +00:00

475 lines
18 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, {
type JSX,
type ChangeEvent,
createRef,
type KeyboardEvent,
type SyntheticEvent,
type ChangeEventHandler,
} from "react";
import { type Room, RoomType, JoinRule, Preset, Visibility } from "matrix-js-sdk/src/matrix";
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
import SdkConfig from "../../../SdkConfig";
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { checkUserIsAllowedToChangeEncryption, type IOpts } from "../../../createRoom";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
interface IProps {
type?: RoomType;
defaultPublic?: boolean;
defaultName?: string;
parentSpace?: Room;
defaultEncrypted?: boolean;
onFinished(proceed?: false): void;
onFinished(proceed: true, opts: IOpts): void;
}
interface IState {
/**
* The selected room join rule.
*/
joinRule: JoinRule;
/**
* Indicates whether the created room should have public visibility (ie, it should be
* shown in the public room list). Only applicable if `joinRule` == `JoinRule.Knock`.
*/
isPublicKnockRoom: boolean;
/**
* Indicates whether end-to-end encryption is enabled for the room.
*/
isEncrypted: boolean;
/**
* The room name.
*/
name: string;
/**
* The room topic.
*/
topic: string;
/**
* The room alias.
*/
alias: string;
/**
* Indicates whether the details section is open.
*/
detailsOpen: boolean;
/**
* Indicates whether federation is disabled for the room.
*/
noFederate: boolean;
/**
* Indicates whether the room name is valid.
*/
nameIsValid: boolean;
/**
* Indicates whether the user can change encryption settings for the room.
*/
canChangeEncryption: boolean;
}
export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly askToJoinEnabled: boolean;
private readonly advancedSettingsEnabled: boolean;
private readonly allowCreatingPublicRooms: boolean;
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>();
private aliasField = createRef<RoomAliasField>();
public constructor(props: IProps) {
super(props);
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
this.advancedSettingsEnabled = SettingsStore.getValue(UIFeature.AdvancedSettings);
this.allowCreatingPublicRooms = SettingsStore.getValue(UIFeature.AllowCreatingPublicRooms);
this.supportsRestricted = !!this.props.parentSpace;
const defaultPublic = this.allowCreatingPublicRooms && this.props.defaultPublic;
let joinRule = JoinRule.Invite;
if (defaultPublic) {
joinRule = JoinRule.Public;
} else if (this.supportsRestricted) {
joinRule = JoinRule.Restricted;
}
const cli = MatrixClientPeg.safeGet();
this.state = {
isPublicKnockRoom: defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
joinRule,
name: this.props.defaultName || "",
topic: "",
alias: "",
detailsOpen: false,
noFederate: SdkConfig.get().default_federate === false,
nameIsValid: false,
canChangeEncryption: false,
};
}
private roomCreateOptions(): IOpts {
const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = (opts.createOpts = {});
opts.roomType = this.props.type;
opts.name = this.state.name;
if (this.state.joinRule === JoinRule.Public) {
createOpts.visibility = Visibility.Public;
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
const { alias } = this.state;
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else {
opts.encryption = this.state.isEncrypted;
}
if (this.state.topic) {
opts.topic = this.state.topic;
}
if (this.state.noFederate) {
createOpts.creation_content = { "m.federate": false };
}
opts.parentSpace = this.props.parentSpace;
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
opts.joinRule = JoinRule.Restricted;
}
if (this.state.joinRule === JoinRule.Knock) {
opts.joinRule = JoinRule.Knock;
createOpts.visibility = this.state.isPublicKnockRoom ? Visibility.Public : Visibility.Private;
}
return opts;
}
public componentDidMount(): void {
const cli = MatrixClientPeg.safeGet();
checkUserIsAllowedToChangeEncryption(cli, Preset.PrivateChat).then(({ allowChange, forcedValue }) =>
this.setState((state) => ({
canChangeEncryption: allowChange,
// override with forcedValue if it is set
isEncrypted: forcedValue ?? state.isEncrypted,
})),
);
// move focus to first field when showing dialog
this.nameField.current?.focus();
}
private onKeyDown = (event: KeyboardEvent): void => {
const action = getKeyBindingsManager().getAccessibilityAction(event);
switch (action) {
case KeyBindingAction.Enter:
this.onOk();
event.preventDefault();
event.stopPropagation();
break;
}
};
private onOk = async (): Promise<void> => {
if (!this.nameField.current) return;
const activeElement = document.activeElement as HTMLElement;
activeElement?.blur();
await this.nameField.current.validate({ allowEmpty: false });
if (this.aliasField.current) {
await this.aliasField.current.validate({ allowEmpty: false });
}
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise<void>((resolve) => this.setState({}, resolve));
if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) {
this.props.onFinished(true, this.roomCreateOptions());
} else {
let field: RoomAliasField | Field | null = null;
if (!this.state.nameIsValid) {
field = this.nameField.current;
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
field = this.aliasField.current;
}
if (field) {
field.focus();
await field.validate({ allowEmpty: false, focused: true });
}
}
};
private onCancel = (): void => {
this.props.onFinished(false);
};
private onNameChange = (ev: ChangeEvent<HTMLInputElement>): void => {
this.setState({ name: ev.target.value });
};
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>): void => {
this.setState({ topic: ev.target.value });
};
private onJoinRuleChange = (joinRule: JoinRule): void => {
this.setState({ joinRule });
};
private onEncryptedChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isEncrypted: evt.target.checked });
};
private onAliasChange = (alias: string): void => {
this.setState({ alias });
};
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>): void => {
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
};
private onNoFederateChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ noFederate: evt.target.checked });
};
private onNameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await CreateRoomDialog.validateRoomName(fieldState);
this.setState({ nameIsValid: !!result.valid });
return result;
};
private onIsPublicKnockRoomChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isPublicKnockRoom: evt.target.checked });
};
private static validateRoomName = withValidation({
rules: [
{
key: "required",
test: async ({ value }) => !!value,
invalid: () => _t("create_room|name_validation_required"),
},
],
});
public render(): React.ReactNode {
const isVideoRoom = this.props.type === RoomType.ElementVideo || this.props.type === RoomType.UnstableCall;
let aliasField: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Public) {
const domain = MatrixClientPeg.safeGet().getDomain()!;
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField
ref={this.aliasField}
onChange={this.onAliasChange}
domain={domain}
value={this.state.alias}
/>
</div>
);
}
let publicPrivateLabel: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Restricted) {
publicPrivateLabel = (
<p>
{_t(
"create_room|join_rule_restricted_label",
{},
{
SpaceName: () => (
<strong>{this.props.parentSpace?.name ?? _t("common|unnamed_space")}</strong>
),
},
)}
&nbsp;
{_t("create_room|join_rule_change_notice")}
</p>
);
} else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) {
publicPrivateLabel = (
<p>
{_t(
"create_room|join_rule_public_parent_space_label",
{},
{
SpaceName: () => (
<strong>{this.props.parentSpace?.name ?? _t("common|unnamed_space")}</strong>
),
},
)}
&nbsp;
{_t("create_room|join_rule_change_notice")}
</p>
);
} else if (this.state.joinRule === JoinRule.Public) {
publicPrivateLabel = (
<p>
{_t("create_room|join_rule_public_label")}
&nbsp;
{_t("create_room|join_rule_change_notice")}
</p>
);
} else if (this.state.joinRule === JoinRule.Invite) {
publicPrivateLabel = (
<p>
{_t("create_room|join_rule_invite_label")}
&nbsp;
{_t("create_room|join_rule_change_notice")}
</p>
);
} else if (this.state.joinRule === JoinRule.Knock) {
publicPrivateLabel = <p>{_t("create_room|join_rule_knock_label")}</p>;
}
let visibilitySection: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Knock) {
visibilitySection = (
<SettingsToggleInput
name="publish-room"
className="mx_CreateRoomDialog_labelledCheckbox"
label={_t("room_settings|security|publish_room")}
onChange={this.onIsPublicKnockRoomChange}
checked={this.state.isPublicKnockRoom}
/>
);
}
let e2eeSection: JSX.Element | undefined;
if (this.state.joinRule !== JoinRule.Public) {
let microcopy: string;
if (privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
if (this.state.canChangeEncryption) {
microcopy = isVideoRoom
? _t("create_room|encrypted_video_room_warning")
: _t("create_room|encrypted_warning");
} else {
microcopy = _t("create_room|encryption_forced");
}
} else {
microcopy = _t("settings|security|e2ee_default_disabled_warning");
}
e2eeSection = (
<SettingsToggleInput
name="encryption-toggle"
label={_t("create_room|encryption_label")}
onChange={this.onEncryptedChange}
checked={this.state.isEncrypted}
disabled={!this.state.canChangeEncryption}
helpMessage={microcopy}
/>
);
}
let federateLabel = _t("create_room|unfederated_label_default_off");
if (SdkConfig.get().default_federate === false) {
// We only change the label if the default setting is different to avoid jarring text changes to the
// user. They will have read the implications of turning this off/on, so no need to rephrase for them.
federateLabel = _t("create_room|unfederated_label_default_on");
}
let title: string;
if (isVideoRoom) {
title = _t("create_room|title_video_room");
} else if (this.props.parentSpace || this.state.joinRule === JoinRule.Knock) {
title = _t("action|create_a_room");
} else {
title =
this.state.joinRule === JoinRule.Public
? _t("create_room|title_public_room")
: _t("create_room|title_private_room");
}
return (
<BaseDialog
className="mx_CreateRoomDialog"
onFinished={this.props.onFinished}
title={title}
screenName="CreateRoom"
>
<div className="mx_Dialog_content">
<Form.Root onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
<Field
ref={this.nameField}
label={_t("common|name")}
onChange={this.onNameChange}
onValidate={this.onNameValidate}
value={this.state.name}
className="mx_CreateRoomDialog_name"
/>
<Field
label={_t("create_room|topic_label")}
onChange={this.onTopicChange}
value={this.state.topic}
className="mx_CreateRoomDialog_topic"
/>
<div>
<JoinRuleDropdown
label={_t("create_room|room_visibility_label")}
labelInvite={_t("create_room|join_rule_invite")}
labelKnock={
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
}
labelPublic={this.allowCreatingPublicRooms ? _t("common|public_room") : undefined}
labelRestricted={
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
}
value={this.state.joinRule}
onChange={this.onJoinRuleChange}
/>
{publicPrivateLabel}
</div>
{visibilitySection}
{e2eeSection}
{aliasField}
{this.advancedSettingsEnabled && (
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
</summary>
<SettingsToggleInput
name="unfederated"
label={_t("create_room|unfederated", {
serverName: MatrixClientPeg.safeGet().getDomain(),
})}
onChange={this.onNoFederateChange}
checked={this.state.noFederate}
helpMessage={federateLabel}
/>
z<p>{federateLabel}</p>
</details>
)}
</Form.Root>
</div>
<DialogButtons
primaryButton={
isVideoRoom ? _t("create_room|action_create_video_room") : _t("create_room|action_create_room")
}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel}
/>
</BaseDialog>
);
}
}