diff --git a/cypress/e2e/knocking/knocking.spec.ts b/cypress/e2e/knocking/knocking.spec.ts
new file mode 100644
index 00000000000..a2f9e9ea3bb
--- /dev/null
+++ b/cypress/e2e/knocking/knocking.spec.ts
@@ -0,0 +1,79 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import Chainable = Cypress.Chainable;
+
+function openCreateRoomDialog(): Chainable> {
+ cy.get('[aria-label="Add room"]').click();
+ cy.get('.mx_ContextualMenu [aria-label="New room"]').click();
+ return cy.get(".mx_CreateRoomDialog");
+}
+
+describe("Knocking", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Tom");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should be able to create a room with knock JoinRule", () => {
+ // Enables labs flag feature
+ cy.enableLabsFeature("feature_knocking");
+ const name = "Test room 1";
+ const topic = "This is a test room";
+
+ // Create a room with knock JoinRule
+ openCreateRoomDialog().within(() => {
+ cy.get('[label="Name"]').type(name);
+ cy.get('[label="Topic (optional)"]').type(topic);
+ cy.get(".mx_JoinRuleDropdown").click();
+ cy.get(".mx_JoinRuleDropdown_knock").click();
+ cy.startMeasuring("from-submit-to-room");
+ cy.get(".mx_Dialog_primary").click();
+ });
+
+ // The room settings initially are set to Ask to join
+ cy.openRoomSettings("Security & Privacy");
+ cy.closeDialog();
+
+ //Check if the room settings are visible if labs flag is disabled
+ cy.openUserSettings("Labs").within(() => {
+ //disables labs flag feature
+ cy.get("[aria-label='Knocking']").click();
+ // cy.disableLabsFeature("feature_knocking");
+ });
+ cy.closeDialog();
+
+ //the default joinRule is set to Private (invite only) when the labs flag is disabled
+ cy.openRoomSettings("Security & Privacy");
+ cy.closeDialog();
+
+ // Click the expand link button to get more detailed view
+ cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
+
+ cy.stopMeasuring("from-submit-to-room");
+ cy.get(".mx_RoomHeader_nametext").contains(name);
+ cy.get(".mx_RoomHeader_topic").contains(topic);
+ });
+});
diff --git a/cypress/support/labs.ts b/cypress/support/labs.ts
index 3fff154e140..16e6f038cde 100644
--- a/cypress/support/labs.ts
+++ b/cypress/support/labs.ts
@@ -28,6 +28,7 @@ declare global {
* @param feature labsFeature to enable (e.g. "feature_spotlight")
*/
enableLabsFeature(feature: string): Chainable;
+ disableLabsFeature(feature: string): Chainable;
}
}
}
@@ -38,5 +39,12 @@ Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable =>
}).then(() => null);
});
+Cypress.Commands.add("disableLabsFeature", (feature: string): Chainable => {
+ return cy.window({ log: false }).then(win => {
+ win.localStorage.removeItem(`mx_labs_feature_${feature}`);
+ win.localStorage.setItem(`mx_labs_feature_${feature}`, "false");
+ }).then(() => null);
+});
+
// Needed to make this file a module
export { };
diff --git a/res/css/views/dialogs/_JoinRuleDropdown.pcss b/res/css/views/dialogs/_JoinRuleDropdown.pcss
index b4d13909a77..d3d8a1669f5 100644
--- a/res/css/views/dialogs/_JoinRuleDropdown.pcss
+++ b/res/css/views/dialogs/_JoinRuleDropdown.pcss
@@ -54,6 +54,11 @@ limitations under the License.
mask-size: contain;
}
+ .mx_JoinRuleDropdown_knock::before {
+ mask-image: url('$(res)/img/element-icons/knocking.svg');
+ mask-size: contain;
+ }
+
.mx_JoinRuleDropdown_public::before {
mask-image: url('$(res)/img/globe.svg');
mask-size: 12px;
diff --git a/res/img/element-icons/knocking.svg b/res/img/element-icons/knocking.svg
new file mode 100644
index 00000000000..718a5b6a910
--- /dev/null
+++ b/res/img/element-icons/knocking.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 9310391e3e2..c4fefc1e851 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -235,6 +235,10 @@ function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => Render
return () => _t('%(senderDisplayName)s made the room invite only.', {
senderDisplayName,
});
+ case JoinRule.Knock:
+ return () => _t('%(senderDisplayName)s made the room knock only.', {
+ senderDisplayName,
+ });
case JoinRule.Restricted:
if (allowJSX) {
return () =>
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index 2217af93879..53364d3f3be 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -20,6 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomType } from "matrix-js-sdk/src/@types/event";
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
+import SettingsStore from '../../../settings/SettingsStore';
import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler';
@@ -59,6 +60,7 @@ interface IState {
export default class CreateRoomDialog extends React.Component {
private readonly supportsRestricted: boolean;
+ private readonly knockingEnabled: boolean;
private nameField = createRef();
private aliasField = createRef();
@@ -66,6 +68,7 @@ export default class CreateRoomDialog extends React.Component {
super(props);
this.supportsRestricted = !!this.props.parentSpace;
+ this.knockingEnabled = SettingsStore.getValue("feature_knocking");
let joinRule = JoinRule.Invite;
if (this.props.defaultPublic) {
@@ -120,6 +123,9 @@ export default class CreateRoomDialog extends React.Component {
opts.joinRule = JoinRule.Restricted;
}
+ if (this.state.joinRule === JoinRule.Knock) {
+ opts.joinRule = JoinRule.Knock;
+ }
return opts;
}
@@ -265,9 +271,13 @@ export default class CreateRoomDialog extends React.Component {
;
} else if (this.state.joinRule === JoinRule.Invite) {
publicPrivateLabel =
- { _t(
- "Only people invited will be able to find and join this room.",
- ) }
+ { _t("Only people invited will be able to find and join this room.") }
+
+ { _t("You can change this at any time from room settings.") }
+
);
}
return {
const cli = room.client;
+ const roomSupportsKnocking = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.KnockingRooms);
+
const roomSupportsRestricted = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.RestrictedRooms);
const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade
? PreferredRoomVersions.RestrictedRooms
@@ -56,6 +59,8 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli);
+ const knockingEnabled = SettingsStore.getValue("feature_knocking");
+
const [content, setContent] = useLocalEcho(
() => room.currentState.getStateEvents(EventType.RoomJoinRules, "")?.getContent(),
content => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""),
@@ -89,6 +94,11 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length),
+ }, {
+ value: JoinRule.Knock,
+ label: _t("Ask to join"),
+ description: _t("Requires users to be granted access in order to join"),
+ checked: joinRule === JoinRule.Knock && knockingEnabled && roomSupportsKnocking,
}, {
value: JoinRule.Public,
label: _t("Public"),
@@ -98,6 +108,17 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
>,
}];
+ if (!knockingEnabled || !roomSupportsKnocking) {
+ // removes the knock option if the room isn't compatible for the same
+ for (let i = 0; i < definitions.length; i++) {
+ if (definitions[i].value === JoinRule.Knock) {
+ definitions.splice(i, 1);
+ break;
+ }
+ }
+ definitions[0].checked = true; //makes invite only room as default
+ }
+
if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) {
let upgradeRequiredPill;
if (preferredRestrictionVersion) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5135e51da88..06f9d4aa6d6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -504,6 +504,7 @@
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgraded this room.",
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s made the room public to whoever knows the link.",
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s made the room invite only.",
+ "%(senderDisplayName)s made the room knock only.": "%(senderDisplayName)s made the room knock only.",
"%(senderDisplayName)s changed who can join this room. View settings.": "%(senderDisplayName)s changed who can join this room. View settings.",
"%(senderDisplayName)s changed who can join this room.": "%(senderDisplayName)s changed who can join this room.",
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s changed the join rule to %(rule)s",
@@ -884,6 +885,8 @@
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Message Pinning": "Message Pinning",
+ "Knocking": "Knocking",
+ "Let users knock on a room to join it.": "Let users knock on a room to join it.",
"Threaded messaging": "Threaded messaging",
"Keep discussions organised with threads.": "Keep discussions organised with threads.",
"Threads help keep conversations on-topic and easy to track. Learn more.": "Threads help keep conversations on-topic and easy to track. Learn more.",
@@ -1326,6 +1329,8 @@
"Integration manager": "Integration manager",
"Private (invite only)": "Private (invite only)",
"Only invited people can join.": "Only invited people can join.",
+ "Ask to join": "Ask to join",
+ "Requires users to be granted access in order to join": "Requires users to be granted access in order to join",
"Anyone can find and join.": "Anyone can find and join.",
"Upgrade required": "Upgrade required",
"& %(count)s more|other": "& %(count)s more",
@@ -2555,6 +2560,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.",
+ "This room type requires users to be granted access in order to join.": "This room type requires users to be granted access in order to join.",
"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.",
@@ -2568,6 +2574,7 @@
"Topic (optional)": "Topic (optional)",
"Room visibility": "Room visibility",
"Private room (invite only)": "Private room (invite only)",
+ "Ask to join (Anyone can knock to join)": "Ask to join (Anyone can knock 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 c5f1511420c..0ac937005dc 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -256,6 +256,14 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_knocking": {
+ isFeature: true,
+ labsGroup: LabGroup.Rooms,
+ supportedLevels: LEVELS_FEATURE,
+ displayName: _td("Knocking"),
+ description: _td("Let users knock on a room to join it."),
+ default: false,
+ },
"feature_thread": {
isFeature: true,
labsGroup: LabGroup.Messaging,
diff --git a/src/utils/PreferredRoomVersions.ts b/src/utils/PreferredRoomVersions.ts
index 2dc269da6c2..377c6dda100 100644
--- a/src/utils/PreferredRoomVersions.ts
+++ b/src/utils/PreferredRoomVersions.ts
@@ -28,6 +28,11 @@ export class PreferredRoomVersions {
*/
public static readonly RestrictedRooms = "9";
+ /**
+ * The room version to use when creating "knocking" rooms.
+ */
+ public static readonly KnockingRooms = "9";
+
private constructor() {
// readonly, static, class
}