mirror of
https://github.com/vector-im/element-web.git
synced 2026-01-07 01:21:48 +01:00
Add UIFeature to hide public space and room creation (#30708)
* Add settings to hide public room & space creation. * Add space changes. * Add room changes. * lint * Add playwright tests * don't specialcase 1 join rule * Ensure mocks get cleared * Fixup test * Add SpaceCreateMenu component unit-tests * Fixup create room test asserts * fix import (cherry picked from commit 6d05bfc4c520575a0f47957fc9128f8cf5729f9d)
This commit is contained in:
parent
4f702b70aa
commit
eb86c0e1fa
@ -585,6 +585,8 @@ Currently, the following UI feature flags are supported:
|
||||
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
|
||||
to true.
|
||||
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
|
||||
- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
|
||||
- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
|
||||
|
||||
## Undocumented / developer options
|
||||
|
||||
|
||||
47
playwright/e2e/room/create-room.spec.ts
Normal file
47
playwright/e2e/room/create-room.spec.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { UIFeature } from "../../../src/settings/UIFeature";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
const name = "Test room";
|
||||
const topic = "A decently explanatory topic for a test room.";
|
||||
|
||||
test.describe("Create Room", () => {
|
||||
test.use({ displayName: "Jim" });
|
||||
|
||||
test.describe("Should hide public room option if not allowed", () => {
|
||||
test.use({
|
||||
config: {
|
||||
setting_defaults: {
|
||||
[UIFeature.AllowCreatingPublicRooms]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
|
||||
const dialog = await app.openCreateRoomDialog();
|
||||
// Fill name & topic
|
||||
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
||||
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
||||
|
||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
// Snapshot it
|
||||
await expect(dialog).toMatchScreenshot("create-room-no-public.png");
|
||||
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/!.+`));
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
await expect(header).toContainText(name);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -11,6 +11,7 @@ import { test, expect } from "../../element-web-test";
|
||||
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { UIFeature } from "../../../src/settings/UIFeature";
|
||||
|
||||
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
|
||||
await page.getByRole("button", { name: "Create a space" }).click();
|
||||
@ -376,4 +377,53 @@ test.describe("Spaces", () => {
|
||||
await app.viewSpaceByName("Root Space");
|
||||
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
||||
});
|
||||
|
||||
test.describe("Should hide public spaces option if not allowed", () => {
|
||||
test.use({
|
||||
config: {
|
||||
setting_defaults: {
|
||||
[UIFeature.AllowCreatingPublicSpaces]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||
const menu = await openSpaceCreateMenu(page);
|
||||
await menu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await menu.getByRole("textbox", { name: "Name" }).fill("This is a private space");
|
||||
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||
await menu
|
||||
.getByRole("textbox", { name: "Description" })
|
||||
.fill("This is a private space because we can't make public ones");
|
||||
await menu.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Me and my teammates" }).click();
|
||||
|
||||
// Create the default General & Random rooms, as well as a custom "Projects" room
|
||||
await expect(page.getByPlaceholder("General")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("Random")).toBeVisible();
|
||||
await page.getByPlaceholder("Support").fill("Projects");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||
|
||||
// Assert rooms exist in the room list
|
||||
const roomList = page.getByRole("tree", { name: "Rooms" });
|
||||
await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||
|
||||
// Assert rooms exist in the space explorer
|
||||
await expect(
|
||||
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "General" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Random" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", { hasText: "Projects" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@ -26,6 +26,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import LabelledCheckbox from "../elements/LabelledCheckbox";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
type?: RoomType;
|
||||
@ -83,6 +84,7 @@ interface IState {
|
||||
|
||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
private readonly askToJoinEnabled: boolean;
|
||||
private readonly allowCreatingPublicRooms: boolean;
|
||||
private readonly supportsRestricted: boolean;
|
||||
private nameField = createRef<Field>();
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
@ -91,10 +93,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
super(props);
|
||||
|
||||
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||
this.allowCreatingPublicRooms = SettingsStore.getValue(UIFeature.AllowCreatingPublicRooms);
|
||||
this.supportsRestricted = !!this.props.parentSpace;
|
||||
const defaultPublic = this.allowCreatingPublicRooms && this.props.defaultPublic;
|
||||
|
||||
let joinRule = JoinRule.Invite;
|
||||
if (this.props.defaultPublic) {
|
||||
if (defaultPublic) {
|
||||
joinRule = JoinRule.Public;
|
||||
} else if (this.supportsRestricted) {
|
||||
joinRule = JoinRule.Restricted;
|
||||
@ -102,7 +106,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
this.state = {
|
||||
isPublicKnockRoom: this.props.defaultPublic || false,
|
||||
isPublicKnockRoom: defaultPublic || false,
|
||||
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(cli),
|
||||
joinRule,
|
||||
name: this.props.defaultName || "",
|
||||
@ -415,7 +419,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
labelKnock={
|
||||
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
|
||||
}
|
||||
labelPublic={_t("common|public_room")}
|
||||
labelPublic={this.allowCreatingPublicRooms ? _t("common|public_room") : undefined}
|
||||
labelRestricted={
|
||||
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ interface IProps {
|
||||
width?: number;
|
||||
labelInvite: string;
|
||||
labelKnock?: string;
|
||||
labelPublic: string;
|
||||
labelPublic?: string;
|
||||
labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
|
||||
onChange(value: JoinRule): void;
|
||||
}
|
||||
@ -38,11 +38,18 @@ const JoinRuleDropdown: React.FC<IProps> = ({
|
||||
<div key={JoinRule.Invite} className="mx_JoinRuleDropdown_invite">
|
||||
{labelInvite}
|
||||
</div>,
|
||||
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
|
||||
{labelPublic}
|
||||
</div>,
|
||||
] as NonEmptyArray<ReactElement & { key: string }>;
|
||||
|
||||
if (labelPublic) {
|
||||
options.push(
|
||||
(
|
||||
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
|
||||
{labelPublic}
|
||||
</div>
|
||||
) as ReactElement & { key: string },
|
||||
);
|
||||
}
|
||||
|
||||
if (labelKnock) {
|
||||
options.unshift(
|
||||
(
|
||||
@ -72,6 +79,7 @@ const JoinRuleDropdown: React.FC<IProps> = ({
|
||||
menuWidth={width}
|
||||
value={value}
|
||||
label={label}
|
||||
disabled={options.length === 1}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
|
||||
@ -45,6 +45,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Filter } from "../dialogs/spotlight/Filter";
|
||||
import { type OpenSpotlightPayload } from "../../../dispatcher/payloads/OpenSpotlightPayload.ts";
|
||||
import { useSettingValue } from "../../../hooks/useSettings.ts";
|
||||
import { UIFeature } from "../../../settings/UIFeature.ts";
|
||||
|
||||
export const createSpace = async (
|
||||
client: MatrixClient,
|
||||
@ -212,7 +214,10 @@ const SpaceCreateMenu: React.FC<{
|
||||
onFinished(): void;
|
||||
}> = ({ onFinished }) => {
|
||||
const cli = useMatrixClientContext();
|
||||
const [visibility, setVisibility] = useState<Visibility | null>(null);
|
||||
const settingAllowPublicSpaces = useSettingValue(UIFeature.AllowCreatingPublicSpaces);
|
||||
const [visibility, setVisibility] = useState<Visibility | null>(
|
||||
settingAllowPublicSpaces === false ? Visibility.Private : null,
|
||||
);
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
@ -303,16 +308,20 @@ const SpaceCreateMenu: React.FC<{
|
||||
} else {
|
||||
body = (
|
||||
<React.Fragment>
|
||||
<AccessibleButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
onClick={() => setVisibility(null)}
|
||||
title={_t("action|go_back")}
|
||||
/>
|
||||
{settingAllowPublicSpaces && (
|
||||
<AccessibleButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
onClick={() => setVisibility(null)}
|
||||
title={_t("action|go_back")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h2>
|
||||
{visibility === Visibility.Public
|
||||
? _t("create_space|public_heading")
|
||||
: _t("create_space|private_heading")}
|
||||
: settingAllowPublicSpaces
|
||||
? _t("create_space|private_heading")
|
||||
: _t("create_space|private_only_heading")}
|
||||
</h2>
|
||||
<p>
|
||||
{_t("create_space|add_details_prompt")} {_t("create_space|add_details_prompt_2")}
|
||||
|
||||
@ -718,6 +718,7 @@
|
||||
"personal_space_description": "A private space to organise your rooms",
|
||||
"private_description": "Invite only, best for yourself or teams",
|
||||
"private_heading": "Your private space",
|
||||
"private_only_heading": "Your space",
|
||||
"private_personal_description": "Make sure the right people have access to %(name)s",
|
||||
"private_personal_heading": "Who are you working with?",
|
||||
"private_space": "Me and my teammates",
|
||||
|
||||
@ -1425,6 +1425,14 @@ export const SETTINGS: Settings = {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
},
|
||||
[UIFeature.AllowCreatingPublicSpaces]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
},
|
||||
[UIFeature.AllowCreatingPublicRooms]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
},
|
||||
|
||||
// Electron-specific settings, they are stored by Electron and set/read over an IPC.
|
||||
// We store them over there are they are necessary to know before the renderer process launches.
|
||||
|
||||
@ -25,6 +25,8 @@ export const enum UIFeature {
|
||||
RoomHistorySettings = "UIFeature.roomHistorySettings",
|
||||
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
|
||||
BulkUnverifiedSessionsReminder = "UIFeature.BulkUnverifiedSessionsReminder",
|
||||
AllowCreatingPublicRooms = "UIFeature.allowCreatingPublicRooms",
|
||||
AllowCreatingPublicSpaces = "UIFeature.allowCreatingPublicSpaces",
|
||||
}
|
||||
|
||||
export enum UIComponent {
|
||||
|
||||
@ -16,25 +16,30 @@ import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<CreateRoomDialog />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getDomain: jest.fn().mockReturnValue("server.org"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
doesServerForceEncryptionForPreset: jest.fn(),
|
||||
// make every alias available
|
||||
getRoomIdForAlias: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
});
|
||||
|
||||
const getE2eeEnableToggleInputElement = () => screen.getByLabelText("Enable end-to-end encryption");
|
||||
// labelled toggle switch doesn't set the disabled attribute, only aria-disabled
|
||||
const getE2eeEnableToggleIsDisabled = () =>
|
||||
getE2eeEnableToggleInputElement().getAttribute("aria-disabled") === "true";
|
||||
|
||||
let mockClient: ReturnType<typeof getMockClientWithEventEmitter>;
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
getDomain: jest.fn().mockReturnValue("server.org"),
|
||||
getClientWellKnown: jest.fn(),
|
||||
doesServerForceEncryptionForPreset: jest.fn(),
|
||||
// make every alias available
|
||||
getRoomIdForAlias: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
});
|
||||
mockClient.doesServerForceEncryptionForPreset.mockResolvedValue(false);
|
||||
mockClient.getClientWellKnown.mockReturnValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const getComponent = (props = {}) => render(<CreateRoomDialog onFinished={jest.fn()} {...props} />);
|
||||
|
||||
it("should default to private room", async () => {
|
||||
|
||||
121
test/unit-tests/components/views/spaces/SpaceCreateMenu-test.tsx
Normal file
121
test/unit-tests/components/views/spaces/SpaceCreateMenu-test.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, cleanup } from "jest-matrix-react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
|
||||
import SpaceCreateMenu from "../../../../../src/components/views/spaces/SpaceCreateMenu";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsRooms,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
withClientContextRenderOptions,
|
||||
} from "../../../../test-utils";
|
||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<SpaceCreateMenu />", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsRooms(),
|
||||
createRoom: jest.fn(),
|
||||
getRoomIdForAlias: jest.fn().mockImplementation(async () => {
|
||||
throw new MatrixError({ errcode: "M_NOT_FOUND", error: "Test says no alias found" }, 404);
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
const { asFragment } = render(
|
||||
<SpaceCreateMenu onFinished={jest.fn()} />,
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should be able to create a public space", async () => {
|
||||
const onFinished = jest.fn();
|
||||
client.createRoom.mockResolvedValue({ room_id: "!room:id" });
|
||||
const { getByText, getByLabelText } = render(
|
||||
<SpaceCreateMenu onFinished={onFinished} />,
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
await userEvent.click(getByText("Public"));
|
||||
await userEvent.type(getByLabelText("Name"), "My Name");
|
||||
await userEvent.type(getByLabelText("Address"), "foobar");
|
||||
await userEvent.type(getByLabelText("Description"), "A description");
|
||||
await userEvent.click(getByText("Create"));
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
expect(client.createRoom).toHaveBeenCalledWith({
|
||||
creation_content: { type: "m.space" },
|
||||
initial_state: [
|
||||
{ content: { guest_access: "can_join" }, state_key: "", type: "m.room.guest_access" },
|
||||
{ content: { history_visibility: "world_readable" }, type: "m.room.history_visibility" },
|
||||
],
|
||||
name: "My Name",
|
||||
power_level_content_override: {
|
||||
events_default: 100,
|
||||
invite: 0,
|
||||
},
|
||||
preset: "public_chat",
|
||||
room_alias_name: "my-namefoobar",
|
||||
topic: "A description",
|
||||
visibility: "private",
|
||||
});
|
||||
});
|
||||
|
||||
it("should be prompted to automatically create a private space when configured", async () => {
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name === UIFeature.AllowCreatingPublicSpaces) {
|
||||
return false;
|
||||
}
|
||||
return realGetValue(name, roomId);
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
client.createRoom.mockResolvedValue({ room_id: "!room:id" });
|
||||
const { getByText, getByLabelText } = render(
|
||||
<SpaceCreateMenu onFinished={onFinished} />,
|
||||
withClientContextRenderOptions(client),
|
||||
);
|
||||
await userEvent.type(getByLabelText("Name"), "My Name");
|
||||
await userEvent.type(getByLabelText("Description"), "A description");
|
||||
await userEvent.click(getByText("Create"));
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
expect(client.createRoom).toHaveBeenCalledWith({
|
||||
creation_content: { type: "m.space" },
|
||||
initial_state: [
|
||||
{ content: { guest_access: "can_join" }, state_key: "", type: "m.room.guest_access" },
|
||||
{ content: { history_visibility: "invited" }, type: "m.room.history_visibility" },
|
||||
],
|
||||
name: "My Name",
|
||||
power_level_content_override: {
|
||||
events_default: 100,
|
||||
invite: 50,
|
||||
},
|
||||
room_alias_name: undefined,
|
||||
preset: "private_chat",
|
||||
topic: "A description",
|
||||
visibility: "private",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpaceCreateMenu /> should render 1`] = `<DocumentFragment />`;
|
||||
Loading…
x
Reference in New Issue
Block a user