mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
* 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>
475 lines
18 KiB
TypeScript
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>
|
|
),
|
|
},
|
|
)}
|
|
|
|
{_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>
|
|
),
|
|
},
|
|
)}
|
|
|
|
{_t("create_room|join_rule_change_notice")}
|
|
</p>
|
|
);
|
|
} else if (this.state.joinRule === JoinRule.Public) {
|
|
publicPrivateLabel = (
|
|
<p>
|
|
{_t("create_room|join_rule_public_label")}
|
|
|
|
{_t("create_room|join_rule_change_notice")}
|
|
</p>
|
|
);
|
|
} else if (this.state.joinRule === JoinRule.Invite) {
|
|
publicPrivateLabel = (
|
|
<p>
|
|
{_t("create_room|join_rule_invite_label")}
|
|
|
|
{_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>
|
|
);
|
|
}
|
|
}
|