Allow creating public knock rooms (#11481)

* Allow creating public knock rooms

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

* Apply PR feedback

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

* Apply PR feedback

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

---------

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
pull/28788/head^2
Charly Nguyen 2023-09-04 18:09:44 +02:00 committed by GitHub
parent f8ff95349a
commit 2ff1056cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 34 deletions

View File

@ -113,3 +113,8 @@ limitations under the License.
font-size: $font-12px; font-size: $font-12px;
} }
} }
.mx_CreateRoomDialog_labelledCheckbox {
color: $muted-fg-color;
margin-top: var(--cpd-space-6x);
}

View File

@ -33,6 +33,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { privateShouldBeEncrypted } from "../../../utils/rooms";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import LabelledCheckbox from "../elements/LabelledCheckbox";
interface IProps { interface IProps {
type?: RoomType; type?: RoomType;
@ -45,15 +46,46 @@ interface IProps {
} }
interface IState { interface IState {
/**
* The selected room join rule.
*/
joinRule: JoinRule; joinRule: JoinRule;
isPublic: boolean; /**
* 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; isEncrypted: boolean;
/**
* The room name.
*/
name: string; name: string;
/**
* The room topic.
*/
topic: string; topic: string;
/**
* The room alias.
*/
alias: string; alias: string;
/**
* Indicates whether the details section is open.
*/
detailsOpen: boolean; detailsOpen: boolean;
/**
* Indicates whether federation is disabled for the room.
*/
noFederate: boolean; noFederate: boolean;
/**
* Indicates whether the room name is valid.
*/
nameIsValid: boolean; nameIsValid: boolean;
/**
* Indicates whether the user can change encryption settings for the room.
*/
canChangeEncryption: boolean; canChangeEncryption: boolean;
} }
@ -78,7 +110,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
this.state = { this.state = {
isPublic: this.props.defaultPublic || false, isPublicKnockRoom: this.props.defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli), isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
joinRule, joinRule,
name: this.props.defaultName || "", name: this.props.defaultName || "",
@ -129,6 +161,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
if (this.state.joinRule === JoinRule.Knock) { if (this.state.joinRule === JoinRule.Knock) {
opts.joinRule = JoinRule.Knock; opts.joinRule = JoinRule.Knock;
createOpts.visibility = this.state.isPublicKnockRoom ? Visibility.Public : Visibility.Private;
} }
return opts; return opts;
@ -215,6 +248,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
return result; return result;
}; };
private onIsPublicKnockRoomChange = (isPublicKnockRoom: boolean): void => {
this.setState({ isPublicKnockRoom });
};
private static validateRoomName = withValidation({ private static validateRoomName = withValidation({
rules: [ rules: [
{ {
@ -298,6 +335,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
); );
} }
let visibilitySection: JSX.Element | undefined;
if (this.state.joinRule === JoinRule.Knock) {
visibilitySection = (
<LabelledCheckbox
className="mx_CreateRoomDialog_labelledCheckbox"
label={_t("Make this room visible in the public room directory.")}
onChange={this.onIsPublicKnockRoomChange}
value={this.state.isPublicKnockRoom}
/>
);
}
let e2eeSection: JSX.Element | undefined; let e2eeSection: JSX.Element | undefined;
if (this.state.joinRule !== JoinRule.Public) { if (this.state.joinRule !== JoinRule.Public) {
let microcopy: string; let microcopy: string;
@ -383,6 +432,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
/> />
{publicPrivateLabel} {publicPrivateLabel}
{visibilitySection}
{e2eeSection} {e2eeSection}
{aliasField} {aliasField}
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details"> <details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import classnames from "classnames";
import StyledCheckbox from "./StyledCheckbox"; import StyledCheckbox from "./StyledCheckbox";
@ -29,11 +30,13 @@ interface IProps {
disabled?: boolean; disabled?: boolean;
// The function to call when the value changes // The function to call when the value changes
onChange(checked: boolean): void; onChange(checked: boolean): void;
// Optional additional CSS class to apply to the label
className?: string;
} }
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange }) => { const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange, className }) => {
return ( return (
<label className="mx_LabelledCheckbox"> <label className={classnames("mx_LabelledCheckbox", className)}>
<StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} /> <StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} />
<div className="mx_LabelledCheckbox_labels"> <div className="mx_LabelledCheckbox_labels">
<span className="mx_LabelledCheckbox_label">{label}</span> <span className="mx_LabelledCheckbox_label">{label}</span>

View File

@ -2691,6 +2691,7 @@
"Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.", "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.",
"Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.", "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.": "Anyone can request to join, but admins or moderators need to grant access. You can change this later.", "Anyone can request to join, but admins or moderators need to grant access. You can change this later.": "Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
"Make this room visible in the public room directory.": "Make this room visible in the public room directory.",
"You can't disable this later. The room will be encrypted but the embedded call will not.": "You can't disable this later. The room will be encrypted but the embedded call will not.", "You can't disable this later. The room will be encrypted but the embedded call will not.": "You can't disable this later. The room will be encrypted but the embedded call will not.",
"You can't disable this later. Bridges & most bots won't work yet.": "You can't disable this later. Bridges & most bots won't work yet.", "You can't disable this later. Bridges & most bots won't work yet.": "You can't disable this later. Bridges & most bots won't work yet.",
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.", "Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",

View File

@ -210,45 +210,73 @@ describe("<CreateRoomDialog />", () => {
}); });
describe("for a knock room", () => { describe("for a knock room", () => {
it("should not have the option to create a knock room", async () => { describe("when feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); it("should not have the option to create a knock room", async () => {
getComponent(); jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
fireEvent.click(screen.getByLabelText("Room visibility")); getComponent();
fireEvent.click(screen.getByLabelText("Room visibility"));
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument(); expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
});
}); });
it("should create a knock room", async () => { describe("when feature is enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
const onFinished = jest.fn(); const onFinished = jest.fn();
getComponent({ onFinished });
await flushPromises();
const roomName = "Test Room Name"; const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
fireEvent.click(screen.getByLabelText("Room visibility")); beforeEach(async () => {
fireEvent.click(screen.getByRole("option", { name: "Ask to join" })); onFinished.mockReset();
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_ask_to_join",
);
getComponent({ onFinished });
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
fireEvent.click(screen.getByLabelText("Room visibility"));
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
});
fireEvent.click(screen.getByText("Create room")); it("should have a heading", () => {
await flushPromises(); expect(screen.getByRole("heading")).toHaveTextContent("Create a room");
});
expect(screen.getByText("Create a room")).toBeInTheDocument(); it("should have a hint", () => {
expect(
screen.getByText(
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
),
).toBeInTheDocument();
});
expect( it("should create a knock room with private visibility", async () => {
screen.getByText( fireEvent.click(screen.getByText("Create room"));
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.", await flushPromises();
), expect(onFinished).toHaveBeenCalledWith(true, {
).toBeInTheDocument(); createOpts: {
name: roomName,
visibility: Visibility.Private,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
});
expect(onFinished).toHaveBeenCalledWith(true, { it("should create a knock room with public visibility", async () => {
createOpts: { fireEvent.click(
name: roomName, screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
}, );
encryption: true, fireEvent.click(screen.getByText("Create room"));
joinRule: JoinRule.Knock, await flushPromises();
parentSpace: undefined, expect(onFinished).toHaveBeenCalledWith(true, {
roomType: undefined, createOpts: {
name: roomName,
visibility: Visibility.Public,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
}); });
}); });
}); });

View File

@ -89,4 +89,16 @@ describe("<LabelledCheckbox />", () => {
expect(checkbox).toBeChecked(); expect(checkbox).toBeChecked();
expect(checkbox).toBeDisabled(); expect(checkbox).toBeDisabled();
}); });
it("should render with a custom class name", () => {
const className = "some class name";
const props: CompProps = {
label: "Hello world",
value: false,
onChange: jest.fn(),
className,
};
const { container } = render(getComponent(props));
expect(container.firstElementChild?.className).toContain(className);
});
}); });