New room list: move secondary filters into primary filters (#29972)
* feat: move secondary filters into primary filters in vm * test: update room list view model tests * feat: remove secondary filter menu * test: update and remove secondary filter component tests * feat: update i18n * test: update remaining tests * test(e2e): update screenshots and tests * feat: add new cases for empty room list * test(e2e): add more tests for empty room list for new primary filters
@@ -25,10 +25,6 @@ test.describe("Room list filters and sort", () => {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
function getSecondaryFilters(page: Page): Locator {
|
||||
return page.getByRole("button", { name: "Filter" });
|
||||
}
|
||||
|
||||
function getRoomOptionsMenu(page: Page): Locator {
|
||||
return page.getByRole("button", { name: "Room Options" });
|
||||
}
|
||||
@@ -181,6 +177,33 @@ test.describe("Room list filters and sort", () => {
|
||||
await app.client.evaluate(async (client, id) => {
|
||||
await client.setRoomTag(id, "m.lowpriority", { order: 0.5 });
|
||||
}, lowPrioId);
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
|
||||
const mentionRoomId = await app.client.createRoom({ name: "room with mention" });
|
||||
await app.client.inviteUser(mentionRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(mentionRoomId);
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { mentionRoomId, userId }) => {
|
||||
await client.sendMessage(mentionRoomId, {
|
||||
// @ts-ignore ignore usage of MsgType.text
|
||||
"msgtype": "m.text",
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: [userId],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ mentionRoomId, userId: user.userId },
|
||||
);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
@@ -197,7 +220,7 @@ test.describe("Room list filters and sort", () => {
|
||||
// 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);
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(4);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
@@ -206,24 +229,23 @@ test.describe("Room list filters and sort", () => {
|
||||
|
||||
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 expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
|
||||
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(4);
|
||||
});
|
||||
|
||||
test("should filter the list (with secondary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const secondaryFilters = getSecondaryFilters(page);
|
||||
await secondaryFilters.click();
|
||||
|
||||
await expect(page.getByRole("menu", { name: "Filter" })).toMatchScreenshot("filter-menu.png");
|
||||
|
||||
await page.getByRole("menuitem", { name: "Low priority" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(5);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
});
|
||||
|
||||
@@ -294,15 +316,25 @@ test.describe("Room list filters and sort", () => {
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
[
|
||||
{ filter: "Unreads", action: "Show all chats" },
|
||||
{ filter: "Mentions", action: "See all activity" },
|
||||
{ filter: "Invites", action: "See all activity" },
|
||||
].forEach(({ filter, action }) => {
|
||||
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("unread-empty-room-list.png");
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||
|
||||
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
|
||||
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
|
||||
await emptyRoomList.getByRole("button", { name: action }).click();
|
||||
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -8,7 +8,7 @@ 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 PrimaryFilter, useFilteredRooms } from "./useFilteredRooms";
|
||||
import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces";
|
||||
@@ -59,16 +59,6 @@ export interface RoomListViewState {
|
||||
*/
|
||||
activePrimaryFilter?: PrimaryFilter;
|
||||
|
||||
/**
|
||||
* A function to activate a given secondary filter.
|
||||
*/
|
||||
activateSecondaryFilter: (filter: SecondaryFilters) => void;
|
||||
|
||||
/**
|
||||
* The currently active secondary filter.
|
||||
*/
|
||||
activeSecondaryFilter: SecondaryFilters;
|
||||
|
||||
/**
|
||||
* The index of the active room in the room list.
|
||||
*/
|
||||
@@ -81,14 +71,7 @@ export interface RoomListViewState {
|
||||
*/
|
||||
export function useRoomListViewModel(): RoomListViewState {
|
||||
const matrixClient = useMatrixClientContext();
|
||||
const {
|
||||
isLoadingRooms,
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
rooms: filteredRooms,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
} = useFilteredRooms();
|
||||
const { isLoadingRooms, primaryFilters, activePrimaryFilter, rooms: filteredRooms } = useFilteredRooms();
|
||||
const { activeIndex, rooms } = useStickyRoomList(filteredRooms);
|
||||
|
||||
useRoomListNavigation(rooms);
|
||||
@@ -111,8 +94,6 @@ export function useRoomListViewModel(): RoomListViewState {
|
||||
createChatRoom,
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
activeIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ interface FilteredRooms {
|
||||
primaryFilters: PrimaryFilter[];
|
||||
isLoadingRooms: boolean;
|
||||
rooms: Room[];
|
||||
activateSecondaryFilter: (filter: SecondaryFilters) => void;
|
||||
activeSecondaryFilter: SecondaryFilters;
|
||||
/**
|
||||
* The currently active primary filter.
|
||||
* If no primary filter is active, this will be undefined.
|
||||
@@ -49,51 +47,11 @@ const filterKeyToNameMap: Map<FilterKey, TranslationKey> = new Map([
|
||||
[FilterKey.UnreadFilter, _td("room_list|filters|unread")],
|
||||
[FilterKey.PeopleFilter, _td("room_list|filters|people")],
|
||||
[FilterKey.RoomsFilter, _td("room_list|filters|rooms")],
|
||||
[FilterKey.MentionsFilter, _td("room_list|filters|mentions")],
|
||||
[FilterKey.InvitesFilter, _td("room_list|filters|invites")],
|
||||
[FilterKey.FavouriteFilter, _td("room_list|filters|favourite")],
|
||||
]);
|
||||
|
||||
/**
|
||||
* These are the secondary filters which are not prominently shown
|
||||
* in the UI.
|
||||
*/
|
||||
export const enum SecondaryFilters {
|
||||
AllActivity,
|
||||
MentionsOnly,
|
||||
InvitesOnly,
|
||||
LowPriority,
|
||||
}
|
||||
|
||||
/**
|
||||
* A map from {@link SecondaryFilters} which the UI understands to
|
||||
* {@link FilterKey} which the store understands.
|
||||
*/
|
||||
const secondaryFiltersToFilterKeyMap = new Map([
|
||||
[SecondaryFilters.AllActivity, undefined],
|
||||
[SecondaryFilters.MentionsOnly, FilterKey.MentionsFilter],
|
||||
[SecondaryFilters.InvitesOnly, FilterKey.InvitesFilter],
|
||||
[SecondaryFilters.LowPriority, FilterKey.LowPriorityFilter],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Use this function to determine if a given primary filter is compatible with
|
||||
* a given secondary filter. Practically, this determines whether it makes sense
|
||||
* to expose two filters together in the UI - for eg, it does not make sense to show the
|
||||
* favourite primary filter if the active secondary filter is low priority.
|
||||
* @param primary Primary filter key
|
||||
* @param secondary Secondary filter key
|
||||
* @returns true if compatible, false otherwise
|
||||
*/
|
||||
function isPrimaryFilterCompatible(primary: FilterKey, secondary: FilterKey): boolean {
|
||||
if (secondary === FilterKey.MentionsFilter) {
|
||||
if (primary === FilterKey.UnreadFilter) return false;
|
||||
} else if (secondary === FilterKey.InvitesFilter) {
|
||||
if (primary === FilterKey.UnreadFilter || primary === FilterKey.FavouriteFilter) return false;
|
||||
} else if (secondary === FilterKey.LowPriorityFilter) {
|
||||
if (primary === FilterKey.FavouriteFilter) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track available filters and provide a filtered list of rooms.
|
||||
*/
|
||||
@@ -103,16 +61,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
* rendered above the room list.
|
||||
*/
|
||||
const [primaryFilter, setPrimaryFilter] = useState<FilterKey | undefined>();
|
||||
/**
|
||||
* Secondary filters are also filters but they are hidden
|
||||
* away in a popup menu.
|
||||
*/
|
||||
const [activeSecondaryFilter, setActiveSecondaryFilter] = useState<SecondaryFilters>(SecondaryFilters.AllActivity);
|
||||
|
||||
const secondaryFilter = useMemo(
|
||||
() => secondaryFiltersToFilterKeyMap.get(activeSecondaryFilter),
|
||||
[activeSecondaryFilter],
|
||||
);
|
||||
|
||||
const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace());
|
||||
const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms);
|
||||
@@ -123,16 +71,13 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
}, []);
|
||||
|
||||
// Reset filters when active space changes
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
|
||||
setPrimaryFilter(undefined);
|
||||
activateSecondaryFilter(SecondaryFilters.AllActivity);
|
||||
});
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => setPrimaryFilter(undefined));
|
||||
|
||||
const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] =>
|
||||
array.filter((f) => f !== undefined) as FilterKey[];
|
||||
|
||||
const getAppliedFilters = (): FilterKey[] => {
|
||||
return filterUndefined([primaryFilter, secondaryFilter]);
|
||||
return filterUndefined([primaryFilter]);
|
||||
};
|
||||
|
||||
useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => {
|
||||
@@ -144,30 +89,6 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
setIsLoadingRooms(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Secondary filters are activated using this function.
|
||||
* This is different to how primary filters work because the secondary
|
||||
* filters are static i.e they are always available and don't need to be
|
||||
* hidden.
|
||||
*/
|
||||
const activateSecondaryFilter = useCallback(
|
||||
(filter: SecondaryFilters): void => {
|
||||
// If the filter is already active, just return.
|
||||
if (filter === activeSecondaryFilter) return;
|
||||
|
||||
// SecondaryFilter is an enum for the UI, let's convert it to something
|
||||
// that the store will understand.
|
||||
const secondary = secondaryFiltersToFilterKeyMap.get(filter);
|
||||
setActiveSecondaryFilter(filter);
|
||||
|
||||
// Reset any active primary filters.
|
||||
setPrimaryFilter(undefined);
|
||||
|
||||
updateRoomsFromStore(filterUndefined([secondary]));
|
||||
},
|
||||
[activeSecondaryFilter, updateRoomsFromStore],
|
||||
);
|
||||
|
||||
/**
|
||||
* This tells the view which primary filters are available, how to toggle them
|
||||
* and whether a given primary filter is active. @see {@link PrimaryFilter}
|
||||
@@ -178,7 +99,7 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
toggle: () => {
|
||||
setPrimaryFilter((currentFilter) => {
|
||||
const filter = currentFilter === key ? undefined : key;
|
||||
updateRoomsFromStore(filterUndefined([filter, secondaryFilter]));
|
||||
updateRoomsFromStore(filterUndefined([filter]));
|
||||
return filter;
|
||||
});
|
||||
},
|
||||
@@ -189,13 +110,10 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
};
|
||||
const filters: PrimaryFilter[] = [];
|
||||
for (const [key, name] of filterKeyToNameMap.entries()) {
|
||||
if (secondaryFilter && !isPrimaryFilterCompatible(key, secondaryFilter)) {
|
||||
continue;
|
||||
}
|
||||
filters.push(createPrimaryFilter(key, _t(name)));
|
||||
}
|
||||
return filters;
|
||||
}, [primaryFilter, updateRoomsFromStore, secondaryFilter]);
|
||||
}, [primaryFilter, updateRoomsFromStore]);
|
||||
|
||||
const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]);
|
||||
|
||||
@@ -204,7 +122,5 @@ export function useFilteredRooms(): FilteredRooms {
|
||||
primaryFilters,
|
||||
activePrimaryFilter,
|
||||
rooms,
|
||||
activateSecondaryFilter,
|
||||
activeSecondaryFilter,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,29 @@ export function EmptyRoomList({ vm }: EmptyRoomListProps): JSX.Element | undefin
|
||||
/>
|
||||
);
|
||||
case FilterKey.UnreadFilter:
|
||||
return <UnreadPlaceholder filter={vm.activePrimaryFilter} />;
|
||||
return (
|
||||
<ActionPlaceholder
|
||||
title={_t("room_list|empty|no_unread")}
|
||||
action={_t("room_list|empty|show_chats")}
|
||||
filter={vm.activePrimaryFilter}
|
||||
/>
|
||||
);
|
||||
case FilterKey.InvitesFilter:
|
||||
return (
|
||||
<ActionPlaceholder
|
||||
title={_t("room_list|empty|no_invites")}
|
||||
action={_t("room_list|empty|show_activity")}
|
||||
filter={vm.activePrimaryFilter}
|
||||
/>
|
||||
);
|
||||
case FilterKey.MentionsFilter:
|
||||
return (
|
||||
<ActionPlaceholder
|
||||
title={_t("room_list|empty|no_mentions")}
|
||||
action={_t("room_list|empty|show_activity")}
|
||||
filter={vm.activePrimaryFilter}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -131,18 +153,21 @@ function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface UnreadPlaceholderProps {
|
||||
interface ActionPlaceholderProps {
|
||||
filter: PrimaryFilter;
|
||||
title: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The empty state for the room list when the unread filter is active
|
||||
* A placeholder for the room list when a filter is active
|
||||
* The user can take action to toggle the filter
|
||||
*/
|
||||
function UnreadPlaceholder({ filter }: UnreadPlaceholderProps): JSX.Element {
|
||||
function ActionPlaceholder({ filter, title, action }: ActionPlaceholderProps): JSX.Element {
|
||||
return (
|
||||
<GenericPlaceholder title={_t("room_list|empty|no_unread")}>
|
||||
<GenericPlaceholder title={title}>
|
||||
<Button kind="tertiary" onClick={filter.toggle}>
|
||||
{_t("room_list|empty|show_chats")}
|
||||
{action}
|
||||
</Button>
|
||||
</GenericPlaceholder>
|
||||
);
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* 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 { IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import React, { type Ref, type JSX, useState } from "react";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ChatIcon,
|
||||
ChatNewIcon,
|
||||
CheckIcon,
|
||||
FilterIcon,
|
||||
MentionIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { SecondaryFilters } from "../../../viewmodels/roomlist/useFilteredRooms";
|
||||
import { textForSecondaryFilter } from "./textForFilter";
|
||||
|
||||
interface MenuTriggerProps extends React.ComponentProps<typeof IconButton> {
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const MenuTrigger = ({ ref, ...props }: MenuTriggerProps): JSX.Element => (
|
||||
<Tooltip label={_t("room_list|filter")}>
|
||||
<IconButton size="28px" aria-label={_t("room_list|filter")} {...props} ref={ref}>
|
||||
<FilterIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
interface FilterOptionProps {
|
||||
/**
|
||||
* The filter to display
|
||||
*/
|
||||
filter: SecondaryFilters;
|
||||
|
||||
/**
|
||||
* True if the filter is selected
|
||||
*/
|
||||
selected: boolean;
|
||||
|
||||
/**
|
||||
* The function to call when the filter is selected
|
||||
*/
|
||||
onSelect: (filter: SecondaryFilters) => void;
|
||||
}
|
||||
|
||||
function iconForFilter(filter: SecondaryFilters, size: string): JSX.Element {
|
||||
switch (filter) {
|
||||
case SecondaryFilters.AllActivity:
|
||||
return <ChatIcon width={size} height={size} />;
|
||||
case SecondaryFilters.MentionsOnly:
|
||||
return <MentionIcon width={size} height={size} />;
|
||||
case SecondaryFilters.InvitesOnly:
|
||||
return <ChatNewIcon width={size} height={size} />;
|
||||
case SecondaryFilters.LowPriority:
|
||||
return <ArrowDownIcon width={size} height={size} />;
|
||||
}
|
||||
}
|
||||
|
||||
function FilterOption({ filter, selected, onSelect }: FilterOptionProps): JSX.Element {
|
||||
const checkComponent = <CheckIcon width="24px" height="24px" color="var(--cpd-color-icon-primary)" />;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
aria-selected={selected}
|
||||
hideChevron={true}
|
||||
Icon={iconForFilter(filter, "20px")}
|
||||
label={textForSecondaryFilter(filter)}
|
||||
onSelect={() => {
|
||||
onSelect(filter);
|
||||
}}
|
||||
>
|
||||
{selected && checkComponent}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The view model for the room list view
|
||||
*/
|
||||
vm: RoomListViewState;
|
||||
}
|
||||
|
||||
export function RoomListFilterMenu({ vm }: Props): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={_t("room_list|filter")}
|
||||
showTitle={true}
|
||||
align="start"
|
||||
trigger={<MenuTrigger />}
|
||||
>
|
||||
{[
|
||||
SecondaryFilters.AllActivity,
|
||||
SecondaryFilters.MentionsOnly,
|
||||
SecondaryFilters.InvitesOnly,
|
||||
SecondaryFilters.LowPriority,
|
||||
].map((filter) => (
|
||||
<FilterOption
|
||||
key={filter}
|
||||
filter={filter}
|
||||
selected={vm.activeSecondaryFilter === filter}
|
||||
onSelect={(selectedFilter) => {
|
||||
vm.activateSecondaryFilter(selectedFilter);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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 } from "react";
|
||||
|
||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListFilterMenu } from "./RoomListFilterMenu";
|
||||
import { textForSecondaryFilter } from "./textForFilter";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The view model for the room list
|
||||
*/
|
||||
vm: RoomListViewState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The secondary filters for the room list (eg. mentions only / invites only).
|
||||
*/
|
||||
export function RoomListSecondaryFilters({ vm }: Props): JSX.Element {
|
||||
const activeFilterText = textForSecondaryFilter(vm.activeSecondaryFilter);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
aria-label={_t("room_list|secondary_filters")}
|
||||
className="mx_RoomListSecondaryFilters"
|
||||
align="center"
|
||||
gap="4px"
|
||||
>
|
||||
<RoomListFilterMenu vm={vm} />
|
||||
{activeFilterText}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewM
|
||||
import { RoomList } from "./RoomList";
|
||||
import { EmptyRoomList } from "./EmptyRoomList";
|
||||
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
import { RoomListSecondaryFilters } from "./RoomListSecondaryFilters";
|
||||
|
||||
/**
|
||||
* Host the room list and the (future) room filters
|
||||
@@ -30,7 +29,6 @@ export function RoomListView(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<RoomListPrimaryFilters vm={vm} />
|
||||
<RoomListSecondaryFilters vm={vm} />
|
||||
{listBody}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* 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 { _t } from "../../../../languageHandler";
|
||||
import { SecondaryFilters } from "../../../viewmodels/roomlist/useFilteredRooms";
|
||||
|
||||
/**
|
||||
* Gives the human readable text name for a secondary filter.
|
||||
* @param filter The filter in question
|
||||
* @returns The translated, human readable name for the filter
|
||||
*/
|
||||
export function textForSecondaryFilter(filter: SecondaryFilters): string {
|
||||
switch (filter) {
|
||||
case SecondaryFilters.AllActivity:
|
||||
return _t("room_list|secondary_filter|all_activity");
|
||||
case SecondaryFilters.MentionsOnly:
|
||||
return _t("room_list|secondary_filter|mentions_only");
|
||||
case SecondaryFilters.InvitesOnly:
|
||||
return _t("room_list|secondary_filter|invites_only");
|
||||
case SecondaryFilters.LowPriority:
|
||||
return _t("room_list|secondary_filter|low_priority");
|
||||
default:
|
||||
throw new Error("Unknown filter");
|
||||
}
|
||||
}
|
||||
@@ -2112,19 +2112,23 @@
|
||||
"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_invites": "You don't have any unread invites",
|
||||
"no_mentions": "You don't have any unread mentions",
|
||||
"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_activity": "See all activity",
|
||||
"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",
|
||||
"filter": "Filter",
|
||||
"filters": {
|
||||
"favourite": "Favourites",
|
||||
"invites": "Invites",
|
||||
"mentions": "Mentions",
|
||||
"people": "People",
|
||||
"rooms": "Rooms",
|
||||
"unread": "Unreads"
|
||||
@@ -2156,13 +2160,6 @@
|
||||
"open_room": "Open room %(roomName)s"
|
||||
},
|
||||
"room_options": "Room Options",
|
||||
"secondary_filter": {
|
||||
"all_activity": "All activity",
|
||||
"invites_only": "Invites only",
|
||||
"low_priority": "Low priority",
|
||||
"mentions_only": "Mentions only"
|
||||
},
|
||||
"secondary_filters": "Secondary filters",
|
||||
"show_less": "Show less",
|
||||
"show_message_previews": "Show message previews",
|
||||
"show_n_more": {
|
||||
|
||||
@@ -13,7 +13,6 @@ import RoomListStoreV3, { LISTS_UPDATE_EVENT } from "../../../../../src/stores/r
|
||||
import { mkStubRoom } from "../../../../test-utils";
|
||||
import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { hasCreateRoomRights, createRoom } from "../../../../../src/components/viewmodels/roomlist/utils";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
@@ -66,10 +65,10 @@ describe("RoomListViewModel", () => {
|
||||
it("should provide list of available filters", () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
// should have 4 filters
|
||||
expect(vm.current.primaryFilters).toHaveLength(4);
|
||||
// should have 6 filters
|
||||
expect(vm.current.primaryFilters).toHaveLength(6);
|
||||
// check the order
|
||||
for (const [i, name] of ["Unreads", "People", "Rooms", "Favourites"].entries()) {
|
||||
for (const [i, name] of ["Unreads", "People", "Rooms", "Mentions", "Invites", "Favourites"].entries()) {
|
||||
expect(vm.current.primaryFilters[i].name).toEqual(name);
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(false);
|
||||
}
|
||||
@@ -107,46 +106,6 @@ describe("RoomListViewModel", () => {
|
||||
expect(vm.current.primaryFilters[j].active).toEqual(true);
|
||||
});
|
||||
|
||||
it("should select all activity as default secondary filter", () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// By default, all activity should be the active secondary filter
|
||||
expect(vm.current.activeSecondaryFilter).toEqual(SecondaryFilters.AllActivity);
|
||||
});
|
||||
|
||||
it("should be able to filter using secondary filters", () => {
|
||||
const { fn } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// Let's say we toggle the mentions secondary filter
|
||||
act(() => {
|
||||
vm.current.activateSecondaryFilter(SecondaryFilters.MentionsOnly);
|
||||
});
|
||||
expect(fn).toHaveBeenCalledWith([FilterKey.MentionsFilter]);
|
||||
});
|
||||
|
||||
it("primary filters are applied on top of secondary filers", () => {
|
||||
const { fn } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// Let's say we toggle the mentions secondary filter
|
||||
act(() => {
|
||||
vm.current.activateSecondaryFilter(SecondaryFilters.MentionsOnly);
|
||||
});
|
||||
|
||||
// Let's say we toggle the People filter
|
||||
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
|
||||
act(() => {
|
||||
vm.current.primaryFilters[i].toggle();
|
||||
});
|
||||
|
||||
// RLS call must include both these filters
|
||||
expect(fn).toHaveBeenLastCalledWith(
|
||||
expect.arrayContaining([FilterKey.PeopleFilter, FilterKey.MentionsFilter]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the current active primary filter", async () => {
|
||||
// Let's say that the user's preferred sorting is alphabetic
|
||||
mockAndCreateRooms();
|
||||
@@ -160,29 +119,6 @@ describe("RoomListViewModel", () => {
|
||||
expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]);
|
||||
});
|
||||
|
||||
it("should remove any active primary filters when secondary filter is changed", async () => {
|
||||
const { fn } = mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// Let's first toggle the People filter
|
||||
const i = vm.current.primaryFilters.findIndex((f) => f.name === "People");
|
||||
act(() => {
|
||||
vm.current.primaryFilters[i].toggle();
|
||||
});
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(true);
|
||||
|
||||
// Let's say we toggle the mentions secondary filter
|
||||
act(() => {
|
||||
vm.current.activateSecondaryFilter(SecondaryFilters.MentionsOnly);
|
||||
});
|
||||
|
||||
// Primary filer should have been unapplied
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(false);
|
||||
|
||||
// RLS call must include only the secondary filter
|
||||
expect(fn).toHaveBeenLastCalledWith(expect.arrayContaining([FilterKey.MentionsFilter]));
|
||||
});
|
||||
|
||||
it("should remove all filters when active space is changed", async () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
@@ -194,58 +130,11 @@ describe("RoomListViewModel", () => {
|
||||
});
|
||||
expect(vm.current.primaryFilters[i].active).toEqual(true);
|
||||
|
||||
// Let's say we toggle the mentions secondary filter
|
||||
act(() => {
|
||||
vm.current.activateSecondaryFilter(SecondaryFilters.MentionsOnly);
|
||||
});
|
||||
expect(vm.current.activeSecondaryFilter).toEqual(SecondaryFilters.MentionsOnly);
|
||||
|
||||
// Simulate a space change
|
||||
await act(() => SpaceStore.instance.emit(UPDATE_SELECTED_SPACE));
|
||||
|
||||
// Primary filer should have been unapplied
|
||||
expect(vm.current.activePrimaryFilter).toEqual(undefined);
|
||||
|
||||
// Secondary filter should be reset to "All Activity"
|
||||
expect(vm.current.activeSecondaryFilter).toEqual(SecondaryFilters.AllActivity);
|
||||
});
|
||||
|
||||
const testcases: Array<[string, { secondary: SecondaryFilters; filterKey: FilterKey }, string]> = [
|
||||
[
|
||||
"Mentions only",
|
||||
{ secondary: SecondaryFilters.MentionsOnly, filterKey: FilterKey.MentionsFilter },
|
||||
"Unreads",
|
||||
],
|
||||
[
|
||||
"Invites only",
|
||||
{ secondary: SecondaryFilters.InvitesOnly, filterKey: FilterKey.InvitesFilter },
|
||||
"Unreads",
|
||||
],
|
||||
[
|
||||
"Invites only",
|
||||
{ secondary: SecondaryFilters.InvitesOnly, filterKey: FilterKey.InvitesFilter },
|
||||
"Favourites",
|
||||
],
|
||||
[
|
||||
"Low priority",
|
||||
{ secondary: SecondaryFilters.LowPriority, filterKey: FilterKey.LowPriorityFilter },
|
||||
"Favourites",
|
||||
],
|
||||
];
|
||||
|
||||
describe.each(testcases)("For secondary filter: %s", (secondaryFilterName, secondary, primaryFilterName) => {
|
||||
it(`should hide incompatible primary filter: ${primaryFilterName}`, () => {
|
||||
mockAndCreateRooms();
|
||||
const { result: vm } = renderHook(() => useRoomListViewModel());
|
||||
|
||||
// Apply the secondary filter
|
||||
act(() => {
|
||||
vm.current.activateSecondaryFilter(secondary.secondary);
|
||||
});
|
||||
|
||||
// Incompatible primary filter must be hidden
|
||||
expect(vm.current.primaryFilters.find((f) => f.name === primaryFilterName)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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 { EmptyRoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/EmptyRoomList";
|
||||
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
|
||||
@@ -22,8 +21,6 @@ describe("<EmptyRoomList />", () => {
|
||||
isLoadingRooms: false,
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
activateSecondaryFilter: jest.fn().mockReturnValue({}),
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
@@ -53,13 +50,17 @@ describe("<EmptyRoomList />", () => {
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should display the empty state for the unread filter", async () => {
|
||||
it.each([
|
||||
{ key: FilterKey.UnreadFilter, name: "unread", action: "Show all chats" },
|
||||
{ key: FilterKey.MentionsFilter, name: "mention", action: "See all activity" },
|
||||
{ key: FilterKey.InvitesFilter, name: "invite", action: "See all activity" },
|
||||
])("should display the empty state for the $name filter", async ({ key, name, action }) => {
|
||||
const user = userEvent.setup();
|
||||
const activePrimaryFilter = {
|
||||
toggle: jest.fn(),
|
||||
active: true,
|
||||
name: "unread",
|
||||
key: FilterKey.UnreadFilter,
|
||||
name,
|
||||
key,
|
||||
};
|
||||
const newState = {
|
||||
...vm,
|
||||
@@ -67,7 +68,7 @@ describe("<EmptyRoomList />", () => {
|
||||
};
|
||||
|
||||
const { asFragment } = render(<EmptyRoomList vm={newState} />);
|
||||
await user.click(screen.getByRole("button", { name: "Show all chats" }));
|
||||
await user.click(screen.getByRole("button", { name: action }));
|
||||
expect(activePrimaryFilter.toggle).toHaveBeenCalled();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
|
||||
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
|
||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
|
||||
|
||||
describe("<RoomList />", () => {
|
||||
@@ -33,8 +32,6 @@ describe("<RoomList />", () => {
|
||||
isLoadingRooms: false,
|
||||
rooms,
|
||||
primaryFilters: [],
|
||||
activateSecondaryFilter: () => {},
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* 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, type RenderOptions, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { RoomListFilterMenu } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListFilterMenu";
|
||||
|
||||
function getRenderOptions(): RenderOptions {
|
||||
return {
|
||||
wrapper: ({ children }) => <TooltipProvider>{children}</TooltipProvider>,
|
||||
};
|
||||
}
|
||||
|
||||
describe("<RoomListFilterMenu />", () => {
|
||||
let vm: RoomListViewState;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = {
|
||||
isLoadingRooms: false,
|
||||
rooms: [],
|
||||
canCreateRoom: true,
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
primaryFilters: [],
|
||||
activateSecondaryFilter: () => {},
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
activeIndex: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
it("should render room list filter menu button", async () => {
|
||||
const { asFragment } = render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
expect(screen.getByRole("button", { name: "Filter" })).toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens the menu on click", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
expect(screen.getByRole("menu", { name: "Filter" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'All activity' checked if selected", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
|
||||
const shouldBeSelected = screen.getByRole("menuitem", { name: "All activity" });
|
||||
expect(shouldBeSelected).toHaveAttribute("aria-selected", "true");
|
||||
expect(shouldBeSelected).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows 'Invites only' checked if selected", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
vm.activeSecondaryFilter = SecondaryFilters.InvitesOnly;
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
|
||||
const shouldBeSelected = screen.getByRole("menuitem", { name: "Invites only" });
|
||||
expect(shouldBeSelected).toHaveAttribute("aria-selected", "true");
|
||||
expect(shouldBeSelected).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows 'Low priority' checked if selected", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
vm.activeSecondaryFilter = SecondaryFilters.LowPriority;
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
|
||||
const shouldBeSelected = screen.getByRole("menuitem", { name: "Low priority" });
|
||||
expect(shouldBeSelected).toHaveAttribute("aria-selected", "true");
|
||||
expect(shouldBeSelected).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows 'Mentions only' checked if selected", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
vm.activeSecondaryFilter = SecondaryFilters.MentionsOnly;
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
|
||||
const shouldBeSelected = screen.getByRole("menuitem", { name: "Mentions only" });
|
||||
expect(shouldBeSelected).toHaveAttribute("aria-selected", "true");
|
||||
expect(shouldBeSelected).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("activates filter when item clicked", async () => {
|
||||
const userevent = userEvent.setup();
|
||||
|
||||
vm.activateSecondaryFilter = jest.fn();
|
||||
render(<RoomListFilterMenu vm={vm} />, getRenderOptions());
|
||||
await userevent.click(screen.getByRole("button", { name: "Filter" }));
|
||||
await userevent.click(screen.getByRole("menuitem", { name: "Invites only" }));
|
||||
|
||||
expect(vm.activateSecondaryFilter).toHaveBeenCalledWith(SecondaryFilters.InvitesOnly);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ 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 { RoomListPrimaryFilters } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListPrimaryFilters";
|
||||
import { FilterKey } from "../../../../../../src/stores/room-list-v3/skip-list/filters";
|
||||
|
||||
@@ -28,8 +27,6 @@ describe("<RoomListPrimaryFilters />", () => {
|
||||
{ name: "People", active: false, toggle: jest.fn(), key: FilterKey.PeopleFilter },
|
||||
{ name: "Rooms", active: true, toggle: jest.fn(), key: FilterKey.RoomsFilter },
|
||||
],
|
||||
activateSecondaryFilter: () => {},
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
activeIndex: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
type RoomListViewState,
|
||||
useRoomListViewModel,
|
||||
} from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
|
||||
import { SecondaryFilters } from "../../../../../../src/components/viewmodels/roomlist/useFilteredRooms";
|
||||
import { RoomListView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListView";
|
||||
import { mkRoom, stubClient } from "../../../../../test-utils";
|
||||
|
||||
@@ -26,8 +25,6 @@ describe("<RoomListView />", () => {
|
||||
isLoadingRooms: false,
|
||||
rooms: [],
|
||||
primaryFilters: [],
|
||||
activateSecondaryFilter: jest.fn().mockReturnValue({}),
|
||||
activeSecondaryFilter: SecondaryFilters.AllActivity,
|
||||
createRoom: jest.fn(),
|
||||
createChatRoom: jest.fn(),
|
||||
canCreateRoom: true,
|
||||
|
||||
@@ -63,6 +63,56 @@ exports[`<EmptyRoomList /> should display empty state for filter rooms 1`] = `
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the invite 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"
|
||||
>
|
||||
You don't have any unread invites
|
||||
</span>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
See all activity
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the mention 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"
|
||||
>
|
||||
You don't have any unread mentions
|
||||
</span>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
See all activity
|
||||
</button>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmptyRoomList /> should display the empty state for the unread filter 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomListFilterMenu /> should render room list filter menu button 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Filter"
|
||||
aria-labelledby="«r2»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-«r0»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 7a1 1 0 0 0 0 2h14a1 1 0 1 0 0-2zm3 4a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2zm2 5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<RoomListFilterMenu /> shows 'All activity' checked if selected 1`] = `
|
||||
<button
|
||||
aria-selected="true"
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1.5 21.25 1.45-4.95a10.2 10.2 0 0 1-.712-2.1A10.2 10.2 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22q-1.125 0-2.2-.238a10.2 10.2 0 0 1-2.1-.712L2.75 22.5a.94.94 0 0 1-1-.25.94.94 0 0 1-.25-1m2.45-1.2 3.2-.95a1 1 0 0 1 .275-.062q.15-.013.275-.013.225 0 .438.038.212.036.412.137a7.4 7.4 0 0 0 1.675.6Q11.1 20 12 20q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12q0 .9.2 1.775t.6 1.675q.176.325.188.688t-.088.712z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
All activity
|
||||
</span>
|
||||
<svg
|
||||
color="var(--cpd-color-icon-primary)"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<RoomListFilterMenu /> shows 'Invites only' checked if selected 1`] = `
|
||||
<button
|
||||
aria-selected="true"
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 6h-2a.97.97 0 0 1-.712-.287A.97.97 0 0 1 16 5q0-.424.288-.713A.97.97 0 0 1 17 4h2V2q0-.424.288-.712A.97.97 0 0 1 20 1q.424 0 .712.288Q21 1.575 21 2v2h2q.424 0 .712.287Q24 4.576 24 5t-.288.713A.97.97 0 0 1 23 6h-2v2q0 .424-.288.713A.97.97 0 0 1 20 9a.97.97 0 0 1-.712-.287A.97.97 0 0 1 19 8z"
|
||||
/>
|
||||
<path
|
||||
d="M22 17v-6.341A6 6 0 0 1 20 11v6H6a2 2 0 0 0-1.414.586L4 18.172V5h10c0-.701.12-1.374.341-2H4a2 2 0 0 0-2 2v15.586c0 .89 1.077 1.337 1.707.707L6 19h14a2 2 0 0 0 2-2"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Invites only
|
||||
</span>
|
||||
<svg
|
||||
color="var(--cpd-color-icon-primary)"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<RoomListFilterMenu /> shows 'Low priority' checked if selected 1`] = `
|
||||
<button
|
||||
aria-selected="true"
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 4.5a1 1 0 0 1 1 1v10.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6a1 1 0 0 1-1.414 0l-6-6a1 1 0 1 1 1.414-1.414L11 16.086V5.5a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Low priority
|
||||
</span>
|
||||
<svg
|
||||
color="var(--cpd-color-icon-primary)"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<RoomListFilterMenu /> shows 'Mentions only' checked if selected 1`] = `
|
||||
<button
|
||||
aria-selected="true"
|
||||
class="_item_dyt4i_8 _interactive_dyt4i_26"
|
||||
data-kind="primary"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
class="_icon_dyt4i_50"
|
||||
fill="currentColor"
|
||||
height="20px"
|
||||
viewBox="0 0 24 24"
|
||||
width="20px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 4a8 8 0 1 0 0 16 1 1 0 1 1 0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10v1.5a3.5 3.5 0 0 1-6.396 1.966A5 5 0 1 1 17 12v1.5a1.5 1.5 0 0 0 3 0V12a8 8 0 0 0-8-8m3 8a3 3 0 1 0-6 0 3 3 0 0 0 6 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_dyt4i_34"
|
||||
>
|
||||
Mentions only
|
||||
</span>
|
||||
<svg
|
||||
color="var(--cpd-color-icon-primary)"
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
@@ -32,10 +32,10 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Room Options"
|
||||
aria-labelledby="«rk»"
|
||||
aria-labelledby="«rc»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-«ri»"
|
||||
id="radix-«ra»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
@@ -136,6 +136,32 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
|
||||
Rooms
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Mentions
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Invites
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
@@ -150,44 +176,6 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
aria-label="Secondary filters"
|
||||
class="mx_Flex mx_RoomListSecondaryFilters"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 4px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Filter"
|
||||
aria-labelledby="«rr»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-«rp»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 7a1 1 0 0 0 0 2h14a1 1 0 1 0 0-2zm3 4a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2zm2 5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
All activity
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListSkeleton"
|
||||
/>
|
||||
@@ -397,6 +385,32 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
Rooms
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Mentions
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="_chat-filter_5qdp0_8"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Invites
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
role="option"
|
||||
@@ -411,44 +425,6 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
aria-label="Secondary filters"
|
||||
class="mx_Flex mx_RoomListSecondaryFilters"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 4px; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Filter"
|
||||
aria-labelledby="«rb»"
|
||||
class="_icon-button_m2erp_8"
|
||||
data-state="closed"
|
||||
id="radix-«r9»"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 7a1 1 0 0 0 0 2h14a1 1 0 1 0 0-2zm3 4a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2zm2 5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
All activity
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomListSkeleton"
|
||||
/>
|
||||
|
||||