Files
ThreadNet-Web/src/components/views/dialogs/CreateRoomDialog.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

512 lines
20 KiB
TypeScript
Raw Normal View History

2017-05-08 12:18:31 +02:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2021-05-19 19:18:28 +01:00
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
2017-05-08 12:18:31 +02:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2017-05-08 12:18:31 +02:00
*/
import React, {
type JSX,
type ChangeEvent,
createRef,
type KeyboardEvent,
type SyntheticEvent,
type ChangeEventHandler,
} from "react";
2025-02-05 13:25:06 +00:00
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";
2025-02-05 13:25:06 +00:00
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
2021-06-14 13:31:58 -06:00
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
2025-02-05 13:25:06 +00:00
import { checkUserIsAllowedToChangeEncryption, type IOpts } from "../../../createRoom";
2021-05-19 19:18:28 +01:00
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";
2023-07-10 10:01:03 +02:00
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
2021-05-19 19:18:28 +01:00
interface IProps {
type?: RoomType;
2021-05-19 19:18:28 +01:00
defaultPublic?: boolean;
defaultName?: string;
2021-05-19 19:18:28 +01:00
parentSpace?: Room;
2021-07-13 13:31:24 +02:00
defaultEncrypted?: boolean;
defaultStateEncrypted?: boolean;
onFinished(proceed?: false): void;
onFinished(proceed: true, opts: IOpts): void;
2021-05-19 19:18:28 +01:00
}
interface IState {
2023-09-04 18:09:44 +02:00
/**
* The selected room join rule.
*/
joinRule: JoinRule;
2023-09-04 18:09:44 +02:00
/**
* 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.
*/
2021-05-19 19:18:28 +01:00
isEncrypted: boolean;
/**
* Indicates whether end-to-end state encryption is enabled for this room.
* See MSC4362. Available if feature_msc4362_encrypted_state_events is enabled.
*/
isStateEncrypted: boolean;
2023-09-04 18:09:44 +02:00
/**
* The room name.
*/
2021-05-19 19:18:28 +01:00
name: string;
2023-09-04 18:09:44 +02:00
/**
* The room topic.
*/
2021-05-19 19:18:28 +01:00
topic: string;
2023-09-04 18:09:44 +02:00
/**
* The room alias.
*/
2021-05-19 19:18:28 +01:00
alias: string;
2023-09-04 18:09:44 +02:00
/**
* Indicates whether the details section is open.
*/
2021-05-19 19:18:28 +01:00
detailsOpen: boolean;
2023-09-04 18:09:44 +02:00
/**
* Indicates whether federation is disabled for the room.
*/
2021-05-19 19:18:28 +01:00
noFederate: boolean;
2023-09-04 18:09:44 +02:00
/**
* Indicates whether the room name is valid.
*/
2021-05-19 19:18:28 +01:00
nameIsValid: boolean;
2023-09-04 18:09:44 +02:00
/**
* Indicates whether the user can change encryption settings for the room.
*/
2021-05-19 19:18:28 +01:00
canChangeEncryption: boolean;
}
2017-05-08 12:18:31 +02:00
2021-05-19 19:18:28 +01:00
export default class CreateRoomDialog extends React.Component<IProps, IState> {
2023-07-10 10:01:03 +02:00
private readonly askToJoinEnabled: boolean;
private readonly advancedSettingsEnabled: boolean;
private readonly allowCreatingPublicRooms: boolean;
private readonly supportsRestricted: boolean;
2021-05-19 19:18:28 +01:00
private nameField = createRef<Field>();
private aliasField = createRef<RoomAliasField>();
2020-08-29 12:14:16 +01:00
2023-02-13 11:39:16 +00:00
public constructor(props: IProps) {
2020-08-29 12:14:16 +01:00
super(props);
2017-05-08 12:18:31 +02:00
2023-07-10 10:01:03 +02:00
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();
2020-08-29 12:14:16 +01:00
this.state = {
isPublicKnockRoom: defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
isStateEncrypted: this.props.defaultStateEncrypted ?? false,
joinRule,
name: this.props.defaultName || "",
2019-09-20 17:41:03 +02:00
topic: "",
2019-09-20 17:46:14 +02:00
alias: "",
2019-09-20 17:44:07 +02:00
detailsOpen: false,
noFederate: SdkConfig.get().default_federate === false,
2019-09-20 17:39:46 +02:00
nameIsValid: false,
canChangeEncryption: false,
2019-09-20 17:39:46 +02:00
};
2020-08-29 12:14:16 +01:00
}
2017-05-08 12:18:31 +02:00
private roomCreateOptions(): IOpts {
2021-05-19 19:18:28 +01:00
const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = (opts.createOpts = {});
opts.roomType = this.props.type;
opts.name = this.state.name;
2021-07-23 08:55:16 +01:00
if (this.state.joinRule === JoinRule.Public) {
2021-05-19 19:18:28 +01:00
createOpts.visibility = Visibility.Public;
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
2021-05-19 19:18:28 +01:00
const { alias } = this.state;
createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
2021-07-23 08:55:16 +01:00
} else {
const encryptedStateFeature = SettingsStore.getValue("feature_msc4362_encrypted_state_events", null, false);
opts.encryption = this.state.isEncrypted;
opts.stateEncryption = encryptedStateFeature && this.state.isStateEncrypted;
2019-09-20 17:46:14 +02:00
}
2021-07-23 08:55:16 +01:00
2019-09-20 17:41:03 +02:00
if (this.state.topic) {
opts.topic = this.state.topic;
2019-09-20 17:41:03 +02:00
}
2019-09-20 17:39:46 +02:00
if (this.state.noFederate) {
2021-05-19 19:18:28 +01:00
createOpts.creation_content = { "m.federate": false };
2019-09-20 17:39:46 +02:00
}
opts.parentSpace = this.props.parentSpace;
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
opts.joinRule = JoinRule.Restricted;
}
2023-07-10 10:01:03 +02:00
if (this.state.joinRule === JoinRule.Knock) {
opts.joinRule = JoinRule.Knock;
2023-09-04 18:09:44 +02:00
createOpts.visibility = this.state.isPublicKnockRoom ? Visibility.Public : Visibility.Private;
2023-07-10 10:01:03 +02:00
}
return opts;
2020-08-29 12:14:16 +01:00
}
2019-09-20 17:39:46 +02:00
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();
2020-08-29 12:14:16 +01:00
}
2019-09-20 17:39:46 +02:00
private onKeyDown = (event: KeyboardEvent): void => {
const action = getKeyBindingsManager().getAccessibilityAction(event);
switch (action) {
case KeyBindingAction.Enter:
this.onOk();
event.preventDefault();
event.stopPropagation();
break;
2019-10-02 16:26:52 +02:00
}
2020-08-29 12:14:16 +01:00
};
2019-10-02 16:26:52 +02:00
private onOk = async (): Promise<void> => {
if (!this.nameField.current) return;
2021-05-19 19:18:28 +01:00
const activeElement = document.activeElement as HTMLElement;
activeElement?.blur();
2021-06-29 13:11:58 +01:00
await this.nameField.current.validate({ allowEmpty: false });
2021-05-19 19:18:28 +01:00
if (this.aliasField.current) {
2021-06-29 13:11:58 +01:00
await this.aliasField.current.validate({ allowEmpty: false });
2019-09-20 17:46:47 +02:00
}
// 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.
2021-05-19 19:18:28 +01:00
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());
2019-09-20 17:46:47 +02:00
} else {
let field: RoomAliasField | Field | null = null;
2019-09-20 17:46:47 +02:00
if (!this.state.nameIsValid) {
2021-05-19 19:18:28 +01:00
field = this.nameField.current;
} else if (this.aliasField.current && !this.aliasField.current.isValid) {
field = this.aliasField.current;
2019-09-20 17:46:47 +02:00
}
if (field) {
field.focus();
await field.validate({ allowEmpty: false, focused: true });
2019-09-20 17:46:47 +02:00
}
}
2020-08-29 12:14:16 +01:00
};
2017-05-08 12:18:31 +02:00
private onCancel = (): void => {
2017-05-08 12:18:31 +02:00
this.props.onFinished(false);
2020-08-29 12:14:16 +01:00
};
2017-05-08 12:18:31 +02:00
private onNameChange = (ev: ChangeEvent<HTMLInputElement>): void => {
2021-05-19 19:18:28 +01:00
this.setState({ name: ev.target.value });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:39:46 +02:00
private onTopicChange = (ev: ChangeEvent<HTMLInputElement>): void => {
2021-05-19 19:18:28 +01:00
this.setState({ topic: ev.target.value });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:41:03 +02:00
private onJoinRuleChange = (joinRule: JoinRule): void => {
this.setState({ joinRule });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:43:14 +02:00
private onEncryptedChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isEncrypted: evt.target.checked });
2020-08-29 12:14:16 +01:00
};
private onStateEncryptedChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isStateEncrypted: evt.target.checked });
};
private onAliasChange = (alias: string): void => {
2021-05-19 19:18:28 +01:00
this.setState({ alias });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:39:46 +02:00
private onDetailsToggled = (ev: SyntheticEvent<HTMLDetailsElement>): void => {
2021-05-19 19:18:28 +01:00
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:39:46 +02:00
private onNoFederateChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ noFederate: evt.target.checked });
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:39:46 +02:00
private onNameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
2021-05-19 19:18:28 +01:00
const result = await CreateRoomDialog.validateRoomName(fieldState);
this.setState({ nameIsValid: !!result.valid });
2019-09-20 17:39:46 +02:00
return result;
2020-08-29 12:14:16 +01:00
};
2019-09-20 17:39:46 +02:00
private onIsPublicKnockRoomChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
this.setState({ isPublicKnockRoom: evt.target.checked });
2023-09-04 18:09:44 +02:00
};
2021-05-19 19:18:28 +01:00
private static validateRoomName = withValidation({
2019-09-20 17:39:46 +02:00
rules: [
{
key: "required",
test: async ({ value }) => !!value,
invalid: () => _t("create_room|name_validation_required"),
2019-09-20 17:39:46 +02:00
},
],
2020-08-29 12:14:16 +01:00
});
2019-09-20 17:39:46 +02:00
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()!;
2019-09-20 17:46:14 +02:00
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
2021-05-19 19:18:28 +01:00
<RoomAliasField
ref={this.aliasField}
onChange={this.onAliasChange}
domain={domain}
value={this.state.alias}
/>
2019-09-20 17:46:14 +02:00
</div>
);
2020-08-27 13:49:40 -06:00
}
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>
);
2023-07-10 10:01:03 +02:00
} else if (this.state.joinRule === JoinRule.Knock) {
publicPrivateLabel = <p>{_t("create_room|join_rule_knock_label")}</p>;
2019-09-20 17:43:14 +02:00
}
2023-09-04 18:09:44 +02:00
let visibilitySection: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Knock) {
visibilitySection = (
<SettingsToggleInput
name="publish-room"
2023-09-04 18:09:44 +02:00
className="mx_CreateRoomDialog_labelledCheckbox"
label={_t("room_settings|security|publish_room")}
2023-09-04 18:09:44 +02:00
onChange={this.onIsPublicKnockRoomChange}
checked={this.state.isPublicKnockRoom}
2023-09-04 18:09:44 +02:00
/>
);
}
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 e2eeStateSection: JSX.Element | undefined;
if (
SettingsStore.getValue("feature_msc4362_encrypted_state_events", null, false) &&
this.state.joinRule !== JoinRule.Public
) {
let microcopy: string;
if (!this.state.canChangeEncryption) {
microcopy = _t("create_room|encryption_forced");
} else {
microcopy = _t("create_room|state_encrypted_warning");
}
e2eeStateSection = (
<SettingsToggleInput
name="state-encryption-toggle"
label={_t("create_room|state_encryption_label")}
onChange={this.onStateEncryptedChange}
checked={this.state.isStateEncrypted}
disabled={!this.state.canChangeEncryption}
helpMessage={microcopy}
/>
);
}
let federateLabel = _t("create_room|unfederated_label_default_off");
2020-08-27 13:49:40 -06:00
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");
2020-08-27 13:49:40 -06:00
}
let title: string;
if (isVideoRoom) {
title = _t("create_room|title_video_room");
2023-07-10 10:01:03 +02:00
} 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");
}
2017-05-08 12:18:31 +02:00
return (
2022-02-09 14:25:58 +00:00
<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}>
2021-05-19 19:18:28 +01:00
<Field
ref={this.nameField}
label={_t("common|name")}
2021-05-19 19:18:28 +01:00
onChange={this.onNameChange}
onValidate={this.onNameValidate}
value={this.state.name}
className="mx_CreateRoomDialog_name"
/>
<Field
label={_t("create_room|topic_label")}
2021-05-19 19:18:28 +01:00
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>
2023-09-04 18:09:44 +02:00
{visibilitySection}
{e2eeSection}
{e2eeStateSection}
2019-09-20 17:46:14 +02:00
{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}
/>
</details>
)}
</Form.Root>
</div>
<DialogButtons
primaryButton={
isVideoRoom ? _t("create_room|action_create_video_room") : _t("create_room|action_create_room")
}
2017-12-23 13:42:44 +13:00
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel}
/>
2017-05-08 12:18:31 +02:00
</BaseDialog>
);
2020-08-29 12:14:16 +01:00
}
}