Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/structures/SpaceRoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ const SpaceSetupFirstRooms: React.FC<{
return createRoom(space.client, {
createOpts: {
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
name,
},
name,
spinner: false,
encryption: false,
andView: false,
Expand Down Expand Up @@ -423,7 +423,7 @@ const SpaceSetupPublicShare: React.FC<ISpaceSetupPublicShareProps> = ({
<div className="mx_SpaceRoomView_publicShare">
<h1>
{_t("create_space|share_heading", {
name: justCreatedOpts?.createOpts?.name || space.name,
name: justCreatedOpts?.name || space.name,
})}
</h1>
<div className="mx_SpaceRoomView_description">{_t("create_space|share_description")}</div>
Expand All @@ -449,7 +449,7 @@ const SpaceSetupPrivateScope: React.FC<{
<h1>{_t("create_space|private_personal_heading")}</h1>
<div className="mx_SpaceRoomView_description">
{_t("create_space|private_personal_description", {
name: justCreatedOpts?.createOpts?.name || space.name,
name: justCreatedOpts?.name || space.name,
})}
</div>

Expand Down Expand Up @@ -686,7 +686,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
<SpaceSetupFirstRooms
space={this.props.space}
title={_t("create_space|setup_rooms_community_heading", {
spaceName: this.props.justCreatedOpts?.createOpts?.name || this.props.space.name,
spaceName: this.props.justCreatedOpts?.name || this.props.space.name,
})}
description={
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/CreateRoomDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = (opts.createOpts = {});
opts.roomType = this.props.type;
createOpts.name = this.state.name;
opts.name = this.state.name;

if (this.state.joinRule === JoinRule.Public) {
createOpts.visibility = Visibility.Public;
Expand All @@ -139,7 +139,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}

if (this.state.topic) {
createOpts.topic = this.state.topic;
opts.topic = this.state.topic;
}
if (this.state.noFederate) {
createOpts.creation_content = { "m.federate": false };
Expand Down
22 changes: 21 additions & 1 deletion src/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,19 @@ import { ElementCallEventType, ElementCallMemberEventType } from "./call-types";

export interface IOpts {
dmUserId?: string;
createOpts?: ICreateRoomOpts;
/**
* The name of the room to be created.
*/
name?: string;
/**
* The topic for the room.
*/
topic?: string;
/**
* Additional options to pass to the room creation API.
* Note: "name", "topic", and "avatar" should be set via their respective properties in IOpts.
*/
createOpts?: Omit<ICreateRoomOpts, "name" | "topic" | "avatar">;
spinner?: boolean;
guestAccess?: boolean;
encryption?: boolean;
Expand Down Expand Up @@ -251,6 +263,14 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
});
}

if (opts.name) {
createOpts.name = opts.name;
}

if (opts.topic) {
createOpts.topic = opts.topic;
}

if (opts.avatar) {
let url = opts.avatar;
if (opts.avatar instanceof File) {
Expand Down
179 changes: 176 additions & 3 deletions test/unit-tests/components/structures/SpaceRoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details.

import React from "react";
import { mocked, type MockedObject } from "jest-mock";
import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, cleanup, screen, fireEvent } from "jest-matrix-react";
import { type MatrixClient, MatrixEvent, Preset, Room } from "matrix-js-sdk/src/matrix";
import { render, cleanup, screen, fireEvent, waitFor, act } from "jest-matrix-react";

import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
Expand All @@ -17,6 +17,8 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts";
import DMRoomMap from "../../../../src/utils/DMRoomMap.ts";
import { type IOpts } from "../../../../src/createRoom.ts";
import SpaceStore from "../../../../src/stores/spaces/SpaceStore.ts";

describe("SpaceRoomView", () => {
let cli: MockedObject<MatrixClient>;
Expand Down Expand Up @@ -86,7 +88,7 @@ describe("SpaceRoomView", () => {
cleanup();
});

const renderSpaceRoomView = async (): Promise<ReturnType<typeof render>> => {
const renderSpaceRoomView = async (justCreatedOpts?: IOpts): Promise<ReturnType<typeof render>> => {
const resizeNotifier = new ResizeNotifier();
const permalinkCreator = new RoomPermalinkCreator(space);

Expand All @@ -97,6 +99,7 @@ describe("SpaceRoomView", () => {
permalinkCreator={permalinkCreator}
onJoinButtonClicked={jest.fn()}
onRejectButtonClicked={jest.fn()}
justCreatedOpts={justCreatedOpts}
/>,
withClientContextRenderOptions(cli),
);
Expand All @@ -113,5 +116,175 @@ describe("SpaceRoomView", () => {

expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});

it("shows SpaceLandingAddButton context menu when Add button is clicked", async () => {
await renderSpaceRoomView();
await expect(screen.findByText("Welcome to")).resolves.toBeVisible();

const addButton = screen.getByRole("button", { name: /add/i });
fireEvent.click(addButton);

expect(await screen.findByText(/new room/i)).toBeInTheDocument();
expect(screen.getByText(/add existing room/i)).toBeInTheDocument();
});
});

describe("Spaces: creating a new community space", () => {
it("asks what topics you want to discuss, creates rooms for them and offers to share", async () => {
cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" });
SpaceStore.instance.addRoomToSpace = jest.fn();

// Given we are creating a space
const view = await renderSpaceRoomView({
createOpts: { preset: Preset.PublicChat },
name: "My MySpace Space",
});

// Then we are asked what topics we want
expect(
view.getByRole("heading", { name: "What are some things you want to discuss in My MySpace Space?" }),
).toBeInTheDocument();

// And some defaults are suggested
expect(view.getByPlaceholderText(/general/i)).toBeInTheDocument();
expect(view.getByPlaceholderText(/random/i)).toBeInTheDocument();
expect(view.getByPlaceholderText(/support/i)).toBeInTheDocument();

// When we enter some room names
const input1 = view.getAllByRole("textbox")[0];
const input2 = view.getAllByRole("textbox")[1];
fireEvent.change(input1, { target: { value: "Room 1" } });
fireEvent.change(input2, { target: { value: "Room 2" } });

// And click "Continue"
const button = view.getByRole("button", { name: "Continue" });
fireEvent.click(button);

// Then we create 2 rooms
await waitFor(() => {
expect(cli.createRoom).toHaveBeenCalledTimes(2);
});

// And offer the user to share this space
await waitFor(() =>
expect(view.getByRole("heading", { name: "Share My MySpace Space" })).toBeInTheDocument(),
);
expect(view.getByRole("button", { name: /Share invite link/ })).toBeInTheDocument();

// And allow them to continue to the first room
expect(view.getByRole("button", { name: "Go to my first room" })).toBeInTheDocument();
});

it("shows 'Skip for now' when all fields are empty, 'Continue' when any field is filled", async () => {
// Given we are creating a space
const view = await renderSpaceRoomView({
createOpts: { preset: Preset.PublicChat },
});

// When we clear all the topics
view.getAllByRole("textbox").forEach((input) => fireEvent.change(input, { target: { value: "" } }));

// Then the button reads "Skip for now"
expect(view.getByRole("button", { name: "Skip for now" })).toBeVisible();

// But when we enter a topic
fireEvent.change(view.getAllByRole("textbox")[0], { target: { value: "Room" } });

// Then the button says "Continue"
expect(view.getByRole("button", { name: "Continue" })).toBeVisible();
});

it("shows error message if room creation fails", async () => {
// Given we are creating a space
const view = await renderSpaceRoomView({
createOpts: { preset: Preset.PublicChat },
});

// And when we create a room it will fail
cli.createRoom.mockRejectedValue(new Error("fail"));

// When we create the space
fireEvent.change(view.getAllByRole("textbox")[0], { target: { value: "Room A" } });
fireEvent.click(view.getByRole("button", { name: "Continue" }));

// Then we display an error message because it failed
await waitFor(() => {
expect(
view.getByText((content) => content.toLowerCase().includes("failed to create initial space rooms")),
).toBeInTheDocument();
});
});

it("disables button and shows 'Creating rooms' while busy", async () => {
// Given we are creating a space
const view = await renderSpaceRoomView({
createOpts: { preset: Preset.PublicChat },
});

// And creating a room will be slow
cli.createRoom.mockImplementation(
() =>
new Promise(() => {
// This promise never resolves
}),
);

// When we create the space
fireEvent.change(view.getAllByRole("textbox")[0], { target: { value: "Room A" } });
fireEvent.click(view.getByRole("button", { name: "Continue" }));

// Then the "Creating rooms..." message is displayed
const button = view.getByRole("button");
expect(button).toBeDisabled();
expect(button).toHaveValue("Creating rooms…"); // Note the ellipsis
});
});

describe("Spaces: creating a new private space", () => {
it("creates rooms inside a private space for a team", async () => {
cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" });
SpaceStore.instance.addRoomToSpace = jest.fn();

// When I create a private space
const view = await renderSpaceRoomView({
createOpts: { preset: Preset.PrivateChat },
name: "Private space",
topic: "a private space for team A",
});

// Then I am asked whether it's individual or team
expect(view.getByRole("heading", { name: "Who are you working with?" })).toBeInTheDocument();

// And when I say team
act(() =>
view
.getByRole("button", {
name: "Me and my teammates A private space for you and your teammates",
})
.click(),
);

// Then I am asked what rooms to create
expect(view.getByRole("heading", { name: "What projects are your team working on?" })).toBeInTheDocument();

expect(view.getByPlaceholderText(/general/i)).toBeInTheDocument();
expect(view.getByPlaceholderText(/random/i)).toBeInTheDocument();
expect(view.getByPlaceholderText(/support/i)).toBeInTheDocument();

// And when I enter some room names
const input1 = view.getAllByRole("textbox")[0];
const input2 = view.getAllByRole("textbox")[1];
fireEvent.change(input1, { target: { value: "Room 1" } });
fireEvent.change(input2, { target: { value: "Room 2" } });

// And click "Continue"
const button = view.getByRole("button", { name: "Continue" });
fireEvent.click(button);

// Then the rooms are created
await waitFor(() => {
expect(cli.createRoom).toHaveBeenCalledTimes(2);
});
});
});
});
Loading
Loading