Skip to content

Commit 4ded69e

Browse files
charlynguyennurjinjafar
authored andcommitted
Allow creating public knock rooms (matrix-org#11481)
* Allow creating public knock rooms Signed-off-by: Charly Nguyen <[email protected]> * Apply PR feedback Signed-off-by: Charly Nguyen <[email protected]> * Apply PR feedback Signed-off-by: Charly Nguyen <[email protected]> --------- Signed-off-by: Charly Nguyen <[email protected]>
1 parent 555faa2 commit 4ded69e

File tree

6 files changed

+133
-34
lines changed

6 files changed

+133
-34
lines changed

res/css/views/dialogs/_CreateRoomDialog.pcss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,8 @@ limitations under the License.
113113
font-size: $font-12px;
114114
}
115115
}
116+
117+
.mx_CreateRoomDialog_labelledCheckbox {
118+
color: $muted-fg-color;
119+
margin-top: var(--cpd-space-6x);
120+
}

src/components/views/dialogs/CreateRoomDialog.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
3333
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
3434
import { privateShouldBeEncrypted } from "../../../utils/rooms";
3535
import SettingsStore from "../../../settings/SettingsStore";
36+
import LabelledCheckbox from "../elements/LabelledCheckbox";
3637

3738
interface IProps {
3839
type?: RoomType;
@@ -45,15 +46,46 @@ interface IProps {
4546
}
4647

4748
interface IState {
49+
/**
50+
* The selected room join rule.
51+
*/
4852
joinRule: JoinRule;
49-
isPublic: boolean;
53+
/**
54+
* Indicates whether the created room should have public visibility (ie, it should be
55+
* shown in the public room list). Only applicable if `joinRule` == `JoinRule.Knock`.
56+
*/
57+
isPublicKnockRoom: boolean;
58+
/**
59+
* Indicates whether end-to-end encryption is enabled for the room.
60+
*/
5061
isEncrypted: boolean;
62+
/**
63+
* The room name.
64+
*/
5165
name: string;
66+
/**
67+
* The room topic.
68+
*/
5269
topic: string;
70+
/**
71+
* The room alias.
72+
*/
5373
alias: string;
74+
/**
75+
* Indicates whether the details section is open.
76+
*/
5477
detailsOpen: boolean;
78+
/**
79+
* Indicates whether federation is disabled for the room.
80+
*/
5581
noFederate: boolean;
82+
/**
83+
* Indicates whether the room name is valid.
84+
*/
5685
nameIsValid: boolean;
86+
/**
87+
* Indicates whether the user can change encryption settings for the room.
88+
*/
5789
canChangeEncryption: boolean;
5890
}
5991

@@ -78,7 +110,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
78110

79111
const cli = MatrixClientPeg.safeGet();
80112
this.state = {
81-
isPublic: this.props.defaultPublic || false,
113+
isPublicKnockRoom: this.props.defaultPublic || false,
82114
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
83115
joinRule,
84116
name: this.props.defaultName || "",
@@ -129,6 +161,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
129161

130162
if (this.state.joinRule === JoinRule.Knock) {
131163
opts.joinRule = JoinRule.Knock;
164+
createOpts.visibility = this.state.isPublicKnockRoom ? Visibility.Public : Visibility.Private;
132165
}
133166

134167
return opts;
@@ -215,6 +248,10 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
215248
return result;
216249
};
217250

251+
private onIsPublicKnockRoomChange = (isPublicKnockRoom: boolean): void => {
252+
this.setState({ isPublicKnockRoom });
253+
};
254+
218255
private static validateRoomName = withValidation({
219256
rules: [
220257
{
@@ -298,6 +335,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
298335
);
299336
}
300337

338+
let visibilitySection: JSX.Element | undefined;
339+
if (this.state.joinRule === JoinRule.Knock) {
340+
visibilitySection = (
341+
<LabelledCheckbox
342+
className="mx_CreateRoomDialog_labelledCheckbox"
343+
label={_t("Make this room visible in the public room directory.")}
344+
onChange={this.onIsPublicKnockRoomChange}
345+
value={this.state.isPublicKnockRoom}
346+
/>
347+
);
348+
}
349+
301350
let e2eeSection: JSX.Element | undefined;
302351
if (this.state.joinRule !== JoinRule.Public) {
303352
let microcopy: string;
@@ -383,6 +432,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
383432
/>
384433

385434
{publicPrivateLabel}
435+
{visibilitySection}
386436
{e2eeSection}
387437
{aliasField}
388438
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">

src/components/views/elements/LabelledCheckbox.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import React from "react";
18+
import classnames from "classnames";
1819

1920
import StyledCheckbox from "./StyledCheckbox";
2021

@@ -29,11 +30,13 @@ interface IProps {
2930
disabled?: boolean;
3031
// The function to call when the value changes
3132
onChange(checked: boolean): void;
33+
// Optional additional CSS class to apply to the label
34+
className?: string;
3235
}
3336

34-
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange }) => {
37+
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange, className }) => {
3538
return (
36-
<label className="mx_LabelledCheckbox">
39+
<label className={classnames("mx_LabelledCheckbox", className)}>
3740
<StyledCheckbox disabled={disabled} checked={value} onChange={(e) => onChange(e.target.checked)} />
3841
<div className="mx_LabelledCheckbox_labels">
3942
<span className="mx_LabelledCheckbox_label">{label}</span>

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2693,6 +2693,7 @@
26932693
"Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.",
26942694
"Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
26952695
"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.",
2696+
"Make this room visible in the public room directory.": "Make this room visible in the public room directory.",
26962697
"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.",
26972698
"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.",
26982699
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",

test/components/views/dialogs/CreateRoomDialog-test.tsx

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -210,45 +210,73 @@ describe("<CreateRoomDialog />", () => {
210210
});
211211

212212
describe("for a knock room", () => {
213-
it("should not have the option to create a knock room", async () => {
214-
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
215-
getComponent();
216-
fireEvent.click(screen.getByLabelText("Room visibility"));
217-
218-
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
213+
describe("when feature is disabled", () => {
214+
it("should not have the option to create a knock room", async () => {
215+
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
216+
getComponent();
217+
fireEvent.click(screen.getByLabelText("Room visibility"));
218+
expect(screen.queryByRole("option", { name: "Ask to join" })).not.toBeInTheDocument();
219+
});
219220
});
220221

221-
it("should create a knock room", async () => {
222-
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
222+
describe("when feature is enabled", () => {
223223
const onFinished = jest.fn();
224-
getComponent({ onFinished });
225-
await flushPromises();
226-
227224
const roomName = "Test Room Name";
228-
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
229225

230-
fireEvent.click(screen.getByLabelText("Room visibility"));
231-
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
226+
beforeEach(async () => {
227+
onFinished.mockReset();
228+
jest.spyOn(SettingsStore, "getValue").mockImplementation(
229+
(setting) => setting === "feature_ask_to_join",
230+
);
231+
getComponent({ onFinished });
232+
fireEvent.change(screen.getByLabelText("Name"), { target: { value: roomName } });
233+
fireEvent.click(screen.getByLabelText("Room visibility"));
234+
fireEvent.click(screen.getByRole("option", { name: "Ask to join" }));
235+
});
232236

233-
fireEvent.click(screen.getByText("Create room"));
234-
await flushPromises();
237+
it("should have a heading", () => {
238+
expect(screen.getByRole("heading")).toHaveTextContent("Create a room");
239+
});
235240

236-
expect(screen.getByText("Create a room")).toBeInTheDocument();
241+
it("should have a hint", () => {
242+
expect(
243+
screen.getByText(
244+
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
245+
),
246+
).toBeInTheDocument();
247+
});
237248

238-
expect(
239-
screen.getByText(
240-
"Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
241-
),
242-
).toBeInTheDocument();
249+
it("should create a knock room with private visibility", async () => {
250+
fireEvent.click(screen.getByText("Create room"));
251+
await flushPromises();
252+
expect(onFinished).toHaveBeenCalledWith(true, {
253+
createOpts: {
254+
name: roomName,
255+
visibility: Visibility.Private,
256+
},
257+
encryption: true,
258+
joinRule: JoinRule.Knock,
259+
parentSpace: undefined,
260+
roomType: undefined,
261+
});
262+
});
243263

244-
expect(onFinished).toHaveBeenCalledWith(true, {
245-
createOpts: {
246-
name: roomName,
247-
},
248-
encryption: true,
249-
joinRule: JoinRule.Knock,
250-
parentSpace: undefined,
251-
roomType: undefined,
264+
it("should create a knock room with public visibility", async () => {
265+
fireEvent.click(
266+
screen.getByRole("checkbox", { name: "Make this room visible in the public room directory." }),
267+
);
268+
fireEvent.click(screen.getByText("Create room"));
269+
await flushPromises();
270+
expect(onFinished).toHaveBeenCalledWith(true, {
271+
createOpts: {
272+
name: roomName,
273+
visibility: Visibility.Public,
274+
},
275+
encryption: true,
276+
joinRule: JoinRule.Knock,
277+
parentSpace: undefined,
278+
roomType: undefined,
279+
});
252280
});
253281
});
254282
});

test/components/views/elements/LabelledCheckbox-test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,16 @@ describe("<LabelledCheckbox />", () => {
8989
expect(checkbox).toBeChecked();
9090
expect(checkbox).toBeDisabled();
9191
});
92+
93+
it("should render with a custom class name", () => {
94+
const className = "some class name";
95+
const props: CompProps = {
96+
label: "Hello world",
97+
value: false,
98+
onChange: jest.fn(),
99+
className,
100+
};
101+
const { container } = render(getComponent(props));
102+
expect(container.firstElementChild?.className).toContain(className);
103+
});
92104
});

0 commit comments

Comments
 (0)