diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 59f5a2fcab..bb87f90f19 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -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`); + }, + ); + }); }); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png new file mode 100644 index 0000000000..8cf4a3c97b Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Favourite-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png new file mode 100644 index 0000000000..3f700037cb Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/People-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png new file mode 100644 index 0000000000..e01564eeb4 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/Rooms-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png new file mode 100644 index 0000000000..45d2a775ea Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/default-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png new file mode 100644 index 0000000000..a84f3ac260 Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png new file mode 100644 index 0000000000..94b09ac14f Binary files /dev/null and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png index d2934c2a76..49cf6ef08a 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png differ diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 1b4dc79296..a3284fb379 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -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"; diff --git a/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss b/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss new file mode 100644 index 0000000000..a0fbfdaea7 --- /dev/null +++ b/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss @@ -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%; + } +} diff --git a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx index 9aa63451f3..8a1fdb1fe7 100644 --- a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx @@ -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, diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index 6c46ca6d38..2143aeae78 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -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( + 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, diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx index a21918e5fa..68f8e3e380 100644 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -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 = 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 }; } diff --git a/src/components/viewmodels/roomlist/utils.ts b/src/components/viewmodels/roomlist/utils.ts index 3886d0e3b0..6220c3b961 100644 --- a/src/components/viewmodels/roomlist/utils.ts +++ b/src/components/viewmodels/roomlist/utils.ts @@ -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 { + 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()), + ); +} diff --git a/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx b/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx new file mode 100644 index 0000000000..c4824e2b45 --- /dev/null +++ b/src/components/views/rooms/RoomListPanel/EmptyRoomList.tsx @@ -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 ; + + switch (vm.activePrimaryFilter.key) { + case FilterKey.FavouriteFilter: + return ( + + ); + case FilterKey.PeopleFilter: + return ( + + ); + case FilterKey.RoomsFilter: + return ( + + ); + case FilterKey.UnreadFilter: + return ; + 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): JSX.Element { + return ( + + {title} + {description && {description}} + {children} + + ); +} + +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 ( + + + + {vm.canCreateRoom && ( + + )} + + + ); +} + +interface UnreadPlaceholderProps { + filter: PrimaryFilter; +} + +/** + * The empty state for the room list when the unread filter is active + */ +function UnreadPlaceholder({ filter }: UnreadPlaceholderProps): JSX.Element { + return ( + + + + ); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index 36fab95658..f4800f7009 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -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 ( <> - + {isRoomListEmpty ? : } ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 34e0e98be3..aa71cac8a7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 don’t have direct chats with anyone yet", + "no_people_description": "You can deselect filters in order to see your other chats", + "no_rooms": "You’re not in any room yet", + "no_rooms_description": "You can deselect filters in order to see your other chats", + "no_unread": "Congrats! You don’t 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", diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx index 2233949d71..399943b6cf 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx @@ -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", () => { diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 43364e0d77..9cfb83a766 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -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); + }); + }); }); diff --git a/test/unit-tests/components/viewmodels/roomlist/utils-test.ts b/test/unit-tests/components/viewmodels/roomlist/utils-test.ts new file mode 100644 index 0000000000..1fd4fc1b4a --- /dev/null +++ b/test/unit-tests/components/viewmodels/roomlist/utils-test.ts @@ -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); + }); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx new file mode 100644 index 0000000000..5c41fb367c --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx @@ -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("", () => { + 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(); + 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(); + 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(); + 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(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx index 5e2d451ff8..3490a3c509 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx @@ -37,6 +37,9 @@ describe("", () => { activeSortOption: SortOption.Activity, shouldShowMessagePreview: false, toggleMessagePreview: jest.fn(), + createRoom: jest.fn(), + createChatRoom: jest.fn(), + canCreateRoom: true, }; // Needed to render a room list cell diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx index f4b97b84b6..301f293835 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListPrimaryFilters-test.tsx @@ -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("", () => { let vm: RoomListViewState; @@ -20,9 +21,12 @@ describe("", () => { 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, diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx new file mode 100644 index 0000000000..015fe5404d --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx @@ -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("", () => { + 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(); + expect(screen.getByText("No chats yet")).toBeInTheDocument(); + }); + + it("should render a room list", () => { + mocked(useRoomListViewModel).mockReturnValue({ + ...defaultValue, + rooms: [mkRoom(matrixClient, "testing room")], + }); + + render(); + expect(screen.getByRole("grid", { name: "Room list" })).toBeInTheDocument(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap new file mode 100644 index 0000000000..bf1733c2ed --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/EmptyRoomList-test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should display empty state for filter favourite 1`] = ` + +
+ + You don't have favourite chat yet + + + You can add a chat to your favourites in the chat settings + +
+
+`; + +exports[` should display empty state for filter people 1`] = ` + +
+ + You don’t have direct chats with anyone yet + + + You can deselect filters in order to see your other chats + +
+
+`; + +exports[` should display empty state for filter rooms 1`] = ` + +
+ + You’re not in any room yet + + + You can deselect filters in order to see your other chats + +
+
+`; + +exports[` should display the empty state for the unread filter 1`] = ` + +
+ + Congrats! You don’t have any unread messages + + +
+
+`; + +exports[` should not render the new room button if the user doesn't have the rights to create a room 1`] = ` + +
+ + No chats yet + + + Get started by messaging someone + +
+ +
+
+
+`; + +exports[` should render the default placeholder when there is no filter 1`] = ` + +
+ + No chats yet + + + Get started by messaging someone or by creating a room + +
+ + +
+
+
+`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap index 28f3befc20..df3f60dcf4 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap @@ -113,34 +113,45 @@ exports[` should not render the RoomListSearch component when U
-
-
+ + Get started by messaging someone + +
+
-
-
-
-
-
+ + New message +
@@ -322,34 +333,66 @@ exports[` should render the RoomListSearch component when UICom
-
-
+ + Get started by messaging someone or by creating a room + +
+
-
-
-
-
-
+ + New message + +