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.") } +

; + } else if (this.state.joinRule === JoinRule.Knock && this.knockingEnabled) { + publicPrivateLabel =

+ { _t("This room type requires users to be granted access in order to join.") }   { _t("You can change this at any time from room settings.") }

; @@ -349,6 +359,7 @@ export default class CreateRoomDialog extends React.Component { { labelRestricted } ); + } else if (labelKnock) { + options.unshift(
+ { labelKnock } +
); } 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 }