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:
121
src/components/views/rooms/RoomListPanel/RoomListFilterMenu.tsx
Normal file
121
src/components/views/rooms/RoomListPanel/RoomListFilterMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
29
src/components/views/rooms/RoomListPanel/textForFilter.ts
Normal file
29
src/components/views/rooms/RoomListPanel/textForFilter.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user