@@ -11,6 +11,8 @@ import {
1111 type FilterId ,
1212 type RoomListViewActions ,
1313 type RoomListViewState ,
14+ type RoomListSection ,
15+ _t ,
1416} from "@element-hq/web-shared-components" ;
1517import { type MatrixClient , type Room } from "matrix-js-sdk/src/matrix" ;
1618
@@ -19,39 +21,53 @@ import dispatcher from "../../dispatcher/dispatcher";
1921import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload" ;
2022import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload" ;
2123import SpaceStore from "../../stores/spaces/SpaceStore" ;
22- import RoomListStoreV3 , { RoomListStoreV3Event , type RoomsResult } from "../../stores/room-list-v3/RoomListStoreV3" ;
23- import { FilterKey } from "../../stores/room-list-v3/skip-list/filters" ;
24+ import RoomListStoreV3 , {
25+ CHATS_TAG ,
26+ RoomListStoreV3Event ,
27+ type RoomsResult ,
28+ } from "../../stores/room-list-v3/RoomListStoreV3" ;
29+ import { FilterEnum } from "../../stores/room-list-v3/skip-list/filters" ;
2430import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore" ;
2531import { RoomListItemViewModel } from "./RoomListItemViewModel" ;
2632import { SdkContextClass } from "../../contexts/SDKContext" ;
2733import { hasCreateRoomRights } from "./utils" ;
2834import { keepIfSame } from "../../utils/keepIfSame" ;
35+ import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag" ;
36+ import { RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderViewModel" ;
2937
3038interface RoomListViewModelProps {
3139 client : MatrixClient ;
3240}
3341
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" ] ,
42+ const filterKeyToIdMap : Map < FilterEnum , FilterId > = new Map ( [
43+ [ FilterEnum . UnreadFilter , "unread" ] ,
44+ [ FilterEnum . PeopleFilter , "people" ] ,
45+ [ FilterEnum . RoomsFilter , "rooms" ] ,
46+ [ FilterEnum . FavouriteFilter , "favourite" ] ,
47+ [ FilterEnum . MentionsFilter , "mentions" ] ,
48+ [ FilterEnum . InvitesFilter , "invites" ] ,
49+ [ FilterEnum . LowPriorityFilter , "low_priority" ] ,
4250] ) ;
4351
52+ const TAG_TO_TITLE_MAP : Record < string , string > = {
53+ [ DefaultTagID . Favourite ] : _t ( "room_list|section|favourites" ) ,
54+ [ CHATS_TAG ] : _t ( "room_list|section|chats" ) ,
55+ [ DefaultTagID . LowPriority ] : _t ( "room_list|section|low_priority" ) ,
56+ } ;
57+
4458export class RoomListViewModel
4559 extends BaseViewModel < RoomListViewSnapshot , RoomListViewModelProps >
4660 implements RoomListViewActions
4761{
4862 // State tracking
49- private activeFilter : FilterKey | undefined = undefined ;
63+ private activeFilter : FilterEnum | undefined = undefined ;
5064 private roomsResult : RoomsResult ;
5165 private lastActiveRoomIndex : number | undefined = undefined ;
5266
5367 // Child view model management
5468 private roomItemViewModels = new Map < string , RoomListItemViewModel > ( ) ;
69+ // Don't clear section vm because we want to keep the expand/collapse state even during space changes.
70+ private roomSectionHeaderViewModels = new Map < string , RoomListSectionHeaderViewModel > ( ) ;
5571 private roomsMap = new Map < string , Room > ( ) ;
5672
5773 public constructor ( props : RoomListViewModelProps ) {
@@ -61,22 +77,23 @@ export class RoomListViewModel
6177 const roomsResult = RoomListStoreV3 . instance . getSortedRoomsInActiveSpace ( undefined ) ;
6278 const canCreateRoom = hasCreateRoomRights ( props . client , activeSpace ) ;
6379 const filterIds = [ ...filterKeyToIdMap . values ( ) ] ;
64- const roomIds = roomsResult . rooms . map ( ( room ) => room . roomId ) ;
65- const sections = [ { id : "all" , roomIds } ] ;
80+
81+ // By default, all sections are expanded
82+ const { sections, isFlatList } = computeSections ( roomsResult , ( tag ) => true ) ;
83+ const isRoomListEmpty = roomsResult . sections . every ( ( section ) => section . rooms . length === 0 ) ;
6684
6785 super ( props , {
6886 // Initial view state - start with empty, will populate in async init
6987 isLoadingRooms : RoomListStoreV3 . instance . isLoadingRooms ,
70- isRoomListEmpty : roomsResult . rooms . length === 0 ,
88+ isRoomListEmpty,
7189 filterIds,
7290 activeFilterId : undefined ,
7391 roomListState : {
7492 activeRoomIndex : undefined ,
7593 spaceId : roomsResult . spaceId ,
7694 filterKeys : undefined ,
7795 } ,
78- // Until we implement sections, this view model only supports the flat list mode
79- isFlatList : true ,
96+ isFlatList,
8097 sections,
8198 canCreateRoom,
8299 } ) ;
@@ -117,7 +134,7 @@ export class RoomListViewModel
117134
118135 public onToggleFilter = ( filterId : FilterId ) : void => {
119136 // Find the FilterKey by matching the filter ID
120- let filterKey : FilterKey | undefined = undefined ;
137+ let filterKey : FilterEnum | undefined = undefined ;
121138 for ( const [ key , id ] of filterKeyToIdMap . entries ( ) ) {
122139 if ( id === filterId ) {
123140 filterKey = key ;
@@ -147,7 +164,7 @@ export class RoomListViewModel
147164 */
148165 private updateRoomsMap ( roomsResult : RoomsResult ) : void {
149166 this . roomsMap . clear ( ) ;
150- for ( const room of roomsResult . rooms ) {
167+ for ( const room of roomsResult . sections . flatMap ( ( section ) => section . rooms ) ) {
151168 this . roomsMap . set ( room . roomId , room ) ;
152169 }
153170 }
@@ -167,7 +184,7 @@ export class RoomListViewModel
167184 * Get the ordered list of room IDs.
168185 */
169186 public get roomIds ( ) : string [ ] {
170- return this . roomsResult . rooms . map ( ( room ) => room . roomId ) ;
187+ return this . roomsResult . sections . flatMap ( ( section ) => section . rooms ) . map ( ( room ) => room . roomId ) ;
171188 }
172189
173190 /**
@@ -199,13 +216,17 @@ export class RoomListViewModel
199216 return viewModel ;
200217 }
201218
202- /**
203- * Not implemented - this view model does not support sections.
204- * Flat list mode is forced so this method is never be called.
205- * @throw Error if called
206- */
207- public getSectionHeaderViewModel ( ) : never {
208- throw new Error ( "Sections are not supported in this room list" ) ;
219+ public getSectionHeaderViewModel ( tag : string ) : RoomListSectionHeaderViewModel {
220+ if ( this . roomSectionHeaderViewModels . has ( tag ) ) return this . roomSectionHeaderViewModels . get ( tag ) ! ;
221+
222+ const title = TAG_TO_TITLE_MAP [ tag ] || tag ;
223+ const viewModel = new RoomListSectionHeaderViewModel ( {
224+ tag,
225+ title,
226+ onToggleExpanded : ( ) => this . updateRoomListData ( ) ,
227+ } ) ;
228+ this . roomSectionHeaderViewModels . set ( tag , viewModel ) ;
229+ return viewModel ;
209230 }
210231
211232 /**
@@ -250,7 +271,7 @@ export class RoomListViewModel
250271 if ( ! currentRoomId ) return ;
251272
252273 const { delta, unread } = payload ;
253- const rooms = this . roomsResult . rooms ;
274+ const rooms = this . roomsResult . sections . flatMap ( ( section ) => section . rooms ) ;
254275
255276 const filteredRooms = unread
256277 ? // Filter the rooms to only include unread ones and the active room
@@ -338,7 +359,9 @@ export class RoomListViewModel
338359 return undefined ;
339360 }
340361
341- const index = this . roomsResult . rooms . findIndex ( ( room ) => room . roomId === roomId ) ;
362+ const index = this . roomsResult . sections
363+ . flatMap ( ( section ) => section . rooms )
364+ . findIndex ( ( room ) => room . roomId === roomId ) ;
342365 return index >= 0 ? index : undefined ;
343366 }
344367
@@ -350,47 +373,47 @@ export class RoomListViewModel
350373 * @param roomId - The room ID to apply sticky logic for (can be null/undefined)
351374 * @returns The modified rooms array with sticky positioning applied
352375 */
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- }
376+ // private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] {
377+ // const rooms = this.roomsResult.rooms;
378+
379+ // if (!roomId) {
380+ // return rooms;
381+ // }
382+
383+ // const newIndex = rooms.findIndex((room) => room.roomId === roomId);
384+ // const oldIndex = this.lastActiveRoomIndex;
385+
386+ // // When opening another room, the index should obviously change
387+ // if (isRoomChange) {
388+ // return rooms;
389+ // }
390+
391+ // // If oldIndex is undefined, then there was no active room before
392+ // // Similarly, if newIndex is -1, the active room is not in the current list
393+ // if (newIndex === -1 || oldIndex === undefined) {
394+ // return rooms;
395+ // }
396+
397+ // // If the index hasn't changed, we have nothing to do
398+ // if (newIndex === oldIndex) {
399+ // return rooms;
400+ // }
401+
402+ // // If the old index falls out of the bounds of the rooms array
403+ // // (usually because rooms were removed), we can no longer place
404+ // // the active room in the same old index
405+ // if (oldIndex > rooms.length - 1) {
406+ // return rooms;
407+ // }
408+
409+ // // Making the active room sticky is as simple as removing it from
410+ // // its new index and placing it in the old index
411+ // const newRooms = [...rooms];
412+ // const [stickyRoom] = newRooms.splice(newIndex, 1);
413+ // newRooms.splice(oldIndex, 0, stickyRoom);
414+
415+ // return newRooms;
416+ // }
394417
395418 private async updateRoomListData (
396419 isRoomChange : boolean = false ,
@@ -400,14 +423,15 @@ export class RoomListViewModel
400423 // Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
401424 const roomId = roomIdOverride ?? SdkContextClass . instance . roomViewStore . getRoomId ( ) ;
402425
426+ // TODO to implement for sections
403427 // Apply sticky room logic to keep selected room at same position
404- const stickyRooms = this . applyStickyRoom ( isRoomChange , roomId ) ;
428+ // const stickyRooms = this.applyStickyRoom(isRoomChange, roomId);
405429
406430 // Update roomsResult with sticky rooms
407- this . roomsResult = {
408- ...this . roomsResult ,
409- rooms : stickyRooms ,
410- } ;
431+ // this.roomsResult = {
432+ // ...this.roomsResult,
433+ // rooms: stickyRooms,
434+ // };
411435
412436 // Rebuild roomsMap with the reordered rooms
413437 this . updateRoomsMap ( this . roomsResult ) ;
@@ -420,8 +444,10 @@ export class RoomListViewModel
420444
421445 // Build the complete state atomically to ensure consistency
422446 // roomIds and roomListState must always be in sync
423- const roomIds = this . roomIds ;
424- const sections = [ { id : "all" , roomIds } ] ;
447+ const { sections, isFlatList } = computeSections (
448+ this . roomsResult ,
449+ ( tag ) => this . roomSectionHeaderViewModels . get ( tag ) ?. isExpanded ?? true ,
450+ ) ;
425451
426452 // Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
427453 const previousFilterKeys = this . snapshot . current . roomListState . filterKeys ;
@@ -433,7 +459,7 @@ export class RoomListViewModel
433459 } ;
434460
435461 const activeFilterId = this . activeFilter !== undefined ? filterKeyToIdMap . get ( this . activeFilter ) : undefined ;
436- const isRoomListEmpty = roomIds . length === 0 ;
462+ const isRoomListEmpty = this . roomsResult . sections . every ( ( section ) => section . rooms . length === 0 ) ;
437463 const isLoadingRooms = RoomListStoreV3 . instance . isLoadingRooms ;
438464
439465 // Single atomic snapshot update
@@ -443,6 +469,7 @@ export class RoomListViewModel
443469 activeFilterId,
444470 roomListState,
445471 sections,
472+ isFlatList,
446473 } ) ;
447474 }
448475
@@ -464,3 +491,30 @@ export class RoomListViewModel
464491 }
465492 } ;
466493}
494+
495+ /**
496+ * Compute the sections to display in the room list based on the rooms result and section expansion state.
497+ * @param roomsResult - The current rooms result containing sections and rooms
498+ * @param isSectionExpanded - A function that takes a section tag and returns whether that section is currently expanded
499+ * @returns An object containing the computed sections with room IDs (empty if section is collapsed) and a boolean indicating if the list should be displayed as a flat list (only one section with all rooms)
500+ */
501+ function computeSections (
502+ roomsResult : RoomsResult ,
503+ isSectionExpanded : ( tag : string ) => boolean ,
504+ ) : { sections : RoomListSection [ ] ; isFlatList : boolean } {
505+ const sections = roomsResult . sections
506+ . map ( ( { tag, rooms } ) => ( {
507+ id : tag ,
508+ roomIds : rooms . map ( ( room ) => room . roomId ) ,
509+ } ) )
510+ // Only include sections that have rooms
511+ . filter ( ( section ) => section . roomIds . length > 0 )
512+ // Remove roomIds for sections that are currently collapsed according to their section header view model
513+ . map ( ( section ) => ( {
514+ ...section ,
515+ roomIds : isSectionExpanded ( section . id ) ? section . roomIds : [ ] ,
516+ } ) ) ;
517+ const isFlatList = sections . length === 1 && sections [ 0 ] . id === CHATS_TAG ;
518+
519+ return { sections, isFlatList } ;
520+ }
0 commit comments