Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@
@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSectionHeaderView.pcss";
@import "./views/rooms/RoomListPanel/_RoomListSkeleton.pcss";
@import "./views/rooms/_AppsDrawer.pcss";
@import "./views/rooms/_Autocomplete.pcss";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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_RoomListSectionHeaderView {
height: 48px;
width: 100%;
padding-left: var(--cpd-space-3x);
display: flex;
align-items: end;

> h4 {
margin-top: 0;
margin-bottom: 4px;
font: var(--cpd-font-body-sm-semibold);
color: var(--cpd-color-text-action-accent);
}
}
9 changes: 5 additions & 4 deletions src/components/utils/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
/** Reference to the DOM element containing the virtualized list */
const virtuosoDomRef = useRef<HTMLElement | Window>(null);
const firstFocusableItem = props.items.find(props.isItemFocusable);
/** Key of the item that should have tabIndex == 0 */
const [tabIndexKey, setTabIndexKey] = useState<string | undefined>(
props.items[0] ? getItemKey(props.items[0]) : undefined,
firstFocusableItem ? getItemKey(firstFocusableItem) : undefined,
);
/** Range of currently visible items in the viewport */
const [visibleRange, setVisibleRange] = useState<ListRange | undefined>(undefined);
Expand All @@ -113,10 +114,10 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex

// Ensure the tabIndexKey is set if there is none already or if the existing key is no longer displayed
useEffect(() => {
if (items.length && (!tabIndexKey || keyToIndexMap.get(tabIndexKey) === undefined)) {
setTabIndexKey(getItemKey(items[0]));
if (firstFocusableItem && (!tabIndexKey || keyToIndexMap.get(tabIndexKey) === undefined)) {
setTabIndexKey(getItemKey(firstFocusableItem));
}
}, [items, getItemKey, tabIndexKey, keyToIndexMap]);
}, [firstFocusableItem, getItemKey, tabIndexKey, keyToIndexMap]);

/**
* Scrolls to a specific item index and sets it as focused.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,15 @@ export interface RoomListHeaderViewState {
/**
* Change the sort order of the room-list.
*/
sort: (option: SortOption) => void;
sort: (option: SortOption, useSections: boolean) => void;
/**
* The currently active sort option.
*/
activeSortOption: SortOption;
/**
* Whether to group rooms into sections
*/
useSections: boolean;
}

/**
Expand All @@ -147,7 +151,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {

/* Actions */

const { activeSortOption, sort } = useSorter();
const { activeSortOption, useSections, sort } = useSorter();

const createChatRoom = useCallback((e: Event) => {
defaultDispatcher.fire(Action.CreateChat);
Expand Down Expand Up @@ -219,6 +223,7 @@ export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
openSpacePreferences,
openSpaceSettings,
activeSortOption,
useSections,
sort,
};
}
9 changes: 5 additions & 4 deletions src/components/viewmodels/roomlist/useRoomListNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@
* Please see LICENSE files in the repository root for full details.
*/

import { type Room } from "matrix-js-sdk/src/matrix";

