Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Allow creating public knock rooms #11481

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions res/css/views/dialogs/_CreateRoomDialog.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,8 @@ limitations under the License.
font-size: $font-12px;
}
}

.mx_CreateRoomDialog_labelledCheckbox {
color: $muted-fg-color;
margin-top: var(--cpd-space-6x);
}
33 changes: 31 additions & 2 deletions src/components/views/dialogs/CreateRoomDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import SettingsStore from "../../../settings/SettingsStore";
import LabelledCheckbox from "../elements/LabelledCheckbox";

interface IProps {
type?: RoomType;
Expand All @@ -45,15 +46,25 @@ interface IProps {
}

interface IState {
// The selected room join rule.
joinRule: JoinRule;
isPublic: boolean;
// Indicates whether the knock room is public visible. Applies only for rooms with knock join rule.
charlynguyen marked this conversation as resolved.
Show resolved Hide resolved
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;
charlynguyen marked this conversation as resolved.
Show resolved Hide resolved
}

Expand All @@ -78,7 +89,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {

const cli = MatrixClientPeg.safeGet();
this.state = {
isPublic: this.props.defaultPublic || false,
isPublicKnockRoom: this.props.defaultPublic || false,
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
joinRule,
name: this.props.defaultName || "",
Expand Down Expand Up @@ -129,6 +140,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {

if (this.state.joinRule === JoinRule.Knock) {
opts.joinRule = JoinRule.Knock;
createOpts.visibility = this.state.isPublicKnockRoom ? Visibility.Public : Visibility.Private;
}

return opts;
Expand Down Expand Up @@ -215,6 +227,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
return result;
};

private onIsPublicKnockRoomChange = (isPublicKnockRoom: boolean): void => {
this.setState({ isPublicKnockRoom });
};

private static validateRoomName = withValidation({
rules: [
{
Expand Down Expand Up @@ -298,6 +314,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;
if (this.state.joinRule !== JoinRule.Public) {
let microcopy: string;
Expand Down Expand Up @@ -383,6 +411,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
/>

{publicPrivateLabel}
{visibilitySection}
{e2eeSection}
{aliasField}
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
Expand Down
7 changes: 5 additions & 2 deletions src/components/views/elements/LabelledCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import React from "react";
import classnames from "classnames";

import StyledCheckbox from "./StyledCheckbox";

Expand All @@ -29,11 +30,13 @@ interface IProps {
disabled?: boolean;
// The function to call when the value changes
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 (
<label className="mx_LabelledCheckbox">
<label className={classnames("mx_LabelledCheckbox", className)}>
<StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} />
<div className="mx_LabelledCheckbox_labels">
<span className="mx_LabelledCheckbox_label">{label}</span>
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2688,6 +2688,7 @@
"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.",
"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. 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.",
Expand Down
88 changes: 58 additions & 30 deletions test/components/views/dialogs/CreateRoomDialog-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,45 +210,73 @@ describe("<CreateRoomDialog />", () => {
});

describe("for a knock room", () => {
it("should not have the option to create a knock room", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
getComponent();
fireEvent.click(screen.getByLabelText("Room visibility"));

expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
describe("when feature is disabled", () => {
it("should not have the option to create a knock room", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
getComponent();
fireEvent.click(screen.getByLabelText("Room visibility"));
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
});
});

it("should create a knock room", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
describe("when feature is enabled", () => {
const onFinished = jest.fn();
charlynguyen marked this conversation as resolved.
Show resolved Hide resolved
getComponent({ onFinished });
await flushPromises();

const roomName = "Test Room Name";
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });

fireEvent.click(screen.getByLabelText("Room visibility"));
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
beforeEach(async () => {
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"));
await flushPromises();
it("should have a heading", () => {
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(
screen.getByText(
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
),
).toBeInTheDocument();
it("should create a knock room with private visibility", async () => {
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
visibility: Visibility.Private,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
});

expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
it("should create a knock room with public visibility", async () => {
fireEvent.click(
screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
);
fireEvent.click(screen.getByText("Create room"));
await flushPromises();
expect(onFinished).toHaveBeenCalledWith(true, {
createOpts: {
name: roomName,
visibility: Visibility.Public,
},
encryption: true,
joinRule: JoinRule.Knock,
parentSpace: undefined,
roomType: undefined,
});
});
});
});
Expand Down
12 changes: 12 additions & 0 deletions test/components/views/elements/LabelledCheckbox-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@ describe("<LabelledCheckbox />", () => {
expect(checkbox).toBeChecked();
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);
});
});