Add secondary filters to the new room list (#29818)

* Secondary filters

* Update snapshots

* Fix imports

* Update screenshots

* Add unit test

* Imports

* Prettier

* Add playwright test
This commit is contained in:
David Baker
2025-04-30 10:12:56 +01:00
committed by GitHub
parent cd05838bf6
commit 4f4f391959
15 changed files with 616 additions and 13 deletions

View File

@@ -0,0 +1,121 @@
/*
* 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>
);
}

View File

@@ -11,6 +11,8 @@ import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListVie
import { Flex } from "../../../utils/Flex";
import { _t } from "../../../../languageHandler";
import { RoomListOptionsMenu } from "./RoomListOptionsMenu";
import { RoomListFilterMenu } from "./RoomListFilterMenu";
import { textForSecondaryFilter } from "./textForFilter";
interface Props {
/**
@@ -23,13 +25,17 @@ interface Props {
* 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="8px"
gap="4px"
>
<RoomListFilterMenu vm={vm} />
{activeFilterText}
<RoomListOptionsMenu vm={vm} />
</Flex>
);

View File

@@ -0,0 +1,29 @@
/*
* 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");
}
}

View File

@@ -2121,6 +2121,7 @@
"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",
"people": "People",
@@ -2154,6 +2155,12 @@
"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",