import dispatcher from "../../../dispatcher/dispatcher";
import { useDispatcher } from "../../../hooks/useDispatcher";
import { Action } from "../../../dispatcher/actions";
import { type ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { isRoomListRoom, type RoomListEntry } from "../../../stores/room-list-v3/RoomListStoreV3.ts";

/**
* Hook to navigate the room list using keyboard shortcuts.
* It listens to the ViewRoomDelta action and updates the room list accordingly.
* @param rooms
* @param entries
*/
export function useRoomListNavigation(rooms: Room[]): void {
export function useRoomListNavigation(entries: RoomListEntry[]): void {
useDispatcher(dispatcher, (payload) => {
if (payload.action !== Action.ViewRoomDelta) return;
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
if (!roomId) return;

const rooms = entries.filter(isRoomListRoom);

const { delta, unread } = payload as ViewRoomDeltaPayload;
const filteredRooms = unread
? // Filter the rooms to only include unread ones and the active room
Expand Down
10 changes: 7 additions & 3 deletions src/components/viewmodels/roomlist/useSorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ const sortOptionToSortingAlgorithm = {
};

interface SortState {
sort: (option: SortOption) => void;
sort: (option: SortOption, useSections: boolean) => void;
activeSortOption: SortOption;
useSections: boolean;
}

/**
Expand All @@ -48,15 +49,18 @@ export function useSorter(): SortState {
const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() =>
SettingsStore.getValue("RoomList.preferredSorting"),
);
const [useSections, setUseSections] = useState(() => SettingsStore.getValue("RoomList.useSections"));

const sort = (option: SortOption): void => {
const sort = (option: SortOption, useSections: boolean): void => {
const sortingAlgorithm = sortOptionToSortingAlgorithm[option];
RoomListStoreV3.instance.resort(sortingAlgorithm);
RoomListStoreV3.instance.resort(sortingAlgorithm, useSections);
setActiveSortingAlgorithm(sortingAlgorithm);
setUseSections(useSections);
};

return {
sort,
activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!],
useSections,
};
}
15 changes: 7 additions & 8 deletions src/components/viewmodels/roomlist/useStickyRoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,21 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { useDispatcher } from "../../../hooks/useDispatcher";
import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Optional } from "matrix-events-sdk";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";
import { isRoomListRoom, type RoomListEntry, type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3";

function getIndexByRoomId(rooms: Room[], roomId: Optional<string>): number | undefined {
const index = rooms.findIndex((room) => room.roomId === roomId);
function getIndexByRoomId(rooms: RoomListEntry[], roomId: Optional<string>): number | undefined {
const index = rooms.findIndex((room) => isRoomListRoom(room) && room.roomId === roomId);
return index === -1 ? undefined : index;
}

function getRoomsWithStickyRoom(
rooms: Room[],
rooms: RoomListEntry[],
oldIndex: number | undefined,
newIndex: number | undefined,
isRoomChange: boolean,
): { newRooms: Room[]; newIndex: number | undefined } {
): { newRooms: RoomListEntry[]; newIndex: number | undefined } {
const updated = { newIndex, newRooms: rooms };
if (isRoomChange) {
/*
Expand Down Expand Up @@ -83,8 +82,8 @@ export interface StickyRoomListResult {
* - Provides a list of rooms such that the active room is sticky i.e the active room is kept
* in the same index even when the order of rooms in the list changes.
* - Provides the index of the active room.
* @param rooms list of rooms
* @see {@link StickyRoomListResult} details what this hook returns..
* @param roomsResult list of rooms
* @see {@link StickyRoomListResult} details what this hook returns.
*/
export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult {
const [listState, setListState] = useState<StickyRoomListResult>({
Expand Down
54 changes: 32 additions & 22 deletions src/components/views/rooms/RoomListPanel/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/

import React, { useCallback, useRef, useState, type JSX } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import React, { type JSX, useCallback, useRef, useState } from "react";
import { type ScrollIntoViewLocation } from "react-virtuoso";
import { isEqual } from "lodash";

Expand All @@ -18,6 +17,12 @@ import { type FilterKey } from "../../../../stores/room-list-v3/skip-list/filter
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
import {
isRoomListRoom,
isRoomListSectionHeader,
type RoomListEntry,
} from "../../../../stores/room-list-v3/RoomListStoreV3.ts";
import { RoomListSectionHeaderView } from "./RoomListSectionHeaderView.tsx";

interface RoomListProps {
/**
Expand All @@ -37,6 +42,11 @@ const ROOM_LIST_ITEM_HEIGHT = 48;
* We would likely need to simplify the item content to improve this case.
*/
const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT;

const getItemKey = (item: RoomListEntry): string => {
return isRoomListSectionHeader(item) ? item.key : item.roomId;
};

/**
* A virtualized list of rooms.
*/
Expand All @@ -48,38 +58,38 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
const getItemComponent = useCallback(
(
index: number,
item: Room,
item: RoomListEntry,
context: ListContext<{
spaceId: string;
filterKeys: FilterKey[] | undefined;
}>,
onFocus: (e: React.FocusEvent) => void,
): JSX.Element => {
const itemKey = item.roomId;
const itemKey = getItemKey(item);
const isRovingItem = itemKey === context.tabIndexKey;
const isFocused = isRovingItem && context.focused;
const isSelected = activeIndex === index;
return (
<RoomListItemView
room={item}
key={itemKey}
isSelected={isSelected}
isFocused={isFocused}
tabIndex={isRovingItem ? 0 : -1}
roomIndex={index}
roomCount={roomCount}
onFocus={onFocus}
listIsScrolling={isScrolling}
/>
);
if (isRoomListSectionHeader(item)) {
return <RoomListSectionHeaderView section={item} />;
} else {
return (
<RoomListItemView
room={item}
key={itemKey}
isSelected={isSelected}
isFocused={isFocused}
tabIndex={isRovingItem ? 0 : -1}
roomIndex={index}
roomCount={roomCount}
onFocus={onFocus}
listIsScrolling={isScrolling}
/>
);
}
},
[activeIndex, roomCount, isScrolling],
);

const getItemKey = useCallback((item: Room): string => {
return item.roomId;
}, []);

const scrollIntoViewOnChange = useCallback(
(params: {
context: ListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>;
Expand Down Expand Up @@ -127,7 +137,7 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
items={roomsResult.rooms}
getItemComponent={getItemComponent}
getItemKey={getItemKey}
isItemFocusable={() => true}
isItemFocusable={isRoomListRoom}
onKeyDown={keyDownCallback}
isScrolling={setIsScrolling}
increaseViewportBy={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/

import { IconButton, Menu, MenuTitle, Tooltip, RadioMenuItem } from "@vector-im/compound-web";
import { IconButton, Menu, MenuTitle, Tooltip, RadioMenuItem, CheckboxMenuItem } from "@vector-im/compound-web";
import React, { type Ref, type JSX, useState, useCallback } from "react";
import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";

Expand Down Expand Up @@ -36,11 +36,15 @@ export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
const [open, setOpen] = useState(false);

const onActivitySelected = useCallback(() => {
vm.sort(SortOption.Activity);
vm.sort(SortOption.Activity, vm.useSections);
}, [vm]);

const onAtoZSelected = useCallback(() => {
vm.sort(SortOption.AToZ);
vm.sort(SortOption.AToZ, vm.useSections);
}, [vm]);

const onUseSectionsSelected = useCallback(() => {
vm.sort(vm.activeSortOption, !vm.useSections);
}, [vm]);

return (
Expand All @@ -63,6 +67,11 @@ export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
checked={vm.activeSortOption === SortOption.AToZ}
onSelect={onAtoZSelected}
/>
<CheckboxMenuItem
label={_t("room_list|sort_sections")}
checked={vm.useSections}
onSelect={onUseSectionsSelected}
/>
</Menu>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 ReactNode } from "react";

import {
type RoomListSectionHeader,
RoomListSectionKey,
} from "../../../../stores/room-list-v3/skip-list/SectionProcessor.ts";
import { _t } from "../../../../shared-components/utils/i18n.tsx";

interface RoomListSectionHeaderViewProps extends React.HTMLAttributes<HTMLDivElement> {
section: RoomListSectionHeader;
}

function sectionTitle(section: RoomListSectionKey): ReactNode {
switch (section) {
case RoomListSectionKey.Favourite:
return _t("room_list|sections|favourite");
case RoomListSectionKey.Unread:
return _t("room_list|sections|unread");
case RoomListSectionKey.Chat:
return _t("room_list|sections|chat");
case RoomListSectionKey.LowPriority:
return _t("room_list|sections|low_priority");
}
}

/**
* An item in the room list
*/
export function RoomListSectionHeaderView({ section, ...props }: RoomListSectionHeaderViewProps): JSX.Element {
return (
<div className="mx_RoomListSectionHeaderView" {...props}>
<h4>{sectionTitle(section.key)}</h4>
</div>
);
}
7 changes: 7 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2200,6 +2200,12 @@
"open_room": "Open room %(roomName)s"
},
"room_options": "Room Options",
"sections": {
"chat": "Chats",
"favourite": "Favourites",
"low_priority": "Low priority",
"unread": "Unreads"
},
"show_less": "Show less",
"show_n_more": {
"one": "Show %(count)s more",
Expand All @@ -2210,6 +2216,7 @@
"sort_by": "Sort by",
"sort_by_activity": "Activity",
"sort_by_alphabet": "A-Z",
"sort_sections": "Group rooms by type",
"sort_type": {
"activity": "Activity",
"atoz": "A-Z"
Expand Down
Loading
Loading