@@ -18,6 +18,8 @@ import { SdkContextClass } from "../../../src/contexts/SDKContext";
1818import DMRoomMap from "../../../src/utils/DMRoomMap" ;
1919import { RoomListViewModel } from "../../../src/viewmodels/room-list/RoomListViewModel" ;
2020import { hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils" ;
21+ import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag" ;
22+ import SettingsStore from "../../../src/settings/SettingsStore" ;
2123
2224jest . mock ( "../../../src/viewmodels/room-list/utils" , ( ) => ( {
2325 hasCreateRoomRights : jest . fn ( ) . mockReturnValue ( false ) ,
@@ -600,5 +602,274 @@ describe("RoomListViewModel", () => {
600602 expect ( disposeSpy1 ) . toHaveBeenCalled ( ) ;
601603 expect ( disposeSpy2 ) . toHaveBeenCalled ( ) ;
602604 } ) ;
605+
606+ describe ( "Sections (feature_room_list_sections)" , ( ) => {
607+ let favRoom1 : Room ;
608+ let favRoom2 : Room ;
609+ let lowPriorityRoom : Room ;
610+ let regularRoom1 : Room ;
611+ let regularRoom2 : Room ;
612+
613+ beforeEach ( ( ) => {
614+ jest . spyOn ( SettingsStore , "getValue" ) . mockImplementation ( ( setting : string ) => {
615+ if ( setting === "feature_room_list_sections" ) return true ;
616+ return false ;
617+ } ) ;
618+
619+ favRoom1 = mkStubRoom ( "!fav1:server" , "Fav 1" , matrixClient ) ;
620+ favRoom2 = mkStubRoom ( "!fav2:server" , "Fav 2" , matrixClient ) ;
621+ lowPriorityRoom = mkStubRoom ( "!low1:server" , "Low 1" , matrixClient ) ;
622+ regularRoom1 = mkStubRoom ( "!reg1:server" , "Reg 1" , matrixClient ) ;
623+ regularRoom2 = mkStubRoom ( "!reg2:server" , "Reg 2" , matrixClient ) ;
624+
625+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
626+ spaceId : "home" ,
627+ sections : [
628+ { tag : DefaultTagID . Favourite , rooms : [ favRoom1 , favRoom2 ] } ,
629+ { tag : CHATS_TAG , rooms : [ regularRoom1 , regularRoom2 ] } ,
630+ { tag : DefaultTagID . LowPriority , rooms : [ lowPriorityRoom ] } ,
631+ ] ,
632+ } ) ;
633+ } ) ;
634+
635+ it ( "should initialize with multiple sections" , ( ) => {
636+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
637+
638+ const snapshot = viewModel . getSnapshot ( ) ;
639+ expect ( snapshot . sections ) . toHaveLength ( 3 ) ;
640+ expect ( snapshot . sections [ 0 ] . id ) . toBe ( DefaultTagID . Favourite ) ;
641+ expect ( snapshot . sections [ 0 ] . roomIds ) . toEqual ( [ "!fav1:server" , "!fav2:server" ] ) ;
642+ expect ( snapshot . sections [ 1 ] . id ) . toBe ( CHATS_TAG ) ;
643+ expect ( snapshot . sections [ 1 ] . roomIds ) . toEqual ( [ "!reg1:server" , "!reg2:server" ] ) ;
644+ expect ( snapshot . sections [ 2 ] . id ) . toBe ( DefaultTagID . LowPriority ) ;
645+ expect ( snapshot . sections [ 2 ] . roomIds ) . toEqual ( [ "!low1:server" ] ) ;
646+ } ) ;
647+
648+ it ( "should not be a flat list when multiple sections exist" , ( ) => {
649+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
650+
651+ expect ( viewModel . getSnapshot ( ) . isFlatList ) . toBe ( false ) ;
652+ } ) ;
653+
654+ it ( "should be a flat list when only chats section has rooms" , ( ) => {
655+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
656+ spaceId : "home" ,
657+ sections : [
658+ { tag : DefaultTagID . Favourite , rooms : [ ] } ,
659+ { tag : CHATS_TAG , rooms : [ regularRoom1 ] } ,
660+ { tag : DefaultTagID . LowPriority , rooms : [ ] } ,
661+ ] ,
662+ } ) ;
663+
664+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
665+
666+ expect ( viewModel . getSnapshot ( ) . isFlatList ) . toBe ( true ) ;
667+ expect ( viewModel . getSnapshot ( ) . sections ) . toHaveLength ( 1 ) ;
668+ expect ( viewModel . getSnapshot ( ) . sections [ 0 ] . id ) . toBe ( CHATS_TAG ) ;
669+ } ) ;
670+
671+ it ( "should exclude favourite and low_priority from filter list" , ( ) => {
672+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
673+
674+ const snapshot = viewModel . getSnapshot ( ) ;
675+ expect ( snapshot . filterIds ) . not . toContain ( "favourite" ) ;
676+ expect ( snapshot . filterIds ) . not . toContain ( "low_priority" ) ;
677+ // Other filters should still be present
678+ expect ( snapshot . filterIds ) . toContain ( "unread" ) ;
679+ expect ( snapshot . filterIds ) . toContain ( "people" ) ;
680+ } ) ;
681+
682+ it ( "should omit empty sections from snapshot" , ( ) => {
683+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
684+ spaceId : "home" ,
685+ sections : [
686+ { tag : DefaultTagID . Favourite , rooms : [ ] } ,
687+ { tag : CHATS_TAG , rooms : [ regularRoom1 ] } ,
688+ { tag : DefaultTagID . LowPriority , rooms : [ ] } ,
689+ ] ,
690+ } ) ;
691+
692+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
693+
694+ const snapshot = viewModel . getSnapshot ( ) ;
695+ expect ( snapshot . sections ) . toHaveLength ( 1 ) ;
696+ expect ( snapshot . sections [ 0 ] . id ) . toBe ( CHATS_TAG ) ;
697+ } ) ;
698+
699+ it ( "should create section header view models on demand" , ( ) => {
700+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
701+
702+ const headerVM = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
703+ expect ( headerVM ) . toBeDefined ( ) ;
704+ expect ( headerVM . getSnapshot ( ) . id ) . toBe ( DefaultTagID . Favourite ) ;
705+ expect ( headerVM . getSnapshot ( ) . isExpanded ) . toBe ( true ) ;
706+ } ) ;
707+
708+ it ( "should reuse section header view models" , ( ) => {
709+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
710+
711+ const headerVM1 = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
712+ const headerVM2 = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
713+ expect ( headerVM1 ) . toBe ( headerVM2 ) ;
714+ } ) ;
715+
716+ it ( "should hide room IDs when a section is collapsed" , ( ) => {
717+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
718+
719+ // Collapse the favourite section
720+ const favHeader = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
721+ favHeader . onClick ( ) ;
722+ expect ( favHeader . isExpanded ) . toBe ( false ) ;
723+
724+ const snapshot = viewModel . getSnapshot ( ) ;
725+ const favSection = snapshot . sections . find ( ( s ) => s . id === DefaultTagID . Favourite ) ;
726+ expect ( favSection ) . toBeDefined ( ) ;
727+ // Collapsed sections have an empty roomIds list
728+ expect ( favSection ! . roomIds ) . toEqual ( [ ] ) ;
729+
730+ // Other sections remain unaffected
731+ const chatsSection = snapshot . sections . find ( ( s ) => s . id === CHATS_TAG ) ;
732+ expect ( chatsSection ! . roomIds ) . toEqual ( [ "!reg1:server" , "!reg2:server" ] ) ;
733+ } ) ;
734+
735+ it ( "should restore room IDs when a section is re-expanded" , ( ) => {
736+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
737+
738+ const favHeader = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
739+
740+ // Collapse then re-expand
741+ favHeader . onClick ( ) ;
742+ favHeader . onClick ( ) ;
743+ expect ( favHeader . isExpanded ) . toBe ( true ) ;
744+
745+ const snapshot = viewModel . getSnapshot ( ) ;
746+ const favSection = snapshot . sections . find ( ( s ) => s . id === DefaultTagID . Favourite ) ;
747+ expect ( favSection ! . roomIds ) . toEqual ( [ "!fav1:server" , "!fav2:server" ] ) ;
748+ } ) ;
749+
750+ it ( "should update sections when room list changes" , ( ) => {
751+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
752+
753+ const newFav = mkStubRoom ( "!fav3:server" , "Fav 3" , matrixClient ) ;
754+
755+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
756+ spaceId : "home" ,
757+ sections : [
758+ { tag : DefaultTagID . Favourite , rooms : [ favRoom1 , favRoom2 , newFav ] } ,
759+ { tag : CHATS_TAG , rooms : [ regularRoom1 , regularRoom2 ] } ,
760+ { tag : DefaultTagID . LowPriority , rooms : [ lowPriorityRoom ] } ,
761+ ] ,
762+ } ) ;
763+
764+ RoomListStoreV3 . instance . emit ( RoomListStoreV3Event . ListsUpdate ) ;
765+
766+ const snapshot = viewModel . getSnapshot ( ) ;
767+ expect ( snapshot . sections [ 0 ] . roomIds ) . toEqual ( [ "!fav1:server" , "!fav2:server" , "!fav3:server" ] ) ;
768+ } ) ;
769+
770+ it ( "should preserve section collapse state across list updates" , ( ) => {
771+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
772+
773+ // Collapse favourites
774+ const favHeader = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
775+ favHeader . onClick ( ) ;
776+
777+ // Trigger a list update
778+ RoomListStoreV3 . instance . emit ( RoomListStoreV3Event . ListsUpdate ) ;
779+
780+ const snapshot = viewModel . getSnapshot ( ) ;
781+ const favSection = snapshot . sections . find ( ( s ) => s . id === DefaultTagID . Favourite ) ;
782+ expect ( favSection ! . roomIds ) . toEqual ( [ ] ) ;
783+ } ) ;
784+
785+ it ( "should preserve section collapse state across space changes" , ( ) => {
786+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
787+
788+ // Collapse favourites
789+ const favHeader = viewModel . getSectionHeaderViewModel ( DefaultTagID . Favourite ) ;
790+ favHeader . onClick ( ) ;
791+
792+ // Switch to a different space with its own rooms
793+ const spaceFav = mkStubRoom ( "!spacefav:server" , "Space Fav" , matrixClient ) ;
794+ const spaceReg = mkStubRoom ( "!spacereg:server" , "Space Reg" , matrixClient ) ;
795+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
796+ spaceId : "!space:server" ,
797+ sections : [
798+ { tag : DefaultTagID . Favourite , rooms : [ spaceFav ] } ,
799+ { tag : CHATS_TAG , rooms : [ spaceReg ] } ,
800+ { tag : DefaultTagID . LowPriority , rooms : [ ] } ,
801+ ] ,
802+ } ) ;
803+ jest . spyOn ( SpaceStore . instance , "getLastSelectedRoomIdForSpace" ) . mockReturnValue ( null ) ;
804+
805+ RoomListStoreV3 . instance . emit ( RoomListStoreV3Event . ListsUpdate ) ;
806+
807+ const snapshot = viewModel . getSnapshot ( ) ;
808+ // Favourites should still be collapsed even after the space change
809+ const favSection = snapshot . sections . find ( ( s ) => s . id === DefaultTagID . Favourite ) ;
810+ expect ( favSection ) . toBeDefined ( ) ;
811+ expect ( favSection ! . roomIds ) . toEqual ( [ ] ) ;
812+
813+ // Other sections should remain expanded
814+ const chatsSection = snapshot . sections . find ( ( s ) => s . id === CHATS_TAG ) ;
815+ expect ( chatsSection ! . roomIds ) . toEqual ( [ "!spacereg:server" ] ) ;
816+ } ) ;
817+
818+ it ( "should apply filters across all sections" , ( ) => {
819+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
820+
821+ // Only favRoom1 is unread
822+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
823+ spaceId : "home" ,
824+ sections : [
825+ { tag : DefaultTagID . Favourite , rooms : [ favRoom1 ] } ,
826+ { tag : CHATS_TAG , rooms : [ ] } ,
827+ { tag : DefaultTagID . LowPriority , rooms : [ ] } ,
828+ ] ,
829+ filterKeys : [ FilterEnum . UnreadFilter ] ,
830+ } ) ;
831+
832+ viewModel . onToggleFilter ( "unread" ) ;
833+
834+ const snapshot = viewModel . getSnapshot ( ) ;
835+ expect ( snapshot . activeFilterId ) . toBe ( "unread" ) ;
836+ // Only the favourite section should remain (chats and low priority are empty)
837+ expect ( snapshot . sections ) . toHaveLength ( 1 ) ;
838+ expect ( snapshot . sections [ 0 ] . id ) . toBe ( DefaultTagID . Favourite ) ;
839+ expect ( snapshot . sections [ 0 ] . roomIds ) . toEqual ( [ "!fav1:server" ] ) ;
840+ } ) ;
841+
842+ it ( "should apply sticky room within the correct section" , async ( ) => {
843+ stubClient ( ) ;
844+ viewModel = new RoomListViewModel ( { client : matrixClient } ) ;
845+
846+ // Select favRoom1 (index 0 globally, index 0 in favourites section)
847+ jest . spyOn ( SdkContextClass . instance . roomViewStore , "getRoomId" ) . mockReturnValue ( "!fav1:server" ) ;
848+ dispatcher . dispatch ( {
849+ action : Action . ActiveRoomChanged ,
850+ newRoomId : "!fav1:server" ,
851+ } ) ;
852+ await flushPromises ( ) ;
853+
854+ expect ( viewModel . getSnapshot ( ) . roomListState . activeRoomIndex ) . toBe ( 0 ) ;
855+
856+ // Room list update moves favRoom1 to second position within favourites
857+ jest . spyOn ( RoomListStoreV3 . instance , "getSortedRoomsInActiveSpace" ) . mockReturnValue ( {
858+ spaceId : "home" ,
859+ sections : [
860+ { tag : DefaultTagID . Favourite , rooms : [ favRoom2 , favRoom1 ] } ,
861+ { tag : CHATS_TAG , rooms : [ regularRoom1 , regularRoom2 ] } ,
862+ { tag : DefaultTagID . LowPriority , rooms : [ lowPriorityRoom ] } ,
863+ ] ,
864+ } ) ;
865+
866+ RoomListStoreV3 . instance . emit ( RoomListStoreV3Event . ListsUpdate ) ;
867+
868+ // Sticky room should keep favRoom1 at index 0 within the favourites section
869+ const snapshot = viewModel . getSnapshot ( ) ;
870+ expect ( snapshot . sections [ 0 ] . roomIds [ 0 ] ) . toBe ( "!fav1:server" ) ;
871+ expect ( snapshot . roomListState . activeRoomIndex ) . toBe ( 0 ) ;
872+ } ) ;
873+ } ) ;
603874 } ) ;
604875} ) ;
0 commit comments