+ {_t(
+ "Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
+ )}
+
+ );
}
let e2eeSection: JSX.Element | undefined;
@@ -332,7 +347,7 @@ export default class CreateRoomDialog extends React.Component {
let title: string;
if (isVideoRoom) {
title = _t("Create a video room");
- } else if (this.props.parentSpace) {
+ } else if (this.props.parentSpace || this.state.joinRule === JoinRule.Knock) {
title = _t("Create a room");
} else {
title = this.state.joinRule === JoinRule.Public ? _t("Create a public room") : _t("Create a private room");
@@ -365,6 +380,7 @@ export default class CreateRoomDialog extends React.Component {
= ({
label,
labelInvite,
+ labelKnock,
labelPublic,
labelRestricted,
value,
@@ -48,6 +51,17 @@ const JoinRuleDropdown: React.FC = ({
,
] as NonEmptyArray;
+ if (labelKnock) {
+ options.unshift(
+ (
+
+
+ {labelKnock}
+
+ ) as ReactElement & { key: string },
+ );
+ }
+
if (labelRestricted) {
options.unshift(
(
diff --git a/src/createRoom.ts b/src/createRoom.ts
index b0888d7d007d..555bb98efcf1 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -222,6 +222,10 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
});
}
+ if (opts.joinRule === JoinRule.Knock) {
+ createOpts.room_version = PreferredRoomVersions.KnockRooms;
+ }
+
if (opts.parentSpace) {
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
if (!opts.historyVisibility) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5c51a0f7fce8..39207112d04e 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1002,6 +1002,7 @@
"Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message",
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
"Enable intentional mentions": "Enable intentional mentions",
+ "Enable ask to join": "Enable ask to join",
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
@@ -2785,6 +2786,7 @@
"Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .",
"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.",
"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.",
@@ -2798,6 +2800,7 @@
"Topic (optional)": "Topic (optional)",
"Room visibility": "Room visibility",
"Private room (invite only)": "Private room (invite only)",
+ "Ask to join": "Ask to join",
"Visible to space members": "Visible to space members",
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
"Create video room": "Create video room",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 5f1cbe8165c4..8be0a5093b68 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -547,6 +547,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
["org.matrix.msc3952_intentional_mentions"],
]),
},
+ "feature_ask_to_join": {
+ default: false,
+ displayName: _td("Enable ask to join"),
+ isFeature: true,
+ labsGroup: LabGroup.Rooms,
+ supportedLevels: LEVELS_FEATURE,
+ },
"useCompactLayout": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("Use a more compact 'Modern' layout"),
diff --git a/src/utils/PreferredRoomVersions.ts b/src/utils/PreferredRoomVersions.ts
index b4944d4a0087..9d1e10417bed 100644
--- a/src/utils/PreferredRoomVersions.ts
+++ b/src/utils/PreferredRoomVersions.ts
@@ -23,6 +23,11 @@ limitations under the License.
* Loosely follows https://spec.matrix.org/latest/rooms/#feature-matrix
*/
export class PreferredRoomVersions {
+ /**
+ * The room version to use when creating "knock" rooms.
+ */
+ public static readonly KnockRooms = "7";
+
/**
* The room version to use when creating "restricted" rooms.
*/
diff --git a/test/PreferredRoomVersions-test.ts b/test/PreferredRoomVersions-test.ts
index caaf3fe5ae09..33dc30e8b876 100644
--- a/test/PreferredRoomVersions-test.ts
+++ b/test/PreferredRoomVersions-test.ts
@@ -36,6 +36,14 @@ describe("doesRoomVersionSupport", () => {
expect(doesRoomVersionSupport("3.1", "2.2")).toBe(true); // newer
});
+ it("should detect knock rooms in v7 and above", () => {
+ expect(doesRoomVersionSupport("6", PreferredRoomVersions.KnockRooms)).toBe(false);
+ expect(doesRoomVersionSupport("7", PreferredRoomVersions.KnockRooms)).toBe(true);
+ expect(doesRoomVersionSupport("8", PreferredRoomVersions.KnockRooms)).toBe(true);
+ expect(doesRoomVersionSupport("9", PreferredRoomVersions.KnockRooms)).toBe(true);
+ expect(doesRoomVersionSupport("10", PreferredRoomVersions.KnockRooms)).toBe(true);
+ });
+
it("should detect restricted rooms in v9 and v10", () => {
// Dev note: we consider it a feature that v8 rooms have to upgrade considering the bug in v8.
// https://spec.matrix.org/v1.3/rooms/v8/#redactions
diff --git a/test/components/views/dialogs/CreateRoomDialog-test.tsx b/test/components/views/dialogs/CreateRoomDialog-test.tsx
index f675efd023de..b319c2886552 100644
--- a/test/components/views/dialogs/CreateRoomDialog-test.tsx
+++ b/test/components/views/dialogs/CreateRoomDialog-test.tsx
@@ -16,10 +16,11 @@ limitations under the License.
import React from "react";
import { fireEvent, render, screen, within } from "@testing-library/react";
-import { MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
+import { JoinRule, MatrixError, Preset, Visibility } from "matrix-js-sdk/src/matrix";
import CreateRoomDialog from "../../../../src/components/views/dialogs/CreateRoomDialog";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
+import SettingsStore from "../../../../src/settings/SettingsStore";
describe("", () => {
const userId = "@alice:server.org";
@@ -208,6 +209,50 @@ describe("", () => {
});
});
+ 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();
+ });
+
+ it("should create a knock room", async () => {
+ jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join");
+ const onFinished = jest.fn();
+ 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" }));
+
+ fireEvent.click(screen.getByText("Create room"));
+ await flushPromises();
+
+ expect(screen.getByText("Create a room")).toBeInTheDocument();
+
+ expect(
+ screen.getByText(
+ "Anyone can request to join, but admins or moderators need to grant access. You can change this later.",
+ ),
+ ).toBeInTheDocument();
+
+ expect(onFinished).toHaveBeenCalledWith(true, {
+ createOpts: {
+ name: roomName,
+ },
+ encryption: true,
+ joinRule: JoinRule.Knock,
+ parentSpace: undefined,
+ roomType: undefined,
+ });
+ });
+ });
+
describe("for a public room", () => {
it("should set join rule to public defaultPublic is truthy", async () => {
const onFinished = jest.fn();