Skip to content

Commit 57b47eb

Browse files
committed
feat: first draft of sections in vms
1 parent 3054358 commit 57b47eb

3 files changed

Lines changed: 127 additions & 68 deletions

File tree

apps/web/src/i18n/strings/en_EN.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2164,6 +2164,11 @@
21642164
"one": "Currently removing messages in %(count)s room",
21652165
"other": "Currently removing messages in %(count)s rooms"
21662166
},
2167+
"section": {
2168+
"chats": "Chats",
2169+
"favourites": "Favourites",
2170+
"low_priority": "Low Priority"
2171+
},
21672172
"show_less": "Show less",
21682173
"show_n_more": {
21692174
"one": "Show %(count)s more",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import {
9+
BaseViewModel,
10+
type RoomListSectionHeaderActions,
11+
type RoomListSectionHeaderViewSnapshot,
12+
} from "@element-hq/web-shared-components";
13+
14+
interface RoomListSectionHeaderViewModelProps {
15+
tag: string;
16+
title: string;
17+
onToggleExpanded: (isExpanded: boolean) => void;
18+
}
19+
20+
export class RoomListSectionHeaderViewModel
21+
extends BaseViewModel<RoomListSectionHeaderViewSnapshot, RoomListSectionHeaderViewModelProps>
22+
implements RoomListSectionHeaderActions
23+
{
24+
public constructor(props: RoomListSectionHeaderViewModelProps) {
25+
super(props, { id: props.tag, title: props.title, isExpanded: true });
26+
}
27+
28+
public onClick = (): void => {
29+
const isExpanded = !this.snapshot.current.isExpanded;
30+
this.props.onToggleExpanded(isExpanded);
31+
this.snapshot.merge({ isExpanded });
32+
};
33+
}

apps/web/src/viewmodels/room-list/RoomListViewViewModel.ts

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type FilterId,
1212
type RoomListViewActions,
1313
type RoomListViewState,
14+
_t,
1415
} from "@element-hq/web-shared-components";
1516
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
1617

@@ -20,37 +21,47 @@ import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDel
2021
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
2122
import SpaceStore from "../../stores/spaces/SpaceStore";
2223
import RoomListStoreV3, { RoomListStoreV3Event, type RoomsResult } from "../../stores/room-list-v3/RoomListStoreV3";
23-
import { FilterKey } from "../../stores/room-list-v3/skip-list/filters";
24+
import { FilterEnum, type FilterKey } from "../../stores/room-list-v3/skip-list/filters";
2425
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
2526
import { RoomListItemViewModel } from "./RoomListItemViewModel";
2627
import { SdkContextClass } from "../../contexts/SDKContext";
2728
import { hasCreateRoomRights } from "./utils";
2829
import { keepIfSame } from "../../utils/keepIfSame";
30+
import { CHATS_TAG } from "../../stores/room-list-v3/SectionStore";
31+
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
32+
import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel";
2933

3034
interface RoomListViewViewModelProps {
3135
client: MatrixClient;
3236
}
3337

