New room list: add empty state (#29512)

* refactor: extract room creation and right verification

* refactor: update `RoomListHeaderViewModel` to use utils

* feat(room list filter): add filter key to `PrimaryFilter` model

* feat(room list filter): return active primary filter

* feat(room list): add create room action and rights verification

* test: update room list tests

* feat(empty room list): add empty room list

* test(empty room list): add empty room list tests

* feat(room list): use empty room list in `RoomListView`

* test(room list panel): update tests

* test(e2e): add e2e tests for empty room list

* test(e2e): update room list header snapshot
This commit is contained in:
Florian Duros
2025-03-18 11:02:33 +01:00
committed by GitHub
parent 55b0b1107e
commit 7de54a385e
26 changed files with 991 additions and 155 deletions

View File

@@ -18,14 +18,6 @@ test.describe("Room list filters and sort", () => {
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
function getPrimaryFilters(page: Page) {
return page.getByRole("listbox", { name: "Room list filters" });
}
@@ -33,56 +25,113 @@ test.describe("Room list filters and sort", () => {
test.beforeEach(async ({ page, app, bot, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
await app.client.createRoom({ name: "empty room" });
const unReadDmId = await bot.createRoom({
name: "unread dm",
invite: [user.userId],
is_direct: true,
});
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
await bot.joinRoom(unReadRoomId);
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, favouriteId);
});
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomList = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);
const allFilters = await primaryFilters.locator("option").all();
for (const filter of allFilters) {
expect(await filter.getAttribute("aria-selected")).toBe("false");
test.describe("Room list", () => {
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Unread" }).click();
// only one room should be visible
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
test.beforeEach(async ({ page, app, bot, user }) => {
await app.client.createRoom({ name: "empty room" });
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
const unReadDmId = await bot.createRoom({
name: "unread dm",
invite: [user.userId],
is_direct: true,
});
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
await bot.joinRoom(unReadRoomId);
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3);
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, favouriteId);
});
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomList = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);
const allFilters = await primaryFilters.locator("option").all();
for (const filter of allFilters) {
expect(await filter.getAttribute("aria-selected")).toBe("false");
}
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Unread" }).click();
// only one room should be visible
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3);
});
});
test.describe("Empty room list", () => {
/**
* Get the empty state
* @param page
*/
function getEmptyRoomList(page: Page) {
return page.getByTestId("empty-room-list");
}
test(
"should render the default placeholder when there is no filter",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
},
);
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Unread" }).click();
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
});
["People", "Rooms", "Favourite"].forEach((filter) => {
test(
`should render the placeholder for ${filter} filter`,
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
},
);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -270,6 +270,7 @@
@import "./views/right_panel/_VerificationPanel.pcss";
@import "./views/right_panel/_WidgetCard.pcss";
@import "./views/room_settings/_AliasSettings.pcss";
@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss";
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 New Vector Ltd.
*
* 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.
*/
.mx_EmptyRoomList_GenericPlaceholder {
align-self: center;
/** It should take 2/3 of the width **/
width: 66%;
/** It should be positioned at 1/3 of the height **/
padding-top: 33%;
.mx_EmptyRoomList_GenericPlaceholder_title {
font: var(--cpd-font-body-lg-semibold);
text-align: center;
}
.mx_EmptyRoomList_GenericPlaceholder_description {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
text-align: center;
}
.mx_EmptyRoomList_DefaultPlaceholder {
margin-top: var(--cpd-space-4x);
}
button {
width: 100%;
}
}

View File

@@ -6,10 +6,8 @@
*/
import { useCallback } from "react";
import { EventTimeline, EventType, JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import PosthogTrackers from "../../../PosthogTrackers";
@@ -32,6 +30,7 @@ import {
} from "../../../utils/space";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { createRoom, hasCreateRoomRights } from "./utils";
/**
* Hook to get the active space and its title.
@@ -128,14 +127,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
const { activeSpace, title } = useSpace();
const isSpaceRoom = Boolean(activeSpace);
const canCreateRoomInSpace = Boolean(
activeSpace
?.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.maySendStateEvent(EventType.RoomAvatar, matrixClient.getSafeUserId()),
);
// If we are in a space, we check canCreateRoomInSpace
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms) && (!isSpaceRoom || canCreateRoomInSpace);
const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace);
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms");
const displayComposeMenu = canCreateRoom || canCreateVideoRoom;
const displaySpaceMenu = isSpaceRoom;
@@ -151,13 +143,9 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
}, []);
const createRoom = useCallback(
const createRoomMemoized = useCallback(
(e: Event) => {
if (activeSpace) {
showCreateNewRoom(activeSpace);
} else {
defaultDispatcher.fire(Action.CreateRoom);
}
createRoom(activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
},
[activeSpace],
@@ -213,7 +201,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
canInviteInSpace,
canAccessSpaceSettings,
createChatRoom,
createRoom,
createRoom: createRoomMemoized,
createVideoRoom,
openSpaceHome,
inviteInSpace,

View File

@@ -5,22 +5,55 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { useCallback } from "react";
import type { Room } from "matrix-js-sdk/src/matrix";
import { type PrimaryFilter, type SecondaryFilters, useFilteredRooms } from "./useFilteredRooms";
import { type SortOption, useSorter } from "./useSorter";
import { useMessagePreviewToggle } from "./useMessagePreviewToggle";
import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
export interface RoomListViewState {
/**
* A list of rooms to be displayed in the left panel.
*/
rooms: Room[];
/**
* Create a chat room
* @param e - The click event
*/
createChatRoom: () => void;
/**
* Whether the user can create a room in the current space
*/
canCreateRoom: boolean;
/**
* Create a room
* @param e - The click event
*/
createRoom: () => void;
/**
* A list of objects that provide the view enough information
* to render primary room filters.
*/
primaryFilters: PrimaryFilter[];
/**
* The currently active primary filter.
* If no primary filter is active, this will be undefined.
*/
activePrimaryFilter?: PrimaryFilter;
/**
* A function to activate a given secondary filter.
*/
@@ -57,13 +90,30 @@ export interface RoomListViewState {
* @see {@link RoomListViewState} for more information about what this view model returns.
*/
export function useRoomListViewModel(): RoomListViewState {
const { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter } = useFilteredRooms();
const matrixClient = useMatrixClientContext();
const { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter } =
useFilteredRooms();
const currentSpace = useEventEmitterState<Room | null>(
SpaceStore.instance,
UPDATE_SELECTED_SPACE,
() => SpaceStore.instance.activeSpaceRoom,
);
const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace);
const { activeSortOption, sort } = useSorter();
const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle();
const createChatRoom = useCallback(() => dispatcher.fire(Action.CreateChat), []);
const createRoom = useCallback(() => createRoomFunc(currentSpace), [currentSpace]);
return {
rooms,
canCreateRoom,
createRoom,
createChatRoom,
primaryFilters,
activePrimaryFilter,
activateSecondaryFilter,
activeSecondaryFilter,
activeSortOption,

View File

@@ -27,6 +27,8 @@ export interface PrimaryFilter {
active: boolean;
// Text that can be used in the UI to represent this filter.
name: string;
// The key of the filter
key: FilterKey;
}
interface FilteredRooms {
@@ -34,6 +36,11 @@ interface FilteredRooms {
rooms: Room[];
activateSecondaryFilter: (filter: SecondaryFilters) => void;
activeSecondaryFilter: SecondaryFilters;
/**
* The currently active primary filter.
* If no primary filter is active, this will be undefined.
*/
activePrimaryFilter?: PrimaryFilter;
}
const filterKeyToNameMap: Map<FilterKey, TranslationKey> = new Map([
@@ -172,6 +179,7 @@ export function useFilteredRooms(): FilteredRooms {
},
active: primaryFilter === key,
name,
key,
};
};
const filters: PrimaryFilter[] = [];
@@ -184,5 +192,7 @@ export function useFilteredRooms(): FilteredRooms {
return filters;
}, [primaryFilter, updateRoomsFromStore, secondaryFilter]);
return { primaryFilters, rooms, activateSecondaryFilter, activeSecondaryFilter };
const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]);
return { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter };
}

View File

@@ -5,11 +5,14 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Room, KnownMembership } from "matrix-js-sdk/src/matrix";
import { type Room, KnownMembership, EventTimeline, EventType, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { isKnockDenied } from "../../../utils/membership";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { showCreateNewRoom } from "../../../utils/space";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
/**
* Check if the user has access to the options menu.
@@ -23,3 +26,33 @@ export function hasAccessToOptionsMenu(room: Room): boolean {
shouldShowComponent(UIComponent.RoomOptionsMenu))
);
}
/**
* Create a room
* @param space - The space to create the room in
*/
export async function createRoom(space?: Room | null): Promise<void> {
if (space) {
await showCreateNewRoom(space);
} else {
dispatcher.fire(Action.CreateRoom);
}
}
/**
* Check if the user has the rights to create a room in the given space
* If the space is not provided, it will check if the user has the rights to create a room in general
* @param matrixClient
* @param space
*/
export function hasCreateRoomRights(matrixClient: MatrixClient, space?: Room | null): boolean {
const hasUIRight = shouldShowComponent(UIComponent.CreateRooms);
if (!space || !hasUIRight) return hasUIRight;
return Boolean(
space
?.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.maySendStateEvent(EventType.RoomAvatar, matrixClient.getSafeUserId()),
);
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2025 New Vector Ltd.
*
* 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, { type JSX, type PropsWithChildren } from "react";
import { Button } from "@vector-im/compound-web";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
import { _t } from "../../../../languageHandler";
import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms";
interface EmptyRoomListProps {
/**
* The view model for the room list
*/
vm: RoomListViewState;
}
/**
* The empty state for the room list
*/
export function EmptyRoomList({ vm }: EmptyRoomListProps): JSX.Element | undefined {
// If there is no active primary filter, show the default empty state
if (!vm.activePrimaryFilter) return <DefaultPlaceholder vm={vm} />;
switch (vm.activePrimaryFilter.key) {
case FilterKey.FavouriteFilter:
return (
<GenericPlaceholder
title={_t("room_list|empty|no_favourites")}
description={_t("room_list|empty|no_favourites_description")}
/>
);
case FilterKey.PeopleFilter:
return (
<GenericPlaceholder
title={_t("room_list|empty|no_people")}
description={_t("room_list|empty|no_people_description")}
/>
);
case FilterKey.RoomsFilter:
return (
<GenericPlaceholder
title={_t("room_list|empty|no_rooms")}
description={_t("room_list|empty|no_rooms_description")}
/>
);
case FilterKey.UnreadFilter:
return <UnreadPlaceholder filter={vm.activePrimaryFilter} />;
default:
return undefined;
}
}
interface GenericPlaceholderProps {
/**
* The title of the placeholder
*/
title: string;
/**
* The description of the placeholder
*/
description?: string;
}
/**
* A generic placeholder for the room list
*/
function GenericPlaceholder({ title, description, children }: PropsWithChildren<GenericPlaceholderProps>): JSX.Element {
return (
<Flex
data-testid="empty-room-list"
className="mx_EmptyRoomList_GenericPlaceholder"
direction="column"
align="stretch"
justify="center"
gap="var(--cpd-space-2x)"
>
<span className="mx_EmptyRoomList_GenericPlaceholder_title">{title}</span>
{description && <span className="mx_EmptyRoomList_GenericPlaceholder_description">{description}</span>}
{children}
</Flex>
);
}
interface DefaultPlaceholderProps {
/**
* The view model for the room list
*/
vm: RoomListViewState;
}
/**
* The default empty state for the room list when no primary filter is active
* The user can create chat or room (if they have the permission)
*/
function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element {
return (
<GenericPlaceholder
title={_t("room_list|empty|no_chats")}
description={
vm.canCreateRoom
? _t("room_list|empty|no_chats_description")
: _t("room_list|empty|no_chats_description_no_room_rights")
}
>
<Flex
className="mx_EmptyRoomList_DefaultPlaceholder"
align="center"
justify="center"
direction="column"
gap="var(--cpd-space-4x)"
>
<Button size="sm" kind="secondary" Icon={UserAddIcon} onClick={vm.createChatRoom}>
{_t("action|new_message")}
</Button>
{vm.canCreateRoom && (
<Button size="sm" kind="secondary" Icon={RoomIcon} onClick={vm.createRoom}>
{_t("action|new_room")}
</Button>
)}
</Flex>
</GenericPlaceholder>
);
}
interface UnreadPlaceholderProps {
filter: PrimaryFilter;
}
/**
* The empty state for the room list when the unread filter is active
*/
function UnreadPlaceholder({ filter }: UnreadPlaceholderProps): JSX.Element {
return (
<GenericPlaceholder title={_t("room_list|empty|no_unread")}>
<Button kind="tertiary" onClick={filter.toggle}>
{_t("room_list|empty|show_chats")}
</Button>
</GenericPlaceholder>
);
}

View File

@@ -9,6 +9,7 @@ import React, { type JSX } from "react";
import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel";
import { RoomList } from "./RoomList";
import { EmptyRoomList } from "./EmptyRoomList";
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
/**
@@ -16,10 +17,12 @@ import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
*/
export function RoomListView(): JSX.Element {
const vm = useRoomListViewModel();
const isRoomListEmpty = vm.rooms.length === 0;
return (
<>
<RoomListPrimaryFilters vm={vm} />
<RoomList vm={vm} />
{isRoomListEmpty ? <EmptyRoomList vm={vm} /> : <RoomList vm={vm} />}
</>
);
}

View File

@@ -2096,6 +2096,19 @@
"add_space_label": "Add space",
"breadcrumbs_empty": "No recently visited rooms",
"breadcrumbs_label": "Recently visited rooms",
"empty": {
"no_chats": "No chats yet",
"no_chats_description": "Get started by messaging someone or by creating a room",
"no_chats_description_no_room_rights": "Get started by messaging someone",
"no_favourites": "You don't have favourite chat yet",
"no_favourites_description": "You can add a chat to your favourites in the chat settings",
"no_people": "You dont have direct chats with anyone yet",
"no_people_description": "You can deselect filters in order to see your other chats",
"no_rooms": "Youre not in any room yet",
"no_rooms_description": "You can deselect filters in order to see your other chats",
"no_unread": "Congrats! You dont have any unread messages",
"show_chats": "Show all chats"
},
"failed_add_tag": "Failed to add tag %(tagName)s to room",
"failed_remove_tag": "Failed to remove tag %(tagName)s from room",
"failed_set_dm_tag": "Failed to set direct message tag",

View File

@@ -6,13 +6,12 @@
*/
import { renderHook } from "jest-matrix-react";
import { JoinRule, type MatrixClient, type Room, type RoomState, RoomType } from "matrix-js-sdk/src/matrix";
import { JoinRule, type MatrixClient, type Room, RoomType } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { useRoomListHeaderViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListHeaderViewModel";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import { mkStubRoom, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
@@ -23,9 +22,11 @@ import {
showSpacePreferences,
showSpaceSettings,
} from "../../../../../src/utils/space";
import { createRoom, hasCreateRoomRights } from "../../../../../src/components/viewmodels/roomlist/utils";
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
createRoom: jest.fn(),
}));
jest.mock("../../../../../src/utils/space", () => ({
@@ -68,19 +69,19 @@ describe("useRoomListHeaderViewModel", () => {
});
it("should be displayComposeMenu=true and canCreateRoom=true if the user can creates room", () => {
mocked(shouldShowComponent).mockReturnValue(false);
mocked(hasCreateRoomRights).mockReturnValue(false);
const { result, rerender } = render();
expect(result.current.displayComposeMenu).toBe(false);
expect(result.current.canCreateRoom).toBe(false);
mocked(shouldShowComponent).mockReturnValue(true);
mocked(hasCreateRoomRights).mockReturnValue(true);
rerender();
expect(result.current.displayComposeMenu).toBe(true);
expect(result.current.canCreateRoom).toBe(true);
});
it("should be displayComposeMenu=true if the user can creates video room", () => {
mocked(shouldShowComponent).mockReturnValue(false);
mocked(hasCreateRoomRights).mockReturnValue(false);
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
const { result } = render();
@@ -93,25 +94,6 @@ describe("useRoomListHeaderViewModel", () => {
expect(result.current.displaySpaceMenu).toBe(true);
});
it("should be canCreateRoom=false if the user has not the right to create a room in a space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
const { result } = render();
expect(result.current.canCreateRoom).toBe(false);
});
it("should be canCreateRoom=true if the user has the right to create a room in a space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
jest.spyOn(space.getLiveTimeline(), "getState").mockReturnValue({
maySendStateEvent: jest.fn().mockReturnValue(true),
} as unknown as RoomState);
const { result } = render();
expect(result.current.canCreateRoom).toBe(true);
});
it("should be canInviteInSpace=true if the space join rule is public", () => {
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
jest.spyOn(space, "getJoinRule").mockReturnValue(JoinRule.Public);
@@ -150,20 +132,19 @@ describe("useRoomListHeaderViewModel", () => {
expect(spy).toHaveBeenCalledWith(Action.CreateChat);
});
it("should fire Action.CreateRoom when createRoom is called", () => {
const spy = jest.spyOn(defaultDispatcher, "fire");
it("should call createRoom from utils when createRoom is called", () => {
const { result } = render();
result.current.createRoom(new Event("click"));
expect(spy).toHaveBeenCalledWith(Action.CreateRoom);
expect(createRoom).toHaveBeenCalled();
});
it("should call showCreateNewRoom when createRoom is called in a space", () => {
it("should call createRoom from utils when createRoom is called in a space", () => {
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(space);
const { result } = render();
result.current.createRoom(new Event("click"));
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
expect(createRoom).toHaveBeenCalledWith(space);
});
it("should fire Action.CreateRoom with RoomType.UnstableCall when createVideoRoom is called and feature_element_call_video_rooms is enabled", () => {

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { range } from "lodash";
import { act, renderHook, waitFor } from "jest-matrix-react";
import { mocked } from "jest-mock";
import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3";
import { mkStubRoom } from "../../../../test-utils";
@@ -17,6 +18,14 @@ import { SecondaryFilters } from "../../../../../src/components/viewmodels/rooml
import { SortingAlgorithm } from "../../../../../src/stores/room-list-v3/skip-list/sorters";
import { SortOption } from "../../../../../src/components/viewmodels/roomlist/useSorter";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
import dispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
jest.mock("../../../../../src/components/viewmodels/roomlist/utils", () => ({
hasCreateRoomRights: jest.fn().mockReturnValue(false),
createRoom: jest.fn(),
}));
describe("RoomListViewModel", () => {
function mockAndCreateRooms() {
@@ -139,6 +148,19 @@ describe("RoomListViewModel", () => {
);
});
it("should return the current active primary filter", async () => {
// Let's say that the user's preferred sorting is alphabetic
mockAndCreateRooms();
const { result: vm } = renderHook(() => useRoomListViewModel());
// Toggle people filter
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
expect(vm.current.primaryFilters[i].active).toEqual(false);
act(() => vm.current.primaryFilters[i].toggle());
// The active primary filter should be the People filter
expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]);
});
const testcases: Array<[string, { secondary: SecondaryFilters; filterKey: FilterKey }, string]> = [
[
"Mentions only",
@@ -240,4 +262,31 @@ describe("RoomListViewModel", () => {
expect(fn).toHaveBeenCalled();
});
});
describe("Create room and chat", () => {
it("should be canCreateRoom=false if hasCreateRoomRights=false", () => {
mocked(hasCreateRoomRights).mockReturnValue(false);
const { result } = renderHook(() => useRoomListViewModel());
expect(result.current.canCreateRoom).toBe(false);
});
it("should be canCreateRoom=true if hasCreateRoomRights=true", () => {
mocked(hasCreateRoomRights).mockReturnValue(true);
const { result } = renderHook(() => useRoomListViewModel());
expect(result.current.canCreateRoom).toBe(true);
});
it("should call createRoom", () => {
const { result } = renderHook(() => useRoomListViewModel());
result.current.createRoom();
expect(mocked(createRoom)).toHaveBeenCalled();
});
it("should dispatch Action.CreateChat", () => {
const spy = jest.spyOn(dispatcher, "fire");
const { result } = renderHook(() => useRoomListViewModel());
result.current.createChatRoom();
expect(spy).toHaveBeenCalledWith(Action.CreateChat);
});
});
});

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 New Vector Ltd.
*
* 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 { mocked } from "jest-mock";
import type { MatrixClient, Room, RoomState } from "matrix-js-sdk/src/matrix";
import { createTestClient, mkStubRoom } from "../../../../test-utils";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { showCreateNewRoom } from "../../../../../src/utils/space";
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
jest.mock("../../../../../src/utils/space", () => ({
showCreateNewRoom: jest.fn(),
}));
describe("utils", () => {
let matrixClient: MatrixClient;
let space: Room;
beforeEach(() => {
matrixClient = createTestClient();
space = mkStubRoom("spaceId", "spaceName", matrixClient);
});
describe("createRoom", () => {
it("should fire Action.CreateRoom when createRoom is called without a space", async () => {
const spy = jest.spyOn(defaultDispatcher, "fire");
await createRoom();
expect(spy).toHaveBeenCalledWith(Action.CreateRoom);
});
it("should call showCreateNewRoom when createRoom is called in a space", async () => {
await createRoom(space);
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
});
});
describe("hasCreateRoomRights", () => {
it("should return false when UIComponent.CreateRooms is disabled", () => {
mocked(shouldShowComponent).mockReturnValue(false);
expect(hasCreateRoomRights(matrixClient, space)).toBe(false);
});
it("should return true when UIComponent.CreateRooms is enabled and no space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
expect(hasCreateRoomRights(matrixClient)).toBe(true);
});
it("should return false in space when UIComponent.CreateRooms is enabled and the user doesn't have the rights", () => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(space.getLiveTimeline(), "getState").mockReturnValue({
maySendStateEvent: jest.fn().mockReturnValue(true),
} as unknown as RoomState);
expect(hasCreateRoomRights(matrixClient)).toBe(true);
});
});
});

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 New Vector Ltd.
*
* 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, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
import { EmptyRoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/EmptyRoomList";
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
describe("<EmptyRoomList />", () => {
let vm: RoomListViewState;
beforeEach(() => {
vm = {
rooms: [],
primaryFilters: [],
activateSecondaryFilter: jest.fn().mockReturnValue({}),
activeSecondaryFilter: SecondaryFilters.AllActivity,
sort: jest.fn(),
activeSortOption: SortOption.Activity,
createRoom: jest.fn(),
createChatRoom: jest.fn(),
canCreateRoom: true,
shouldShowMessagePreview: false,
toggleMessagePreview: jest.fn(),
};
});
test("should render the default placeholder when there is no filter", async () => {
const user = userEvent.setup();
const { asFragment } = render(<EmptyRoomList vm={vm} />);
expect(screen.getByText("No chats yet")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "New message" }));
expect(vm.createChatRoom).toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: "New room" }));
expect(vm.createRoom).toHaveBeenCalled();
});
test("should not render the new room button if the user doesn't have the rights to create a room", async () => {
const newState = { ...vm, canCreateRoom: false };
const { asFragment } = render(<EmptyRoomList vm={newState} />);
expect(screen.queryByRole("button", { name: "New room" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
it("should display the empty state for the unread filter", async () => {
const user = userEvent.setup();
const activePrimaryFilter = {
toggle: jest.fn(),
active: true,
name: "unread",
key: FilterKey.UnreadFilter,
};
const newState = {
...vm,
activePrimaryFilter,
};
const { asFragment } = render(<EmptyRoomList vm={newState} />);
await user.click(screen.getByRole("button", { name: "Show all chats" }));
expect(activePrimaryFilter.toggle).toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot();
});
it.each([
{ key: FilterKey.FavouriteFilter, name: "favourite" },
{ key: FilterKey.PeopleFilter, name: "people" },
{ key: FilterKey.RoomsFilter, name: "rooms" },
])("should display empty state for filter $name", ({ name, key }) => {
const activePrimaryFilter = {
toggle: jest.fn(),
active: true,
name,
key,
};
const newState = { ...vm, activePrimaryFilter };
const { asFragment } = render(<EmptyRoomList vm={newState} />);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -37,6 +37,9 @@ describe("<RoomList />", () => {
activeSortOption: SortOption.Activity,
shouldShowMessagePreview: false,
toggleMessagePreview: jest.fn(),
createRoom: jest.fn(),
createChatRoom: jest.fn(),
canCreateRoom: true,
};
// Needed to render a room list cell

View File

@@ -13,6 +13,7 @@ import { type RoomListViewState } from "../../../../../../src/components/viewmod
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
import { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
describe("<RoomListPrimaryFilters />", () => {
let vm: RoomListViewState;
@@ -20,9 +21,12 @@ describe("<RoomListPrimaryFilters />", () => {
beforeEach(() => {
vm = {
rooms: [],
canCreateRoom: true,
createRoom: jest.fn(),
createChatRoom: jest.fn(),
primaryFilters: [
{ name: "People", active: false, toggle: jest.fn() },
{ name: "Rooms", active: true, toggle: jest.fn() },
{ name: "People", active: false, toggle: jest.fn(), key: FilterKey.PeopleFilter },
{ name: "Rooms", active: true, toggle: jest.fn(), key: FilterKey.RoomsFilter },
],
activateSecondaryFilter: () => {},
activeSecondaryFilter: SecondaryFilters.AllActivity,

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 New Vector Ltd.
*
* 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 { mocked } from "jest-mock";
import { render, screen } from "jest-matrix-react";
import React from "react";
import {
type RoomListViewState,
useRoomListViewModel,
} from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
import { SortOption } from "../../../../../../src/components/viewmodels/roomlist/useSorter";
import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListView";
import { mkRoom, stubClient } from "../../../../../test-utils";
jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewModel", () => ({
useRoomListViewModel: jest.fn(),
}));
describe("<RoomListView />", () => {
const defaultValue: RoomListViewState = {
rooms: [],
primaryFilters: [],
activateSecondaryFilter: jest.fn().mockReturnValue({}),
activeSecondaryFilter: SecondaryFilters.AllActivity,
sort: jest.fn(),
activeSortOption: SortOption.Activity,
createRoom: jest.fn(),
createChatRoom: jest.fn(),
canCreateRoom: true,
toggleMessagePreview: jest.fn(),
shouldShowMessagePreview: false,
};
const matrixClient = stubClient();
afterEach(() => {
jest.resetAllMocks();
});
it("should render an empty room list", () => {
mocked(useRoomListViewModel).mockReturnValue(defaultValue);
render(<RoomListView />);
expect(screen.getByText("No chats yet")).toBeInTheDocument();
});
it("should render a room list", () => {
mocked(useRoomListViewModel).mockReturnValue({
...defaultValue,
rooms: [mkRoom(matrixClient, "testing room")],
});
render(<RoomListView />);
expect(screen.getByRole("grid", { name: "Room list" })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,204 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EmptyRoomList /> should display empty state for filter favourite 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
You don't have favourite chat yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
You can add a chat to your favourites in the chat settings
</span>
</div>
</DocumentFragment>
`;
exports[`<EmptyRoomList /> should display empty state for filter people 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
You dont have direct chats with anyone yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
You can deselect filters in order to see your other chats
</span>
</div>
</DocumentFragment>
`;
exports[`<EmptyRoomList /> should display empty state for filter rooms 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
Youre not in any room yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
You can deselect filters in order to see your other chats
</span>
</div>
</DocumentFragment>
`;
exports[`<EmptyRoomList /> should display the empty state for the unread filter 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
Congrats! You dont have any unread messages
</span>
<button
class="_button_vczzf_8"
data-kind="tertiary"
data-size="lg"
role="button"
tabindex="0"
>
Show all chats
</button>
</div>
</DocumentFragment>
`;
exports[`<EmptyRoomList /> should not render the new room button if the user doesn't have the rights to create a room 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
No chats yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
Get started by messaging someone
</span>
<div
class="mx_Flex mx_EmptyRoomList_DefaultPlaceholder"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
/>
</svg>
New message
</button>
</div>
</div>
</DocumentFragment>
`;
exports[`<EmptyRoomList /> should render the default placeholder when there is no filter 1`] = `
<DocumentFragment>
<div
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
No chats yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
Get started by messaging someone or by creating a room
</span>
<div
class="mx_Flex mx_EmptyRoomList_DefaultPlaceholder"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
/>
</svg>
New message
</button>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m8.566 17-.944 4.094q-.086.406-.372.656t-.687.25q-.543 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.801-3.5H3.158q-.572 0-.916-.484a1.27 1.27 0 0 1-.2-1.078 1.12 1.12 0 0 1 1.116-.938H6.85l1.145-5h-3.12q-.57 0-.915-.484a1.27 1.27 0 0 1-.2-1.078A1.12 1.12 0 0 1 4.875 7h3.691l.945-4.094q.085-.406.372-.656.286-.25.686-.25.544 0 .887.469.345.468.2 1.031l-.8 3.5h4.578l.944-4.094q.085-.406.372-.656.286-.25.687-.25.543 0 .887.469t.2 1.031L17.723 7h3.119q.573 0 .916.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937H17.15l-1.145 5h3.12q.57 0 .915.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937h-3.691l-.944 4.094q-.087.406-.373.656t-.686.25q-.544 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.8-3.5zm.573-2.5h4.578l1.144-5h-4.578z"
/>
</svg>
New room
</button>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -113,34 +113,45 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
</li>
</ul>
<div
class="mx_RoomList"
data-testid="room-list"
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<div
style="overflow: visible; height: 0px; width: 0px;"
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
<div
aria-label="Room list"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
No chats yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
Get started by messaging someone
</span>
<div
class="mx_Flex mx_EmptyRoomList_DefaultPlaceholder"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
/>
</svg>
New message
</button>
</div>
</div>
</section>
@@ -322,34 +333,66 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
</li>
</ul>
<div
class="mx_RoomList"
data-testid="room-list"
class="mx_Flex mx_EmptyRoomList_GenericPlaceholder"
data-testid="empty-room-list"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<div
style="overflow: visible; height: 0px; width: 0px;"
<span
class="mx_EmptyRoomList_GenericPlaceholder_title"
>
<div
aria-label="Room list"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
No chats yet
</span>
<span
class="mx_EmptyRoomList_GenericPlaceholder_description"
>
Get started by messaging someone or by creating a room
</span>
<div
class="mx_Flex mx_EmptyRoomList_DefaultPlaceholder"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12q-1.65 0-2.825-1.175T6 8t1.175-2.825T10 4t2.825 1.175T14 8t-1.175 2.825T10 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 10 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q18 16.35 18 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 16 20H4q-.824 0-1.412-.587A1.93 1.93 0 0 1 2 18m2 0h12v-.8a.97.97 0 0 0-.5-.85q-1.35-.675-2.725-1.012a11.6 11.6 0 0 0-5.55 0Q5.85 15.675 4.5 16.35a.97.97 0 0 0-.5.85zm6-8q.825 0 1.412-.588Q12 8.826 12 8q0-.824-.588-1.412A1.93 1.93 0 0 0 10 6q-.825 0-1.412.588A1.93 1.93 0 0 0 8 8q0 .825.588 1.412Q9.175 10 10 10m7 1h2v2q0 .424.288.713.287.287.712.287.424 0 .712-.287A.97.97 0 0 0 21 13v-2h2q.424 0 .712-.287A.97.97 0 0 0 24 10a.97.97 0 0 0-.288-.713A.97.97 0 0 0 23 9h-2V7a.97.97 0 0 0-.288-.713A.97.97 0 0 0 20 6a.97.97 0 0 0-.712.287A.97.97 0 0 0 19 7v2h-2a.97.97 0 0 0-.712.287A.97.97 0 0 0 16 10q0 .424.288.713.287.287.712.287"
/>
</svg>
New message
</button>
<button
class="_button_vczzf_8 _has-icon_vczzf_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m8.566 17-.944 4.094q-.086.406-.372.656t-.687.25q-.543 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.801-3.5H3.158q-.572 0-.916-.484a1.27 1.27 0 0 1-.2-1.078 1.12 1.12 0 0 1 1.116-.938H6.85l1.145-5h-3.12q-.57 0-.915-.484a1.27 1.27 0 0 1-.2-1.078A1.12 1.12 0 0 1 4.875 7h3.691l.945-4.094q.085-.406.372-.656.286-.25.686-.25.544 0 .887.469.345.468.2 1.031l-.8 3.5h4.578l.944-4.094q.085-.406.372-.656.286-.25.687-.25.543 0 .887.469t.2 1.031L17.723 7h3.119q.573 0 .916.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937H17.15l-1.145 5h3.12q.57 0 .915.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937h-3.691l-.944 4.094q-.087.406-.373.656t-.686.25q-.544 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.8-3.5zm.573-2.5h4.578l1.144-5h-4.578z"
/>
</svg>
New room
</button>
</div>
</div>
</section>