34-
const filterKeyToIdMap: Map<FilterKey, FilterId> = new Map([
35-
[FilterKey.UnreadFilter, "unread"],
36-
[FilterKey.PeopleFilter, "people"],
37-
[FilterKey.RoomsFilter, "rooms"],
38-
[FilterKey.FavouriteFilter, "favourite"],
39-
[FilterKey.MentionsFilter, "mentions"],
40-
[FilterKey.InvitesFilter, "invites"],
41-
[FilterKey.LowPriorityFilter, "low_priority"],
38+
const filterKeyToIdMap: Map<FilterEnum, FilterId> = new Map([
39+
[FilterEnum.UnreadFilter, "unread"],
40+
[FilterEnum.PeopleFilter, "people"],
41+
[FilterEnum.RoomsFilter, "rooms"],
42+
[FilterEnum.FavouriteFilter, "favourite"],
43+
[FilterEnum.MentionsFilter, "mentions"],
44+
[FilterEnum.InvitesFilter, "invites"],
45+
[FilterEnum.LowPriorityFilter, "low_priority"],
4246
]);
4347

48+
const TAG_TO_TITLE_MAP: Record<string, string> = {
49+
[DefaultTagID.Favourite]: _t("room_list|section|favourites"),
50+
[DefaultTagID.LowPriority]: _t("room_list|section|low_priority"),
51+
[CHATS_TAG]: _t("room_list|section|chats"),
52+
};
53+
4454
export class RoomListViewViewModel
4555
extends BaseViewModel<RoomListSnapshot, RoomListViewViewModelProps>
4656
implements RoomListViewActions
4757
{
4858
// State tracking
49-
private activeFilter: FilterKey | undefined = undefined;
59+
private activeFilter: FilterEnum | undefined = undefined;
5060
private roomsResult: RoomsResult;
5161
private lastActiveRoomIndex: number | undefined = undefined;
5262

5363
// Child view model management
64+
private roomSectionHeaderViewModels = new Map<string, RoomListSectionHeaderViewModel>();
5465
private roomItemViewModels = new Map<string, RoomListItemViewModel>();
5566
private roomsMap = new Map<string, Room>();
5667

@@ -61,22 +72,26 @@ export class RoomListViewViewModel
6172
const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined);
6273
const canCreateRoom = hasCreateRoomRights(props.client, activeSpace);
6374
const filterIds = [...filterKeyToIdMap.values()];
64-
const roomIds = roomsResult.rooms.map((room) => room.roomId);
65-
const sections = [{ id: "all", roomIds }];
75+
const sections = roomsResult.sections.map(({ tag, rooms }) => ({
76+
id: tag,
77+
roomIds: rooms.map((room) => room.roomId),
78+
}));
79+
80+
const isRoomListEmpty = roomsResult.sections.every((section) => section.rooms.length === 0);
81+
const isFlatList = roomsResult.sections.length === 1 && roomsResult.sections[0].tag == CHATS_TAG;
6682

6783
super(props, {
6884
// Initial view state - start with empty, will populate in async init
6985
isLoadingRooms: RoomListStoreV3.instance.isLoadingRooms,
70-
isRoomListEmpty: roomsResult.rooms.length === 0,
86+
isRoomListEmpty,
7187
filterIds,
7288
activeFilterId: undefined,
7389
roomListState: {
7490
activeRoomIndex: undefined,
7591
spaceId: roomsResult.spaceId,
7692
filterKeys: undefined,
7793
},
78-
// Until we implement sections, this view model only supports the flat list mode
79-
isFlatList: true,
94+
isFlatList,
8095
sections,
8196
canCreateRoom,
8297
});
@@ -147,7 +162,7 @@ export class RoomListViewViewModel
147162
*/
148163
private updateRoomsMap(roomsResult: RoomsResult): void {
149164
this.roomsMap.clear();
150-
for (const room of roomsResult.rooms) {
165+
for (const room of roomsResult.sections.flatMap((section) => section.rooms)) {
151166
this.roomsMap.set(room.roomId, room);
152167
}
153168
}
@@ -167,7 +182,7 @@ export class RoomListViewViewModel
167182
* Get the ordered list of room IDs.
168183
*/
169184
public get roomIds(): string[] {
170-
return this.roomsResult.rooms.map((room) => room.roomId);
185+
return this.roomsResult.sections.flatMap((section) => section.rooms).map((room) => room.roomId);
171186
}
172187

173188
/**
@@ -250,7 +265,7 @@ export class RoomListViewViewModel
250265
if (!currentRoomId) return;
251266

252267
const { delta, unread } = payload;
253-
const rooms = this.roomsResult.rooms;
268+
const rooms = this.roomsResult.sections.flatMap((section) => section.rooms);
254269

255270
const filteredRooms = unread
256271
? // Filter the rooms to only include unread ones and the active room
@@ -338,7 +353,9 @@ export class RoomListViewViewModel
338353
return undefined;
339354
}
340355

341-
const index = this.roomsResult.rooms.findIndex((room) => room.roomId === roomId);
356+
const index = this.roomsResult.sections
357+
.flatMap((section) => section.rooms)
358+
.findIndex((room) => room.roomId === roomId);
342359
return index >= 0 ? index : undefined;
343360
}
344361

@@ -350,47 +367,47 @@ export class RoomListViewViewModel
350367
* @param roomId - The room ID to apply sticky logic for (can be null/undefined)
351368
* @returns The modified rooms array with sticky positioning applied
352369
*/
353-
private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] {
354-
const rooms = this.roomsResult.rooms;
355-
356-
if (!roomId) {
357-
return rooms;
358-
}
359-
360-
const newIndex = rooms.findIndex((room) => room.roomId === roomId);
361-
const oldIndex = this.lastActiveRoomIndex;
362-
363-
// When opening another room, the index should obviously change
364-
if (isRoomChange) {
365-
return rooms;
366-
}
367-
368-
// If oldIndex is undefined, then there was no active room before
369-
// Similarly, if newIndex is -1, the active room is not in the current list
370-
if (newIndex === -1 || oldIndex === undefined) {
371-
return rooms;
372-
}
373-
374-
// If the index hasn't changed, we have nothing to do
375-
if (newIndex === oldIndex) {
376-
return rooms;
377-
}
378-
379-
// If the old index falls out of the bounds of the rooms array
380-
// (usually because rooms were removed), we can no longer place
381-
// the active room in the same old index
382-
if (oldIndex > rooms.length - 1) {
383-
return rooms;
384-
}
385-
386-
// Making the active room sticky is as simple as removing it from
387-
// its new index and placing it in the old index
388-
const newRooms = [...rooms];
389-
const [stickyRoom] = newRooms.splice(newIndex, 1);
390-
newRooms.splice(oldIndex, 0, stickyRoom);
391-
392-
return newRooms;
393-
}
370+
// private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] {
371+
// const rooms = this.roomsResult.rooms;
372+
373+
// if (!roomId) {
374+
// return rooms;
375+
// }
376+
377+
// const newIndex = rooms.findIndex((room) => room.roomId === roomId);
378+
// const oldIndex = this.lastActiveRoomIndex;
379+
380+
// // When opening another room, the index should obviously change
381+
// if (isRoomChange) {
382+
// return rooms;
383+
// }
384+
385+
// // If oldIndex is undefined, then there was no active room before
386+
// // Similarly, if newIndex is -1, the active room is not in the current list
387+
// if (newIndex === -1 || oldIndex === undefined) {
388+
// return rooms;
389+
// }
390+
391+
// // If the index hasn't changed, we have nothing to do
392+
// if (newIndex === oldIndex) {
393+
// return rooms;
394+
// }
395+
396+
// // If the old index falls out of the bounds of the rooms array
397+
// // (usually because rooms were removed), we can no longer place
398+
// // the active room in the same old index
399+
// if (oldIndex > rooms.length - 1) {
400+
// return rooms;
401+
// }
402+
403+
// // Making the active room sticky is as simple as removing it from
404+
// // its new index and placing it in the old index
405+
// const newRooms = [...rooms];
406+
// const [stickyRoom] = newRooms.splice(newIndex, 1);
407+
// newRooms.splice(oldIndex, 0, stickyRoom);
408+
409+
// return newRooms;
410+
// }
394411

395412
private async updateRoomListData(
396413
isRoomChange: boolean = false,
@@ -400,14 +417,15 @@ export class RoomListViewViewModel
400417
// Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
401418
const roomId = roomIdOverride ?? SdkContextClass.instance.roomViewStore.getRoomId();
402419

420+
// TODO to implement for sections
403421
// Apply sticky room logic to keep selected room at same position
404-
const stickyRooms = this.applyStickyRoom(isRoomChange, roomId);
422+
//const stickyRooms = this.applyStickyRoom(isRoomChange, roomId);
405423

406424
// Update roomsResult with sticky rooms
407-
this.roomsResult = {
408-
...this.roomsResult,
409-
rooms: stickyRooms,
410-
};
425+
// this.roomsResult = {
426+
// ...this.roomsResult,
427+
// rooms: stickyRooms,
428+
// };
411429

412430
// Rebuild roomsMap with the reordered rooms
413431
this.updateRoomsMap(this.roomsResult);
@@ -420,8 +438,11 @@ export class RoomListViewViewModel
420438

421439
// Build the complete state atomically to ensure consistency
422440
// roomIds and roomListState must always be in sync
423-
const roomIds = this.roomIds;
424-
const sections = [{ id: "all", roomIds }];
441+
//const roomIds = this.roomIds;
442+
const sections = this.roomsResult.sections.map(({ tag, rooms }) => ({
443+
id: tag,
444+
roomIds: rooms.map((room) => room.roomId),
445+
}));
425446

426447
// Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
427448
const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
@@ -433,7 +454,7 @@ export class RoomListViewViewModel
433454
};
434455

435456
const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined;
436-
const isRoomListEmpty = roomIds.length === 0;
457+
const isRoomListEmpty = this.roomsResult.sections.every((section) => section.rooms.length === 0);
437458
const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;
438459

439460
// Single atomic snapshot update

0 commit comments

Comments
 (0)