matrix_sdk_ui/spaces/
room_list.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for that specific language governing permissions and
13// limitations under the License.
14
15use std::{cmp::Ordering, collections::HashMap, sync::Arc};
16
17use eyeball::{ObservableWriteGuard, SharedObservable, Subscriber};
18use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
19use futures_util::pin_mut;
20use imbl::Vector;
21use itertools::Itertools;
22use matrix_sdk::{Client, Error, executor::AbortOnDrop, locks::Mutex, paginators::PaginationToken};
23use matrix_sdk_common::executor::spawn;
24use ruma::{
25    OwnedRoomId,
26    api::client::space::get_hierarchy,
27    events::space::child::{HierarchySpaceChildEvent, SpaceChildEventContent},
28    uint,
29};
30use tokio::sync::Mutex as AsyncMutex;
31use tracing::{error, warn};
32
33use crate::spaces::SpaceRoom;
34
35#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum SpaceRoomListPaginationState {
38    Idle { end_reached: bool },
39    Loading,
40}
41
42/// The `SpaceRoomList`represents a paginated list of direct rooms
43/// that belong to a particular space.
44///
45/// It can be used to paginate through the list (and have live updates on the
46/// pagination state) as well as subscribe to changes as rooms are joined or
47/// left.
48///
49/// The `SpaceRoomList` also automatically subscribes to client room changes
50/// and updates the list accordingly as rooms are joined or left.
51///
52/// # Examples
53///
54/// ```no_run
55/// use futures_util::StreamExt;
56/// use matrix_sdk::Client;
57/// use matrix_sdk_ui::spaces::{
58///     SpaceService, room_list::SpaceRoomListPaginationState,
59/// };
60/// use ruma::owned_room_id;
61///
62/// # async {
63/// # let client: Client = todo!();
64/// let space_service = SpaceService::new(client.clone());
65///
66/// // Get a list of all the rooms in a particular space
67/// let room_list = space_service
68///     .space_room_list(owned_room_id!("!some_space:example.org"))
69///     .await;
70///
71/// // Start off with an empty and idle list
72/// room_list.rooms().is_empty();
73///
74/// assert_eq!(
75///     room_list.pagination_state(),
76///     SpaceRoomListPaginationState::Idle { end_reached: false }
77/// );
78///
79/// // Subscribe to pagination state updates
80/// let pagination_state_stream =
81///     room_list.subscribe_to_pagination_state_updates();
82///
83/// // And to room list updates
84/// let (_, room_stream) = room_list.subscribe_to_room_updates();
85///
86/// // Run this in a background task so it doesn't block
87/// while let Some(pagination_state) = pagination_state_stream.next().await {
88///     println!("Received pagination state update: {pagination_state:?}");
89/// }
90///
91/// // Run this in a background task so it doesn't block
92/// while let Some(diffs) = room_stream.next().await {
93///     println!("Received room list update: {diffs:?}");
94/// }
95///
96/// // Ask the room to load the next page
97/// room_list.paginate().await.unwrap();
98///
99/// // And, if successful, rooms are available
100/// let rooms = room_list.rooms();
101/// # anyhow::Ok(()) };
102/// ```
103pub struct SpaceRoomList {
104    client: Client,
105
106    space_id: OwnedRoomId,
107
108    space: SharedObservable<Option<SpaceRoom>>,
109
110    children_state: Mutex<Option<HashMap<OwnedRoomId, HierarchySpaceChildEvent>>>,
111
112    token: AsyncMutex<PaginationToken>,
113
114    pagination_state: SharedObservable<SpaceRoomListPaginationState>,
115
116    rooms: Arc<Mutex<ObservableVector<SpaceRoom>>>,
117
118    _space_update_handle: Option<AbortOnDrop<()>>,
119
120    _room_update_handle: AbortOnDrop<()>,
121}
122
123impl SpaceRoomList {
124    /// Creates a new `SpaceRoomList` for the given space identifier.
125    pub async fn new(client: Client, space_id: OwnedRoomId) -> Self {
126        let rooms = Arc::new(Mutex::new(ObservableVector::<SpaceRoom>::new()));
127
128        let all_room_updates_receiver = client.subscribe_to_all_room_updates();
129
130        let room_update_handle = spawn({
131            let client = client.clone();
132            let rooms = rooms.clone();
133
134            async move {
135                pin_mut!(all_room_updates_receiver);
136
137                loop {
138                    match all_room_updates_receiver.recv().await {
139                        Ok(updates) => {
140                            if updates.is_empty() {
141                                continue;
142                            }
143
144                            let mut mutable_rooms = rooms.lock();
145
146                            updates.iter_all_room_ids().for_each(|updated_room_id| {
147                                if let Some((position, room)) = mutable_rooms
148                                    .clone()
149                                    .iter()
150                                    .find_position(|room| &room.room_id == updated_room_id)
151                                    && let Some(updated_room) = client.get_room(updated_room_id)
152                                {
153                                    mutable_rooms.set(
154                                        position,
155                                        SpaceRoom::new_from_known(
156                                            &updated_room,
157                                            room.children_count,
158                                        ),
159                                    );
160                                }
161                            })
162                        }
163                        Err(err) => {
164                            error!("error when listening to room updates: {err}");
165                        }
166                    }
167                }
168            }
169        });
170
171        let space_observable = SharedObservable::new(None);
172
173        let (space_room, space_update_handle) = if let Some(parent) = client.get_room(&space_id) {
174            let children_count = parent
175                .get_state_events_static::<SpaceChildEventContent>()
176                .await
177                .map_or(0, |c| c.len() as u64);
178
179            let mut subscriber = parent.subscribe_info();
180            let space_update_handle = spawn({
181                let client = client.clone();
182                let space_id = space_id.clone();
183                let space_observable = space_observable.clone();
184                async move {
185                    while subscriber.next().await.is_some() {
186                        if let Some(room) = client.get_room(&space_id) {
187                            space_observable
188                                .set(Some(SpaceRoom::new_from_known(&room, children_count)));
189                        }
190                    }
191                }
192            });
193
194            (
195                Some(SpaceRoom::new_from_known(&parent, children_count)),
196                Some(AbortOnDrop::new(space_update_handle)),
197            )
198        } else {
199            (None, None)
200        };
201
202        space_observable.set(space_room);
203
204        Self {
205            client,
206            space_id,
207            space: space_observable,
208            children_state: Mutex::new(None),
209            token: AsyncMutex::new(None.into()),
210            pagination_state: SharedObservable::new(SpaceRoomListPaginationState::Idle {
211                end_reached: false,
212            }),
213            rooms,
214            _space_update_handle: space_update_handle,
215            _room_update_handle: AbortOnDrop::new(room_update_handle),
216        }
217    }
218
219    /// Returns the space of the room list if known.
220    pub fn space(&self) -> Option<SpaceRoom> {
221        self.space.get()
222    }
223
224    /// Subscribe to space updates.
225    pub fn subscribe_to_space_updates(&self) -> Subscriber<Option<SpaceRoom>> {
226        self.space.subscribe()
227    }
228
229    /// Returns if the room list is currently paginating or not.
230    pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
231        self.pagination_state.get()
232    }
233
234    /// Subscribe to pagination updates.
235    pub fn subscribe_to_pagination_state_updates(
236        &self,
237    ) -> Subscriber<SpaceRoomListPaginationState> {
238        self.pagination_state.subscribe()
239    }
240
241    /// Return the current list of rooms.
242    pub fn rooms(&self) -> Vec<SpaceRoom> {
243        self.rooms.lock().iter().cloned().collect_vec()
244    }
245
246    /// Subscribes to room list updates.
247    pub fn subscribe_to_room_updates(
248        &self,
249    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
250        self.rooms.lock().subscribe().into_values_and_batched_stream()
251    }
252
253    /// Ask the list to retrieve the next page if the end hasn't been reached
254    /// yet. Otherwise it no-ops.
255    pub async fn paginate(&self) -> Result<(), Error> {
256        {
257            let mut pagination_state = self.pagination_state.write();
258
259            match *pagination_state {
260                SpaceRoomListPaginationState::Idle { end_reached } if end_reached => {
261                    return Ok(());
262                }
263                SpaceRoomListPaginationState::Loading => {
264                    return Ok(());
265                }
266                _ => {}
267            }
268
269            ObservableWriteGuard::set(&mut pagination_state, SpaceRoomListPaginationState::Loading);
270        }
271
272        let mut request = get_hierarchy::v1::Request::new(self.space_id.clone());
273        request.max_depth = Some(uint!(1)); // We only want the immediate children of the space
274
275        let mut pagination_token = self.token.lock().await;
276
277        if let PaginationToken::HasMore(ref token) = *pagination_token {
278            request.from = Some(token.clone());
279        }
280
281        match self.client.send(request).await {
282            Ok(result) => {
283                *pagination_token = match &result.next_batch {
284                    Some(val) => PaginationToken::HasMore(val.clone()),
285                    None => PaginationToken::HitEnd,
286                };
287
288                let mut rooms = self.rooms.lock();
289
290                // The space is part of the /hierarchy response. Partition the room array
291                // so we can use its details but also filter it out of the room list
292                let (space, children): (Vec<_>, Vec<_>) =
293                    result.rooms.into_iter().partition(|f| f.summary.room_id == self.space_id);
294
295                if let Some(room) = space.first() {
296                    let mut children_state =
297                        HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
298                    for child_state in &room.children_state {
299                        match child_state.deserialize() {
300                            Ok(child) => {
301                                children_state.insert(child.state_key.clone(), child.clone());
302                            }
303                            Err(error) => {
304                                warn!("Failed deserializing space child event: {error}");
305                            }
306                        }
307                    }
308                    *self.children_state.lock() = Some(children_state);
309
310                    let mut space = self.space.write();
311                    if space.is_none() {
312                        ObservableWriteGuard::set(
313                            &mut space,
314                            Some(SpaceRoom::new_from_summary(
315                                &room.summary,
316                                self.client.get_room(&room.summary.room_id),
317                                room.children_state.len() as u64,
318                                vec![],
319                            )),
320                        );
321                    }
322                }
323
324                let children_state = (*self.children_state.lock()).clone().unwrap_or_default();
325
326                children
327                    .iter()
328                    .map(|room| {
329                        let via = children_state
330                            .get(&room.summary.room_id)
331                            .map(|state| state.content.via.clone());
332
333                        SpaceRoom::new_from_summary(
334                            &room.summary,
335                            self.client.get_room(&room.summary.room_id),
336                            room.children_state.len() as u64,
337                            via.unwrap_or_default(),
338                        )
339                    })
340                    .sorted_by(|a, b| Self::compare_rooms(a, b, &children_state))
341                    .for_each(|room| rooms.push_back(room));
342
343                self.pagination_state.set(SpaceRoomListPaginationState::Idle {
344                    end_reached: result.next_batch.is_none(),
345                });
346
347                Ok(())
348            }
349            Err(err) => {
350                self.pagination_state
351                    .set(SpaceRoomListPaginationState::Idle { end_reached: false });
352                Err(err.into())
353            }
354        }
355    }
356
357    /// Sorts spare rooms by various criteria as defined in
358    /// https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
359    fn compare_rooms(
360        a: &SpaceRoom,
361        b: &SpaceRoom,
362        children_state: &HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
363    ) -> Ordering {
364        let a_state = children_state.get(&a.room_id);
365        let b_state = children_state.get(&b.room_id);
366
367        match (a_state, b_state) {
368            (Some(a_state), Some(b_state)) => {
369                match (&a_state.content.order, &b_state.content.order) {
370                    (Some(a_order), Some(b_order)) => a_order
371                        .cmp(b_order)
372                        .then(a_state.origin_server_ts.cmp(&b_state.origin_server_ts))
373                        .then(a.room_id.cmp(&b.room_id)),
374                    (Some(_), None) => Ordering::Greater,
375                    (None, Some(_)) => Ordering::Less,
376                    (None, None) => a_state
377                        .origin_server_ts
378                        .cmp(&b_state.origin_server_ts)
379                        .then(a.room_id.to_string().cmp(&b.room_id.to_string())),
380                }
381            }
382            _ => a.room_id.to_string().cmp(&b.room_id.to_string()),
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use std::{cmp::Ordering, collections::HashMap};
390
391    use assert_matches2::{assert_let, assert_matches};
392    use eyeball_im::VectorDiff;
393    use futures_util::pin_mut;
394    use matrix_sdk::{RoomState, test_utils::mocks::MatrixMockServer};
395    use matrix_sdk_test::{
396        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
397    };
398    use ruma::{
399        OwnedRoomId, RoomId,
400        events::space::child::HierarchySpaceChildEvent,
401        owned_room_id, owned_server_name,
402        room::{JoinRuleSummary, RoomSummary},
403        room_id, server_name, uint,
404    };
405    use serde_json::{from_value, json};
406    use stream_assert::{assert_next_eq, assert_next_matches, assert_pending, assert_ready};
407
408    use crate::spaces::{
409        SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState,
410    };
411
412    #[async_test]
413    async fn test_room_list_pagination() {
414        let server = MatrixMockServer::new().await;
415        let client = server.client_builder().build().await;
416        let user_id = client.user_id().unwrap();
417        let space_service = SpaceService::new(client.clone());
418        let factory = EventFactory::new();
419
420        server.mock_room_state_encryption().plain().mount().await;
421
422        let parent_space_id = room_id!("!parent_space:example.org");
423        let child_space_id_1 = room_id!("!1:example.org");
424        let child_space_id_2 = room_id!("!2:example.org");
425
426        server
427            .sync_room(
428                &client,
429                JoinedRoomBuilder::new(parent_space_id)
430                    .add_state_event(
431                        factory
432                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
433                            .sender(user_id),
434                    )
435                    .add_state_event(
436                        factory
437                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
438                            .sender(user_id),
439                    ),
440            )
441            .await;
442
443        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
444
445        // The space parent is known to the client and should be populated accordingly
446        assert_let!(Some(parent_space) = room_list.space());
447        assert_eq!(parent_space.children_count, 2);
448
449        // Start off idle
450        assert_matches!(
451            room_list.pagination_state(),
452            SpaceRoomListPaginationState::Idle { end_reached: false }
453        );
454
455        // without any rooms
456        assert_eq!(room_list.rooms(), vec![]);
457
458        // and with pending subscribers
459
460        let pagination_state_subscriber = room_list.subscribe_to_pagination_state_updates();
461        pin_mut!(pagination_state_subscriber);
462        assert_pending!(pagination_state_subscriber);
463
464        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
465        pin_mut!(rooms_subscriber);
466        assert_pending!(rooms_subscriber);
467
468        // Paginating the room list
469        server
470            .mock_get_hierarchy()
471            .ok_with_room_ids_and_children_state(
472                vec![child_space_id_1, child_space_id_2],
473                vec![(room_id!("!child:example.org"), vec![])],
474            )
475            .mount()
476            .await;
477
478        room_list.paginate().await.unwrap();
479
480        // informs that the pagination reached the end
481        assert_next_matches!(
482            pagination_state_subscriber,
483            SpaceRoomListPaginationState::Idle { end_reached: true }
484        );
485
486        // and yields results
487        assert_next_eq!(
488            rooms_subscriber,
489            vec![
490                VectorDiff::PushBack {
491                    value: SpaceRoom::new_from_summary(
492                        &RoomSummary::new(
493                            child_space_id_1.to_owned(),
494                            JoinRuleSummary::Public,
495                            false,
496                            uint!(1),
497                            false,
498                        ),
499                        None,
500                        1,
501                        vec![],
502                    )
503                },
504                VectorDiff::PushBack {
505                    value: SpaceRoom::new_from_summary(
506                        &RoomSummary::new(
507                            child_space_id_2.to_owned(),
508                            JoinRuleSummary::Public,
509                            false,
510                            uint!(1),
511                            false,
512                        ),
513                        None,
514                        1,
515                        vec![],
516                    ),
517                }
518            ]
519        );
520    }
521
522    #[async_test]
523    async fn test_room_state_updates() {
524        let server = MatrixMockServer::new().await;
525        let client = server.client_builder().build().await;
526        let space_service = SpaceService::new(client.clone());
527
528        let parent_space_id = room_id!("!parent_space:example.org");
529        let child_room_id_1 = room_id!("!1:example.org");
530        let child_room_id_2 = room_id!("!2:example.org");
531
532        server
533            .mock_get_hierarchy()
534            .ok_with_room_ids(vec![child_room_id_1, child_room_id_2])
535            .mount()
536            .await;
537
538        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
539
540        room_list.paginate().await.unwrap();
541
542        // This space contains 2 rooms
543        assert_eq!(room_list.rooms().first().unwrap().room_id, child_room_id_1);
544        assert_eq!(room_list.rooms().last().unwrap().room_id, child_room_id_2);
545
546        // and we don't know about either of them
547        assert_eq!(room_list.rooms().first().unwrap().state, None);
548        assert_eq!(room_list.rooms().last().unwrap().state, None);
549
550        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
551        pin_mut!(rooms_subscriber);
552        assert_pending!(rooms_subscriber);
553
554        // Joining one of them though
555        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_1)).await;
556
557        // Results in an update being pushed through
558        assert_ready!(rooms_subscriber);
559        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Joined));
560        assert_eq!(room_list.rooms().last().unwrap().state, None);
561
562        // Same for the second one
563        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_2)).await;
564        assert_ready!(rooms_subscriber);
565        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Joined));
566        assert_eq!(room_list.rooms().last().unwrap().state, Some(RoomState::Joined));
567
568        // And when leaving them
569        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_1)).await;
570        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_2)).await;
571        assert_ready!(rooms_subscriber);
572        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Left));
573        assert_eq!(room_list.rooms().last().unwrap().state, Some(RoomState::Left));
574    }
575
576    #[async_test]
577    async fn test_parent_space_updates() {
578        let server = MatrixMockServer::new().await;
579        let client = server.client_builder().build().await;
580        let user_id = client.user_id().unwrap();
581        let space_service = SpaceService::new(client.clone());
582        let factory = EventFactory::new();
583
584        server.mock_room_state_encryption().plain().mount().await;
585
586        let parent_space_id = room_id!("!parent_space:example.org");
587        let child_space_id_1 = room_id!("!1:example.org");
588        let child_space_id_2 = room_id!("!2:example.org");
589
590        // Parent space is unknown to the client and thus not populated yet
591        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
592        assert!(room_list.space().is_none());
593
594        let parent_space_subscriber = room_list.subscribe_to_space_updates();
595        pin_mut!(parent_space_subscriber);
596        assert_pending!(parent_space_subscriber);
597
598        server
599            .mock_get_hierarchy()
600            .ok_with_room_ids_and_children_state(
601                vec![parent_space_id, child_space_id_1, child_space_id_2],
602                vec![(
603                    room_id!("!child:example.org"),
604                    vec![server_name!("matrix-client.example.org")],
605                )],
606            )
607            .mount()
608            .await;
609
610        // Pagination will however fetch and populate it from /hierarchy
611        room_list.paginate().await.unwrap();
612        assert_let!(Some(parent_space) = room_list.space());
613        assert_eq!(parent_space.room_id, parent_space_id);
614
615        // And the subscription is informed about the change
616        assert_next_eq!(parent_space_subscriber, Some(parent_space));
617
618        // If the room is already known to the client then the space parent
619        // is populated directly on creation
620        server
621            .sync_room(
622                &client,
623                JoinedRoomBuilder::new(parent_space_id)
624                    .add_state_event(
625                        factory
626                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
627                            .sender(user_id),
628                    )
629                    .add_state_event(
630                        factory
631                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
632                            .sender(user_id),
633                    ),
634            )
635            .await;
636
637        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
638
639        // The parent space is known to the client and should be populated accordingly
640        assert_let!(Some(parent_space) = room_list.space());
641        assert_eq!(parent_space.children_count, 2);
642    }
643
644    #[async_test]
645    async fn test_parent_space_room_info_update() {
646        let server = MatrixMockServer::new().await;
647        let client = server.client_builder().build().await;
648        let user_id = client.user_id().unwrap();
649        let space_service = SpaceService::new(client.clone());
650        let factory = EventFactory::new();
651
652        server.mock_room_state_encryption().plain().mount().await;
653
654        let parent_space_id = room_id!("!parent_space:example.org");
655
656        server.sync_room(&client, JoinedRoomBuilder::new(parent_space_id)).await;
657
658        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
659        assert_let!(Some(parent_space) = room_list.space());
660
661        // The parent space is known to the client
662        let parent_space_subscriber = room_list.subscribe_to_space_updates();
663        pin_mut!(parent_space_subscriber);
664        assert_pending!(parent_space_subscriber);
665
666        // So any room info changes are automatically published
667        server
668            .sync_room(
669                &client,
670                JoinedRoomBuilder::new(parent_space_id)
671                    .add_state_event(factory.room_topic("New room topic").sender(user_id))
672                    .add_state_event(factory.room_name("New room name").sender(user_id)),
673            )
674            .await;
675
676        let mut updated_parent_space = parent_space.clone();
677        updated_parent_space.topic = Some("New room topic".to_owned());
678        updated_parent_space.name = Some("New room name".to_owned());
679        updated_parent_space.display_name = "New room name".to_owned();
680
681        // And the subscription is informed about the change
682        assert_next_eq!(parent_space_subscriber, Some(updated_parent_space));
683    }
684
685    #[async_test]
686    async fn test_via_retrieval() {
687        let server = MatrixMockServer::new().await;
688        let client = server.client_builder().build().await;
689        let space_service = SpaceService::new(client.clone());
690
691        server.mock_room_state_encryption().plain().mount().await;
692
693        let parent_space_id = room_id!("!parent_space:example.org");
694        let child_space_id_1 = room_id!("!1:example.org");
695        let child_space_id_2 = room_id!("!2:example.org");
696
697        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
698
699        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
700        pin_mut!(rooms_subscriber);
701
702        // When retrieving the parent and children via /hierarchy
703        server
704            .mock_get_hierarchy()
705            .ok_with_room_ids_and_children_state(
706                vec![parent_space_id, child_space_id_1, child_space_id_2],
707                vec![
708                    (child_space_id_1, vec![server_name!("matrix-client.example.org")]),
709                    (child_space_id_2, vec![server_name!("other-matrix-client.example.org")]),
710                ],
711            )
712            .mount()
713            .await;
714
715        room_list.paginate().await.unwrap();
716
717        // The parent `children_state` is used to populate children via params
718        assert_next_eq!(
719            rooms_subscriber,
720            vec![
721                VectorDiff::PushBack {
722                    value: SpaceRoom::new_from_summary(
723                        &RoomSummary::new(
724                            child_space_id_1.to_owned(),
725                            JoinRuleSummary::Public,
726                            false,
727                            uint!(1),
728                            false,
729                        ),
730                        None,
731                        2,
732                        vec![owned_server_name!("matrix-client.example.org")],
733                    )
734                },
735                VectorDiff::PushBack {
736                    value: SpaceRoom::new_from_summary(
737                        &RoomSummary::new(
738                            child_space_id_2.to_owned(),
739                            JoinRuleSummary::Public,
740                            false,
741                            uint!(1),
742                            false,
743                        ),
744                        None,
745                        2,
746                        vec![owned_server_name!("other-matrix-client.example.org")],
747                    ),
748                }
749            ]
750        );
751    }
752
753    #[async_test]
754    async fn test_room_list_sorting() {
755        let mut children_state = HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
756
757        // Rooms not present in the `children_state` should be sorted by their room ID
758        assert_eq!(
759            SpaceRoomList::compare_rooms(
760                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
761                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
762                &children_state,
763            ),
764            Ordering::Less
765        );
766
767        assert_eq!(
768            SpaceRoomList::compare_rooms(
769                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
770                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
771                &children_state,
772            ),
773            Ordering::Greater
774        );
775
776        // Rooms without an order provided through the `children_state` should be
777        // sorted by their `m.space.child` `origin_server_ts`
778        assert_eq!(
779            SpaceRoomList::compare_rooms(
780                &make_space_room(owned_room_id!("!Luana:a.b"), None, Some(1), &mut children_state),
781                &make_space_room(
782                    owned_room_id!("!Marțolea:a.b"),
783                    None,
784                    Some(0),
785                    &mut children_state
786                ),
787                &children_state,
788            ),
789            Ordering::Greater
790        );
791
792        // The `m.space.child` `content.order` field should be used if provided
793        assert_eq!(
794            SpaceRoomList::compare_rooms(
795                &make_space_room(
796                    owned_room_id!("!Joiana:a.b"),
797                    Some("last"),
798                    Some(123),
799                    &mut children_state
800                ),
801                &make_space_room(
802                    owned_room_id!("!Mioara:a.b"),
803                    Some("first"),
804                    Some(234),
805                    &mut children_state
806                ),
807                &children_state,
808            ),
809            Ordering::Greater
810        );
811
812        // The timestamp should be used when the `order` is the same
813        assert_eq!(
814            SpaceRoomList::compare_rooms(
815                &make_space_room(
816                    owned_room_id!("!Joiana:a.b"),
817                    Some("Same pasture"),
818                    Some(1),
819                    &mut children_state
820                ),
821                &make_space_room(
822                    owned_room_id!("!Mioara:a.b"),
823                    Some("Same pasture"),
824                    Some(0),
825                    &mut children_state
826                ),
827                &children_state,
828            ),
829            Ordering::Greater
830        );
831
832        // And the `room_id` should be used when both the `order` and the
833        // `timestamp` are equal
834        assert_eq!(
835            SpaceRoomList::compare_rooms(
836                &make_space_room(
837                    owned_room_id!("!Joiana:a.b"),
838                    Some("same_pasture"),
839                    Some(0),
840                    &mut children_state
841                ),
842                &make_space_room(
843                    owned_room_id!("!Mioara:a.b"),
844                    Some("same_pasture"),
845                    Some(0),
846                    &mut children_state
847                ),
848                &children_state,
849            ),
850            Ordering::Less
851        );
852
853        // Finally, when one of the rooms is missing `children_state` data the
854        // other one should take precedence
855        assert_eq!(
856            SpaceRoomList::compare_rooms(
857                &make_space_room(owned_room_id!("!Viola:a.b"), None, None, &mut children_state),
858                &make_space_room(
859                    owned_room_id!("!Sâmbotina:a.b"),
860                    None,
861                    Some(0),
862                    &mut children_state
863                ),
864                &children_state,
865            ),
866            Ordering::Greater
867        );
868
869        assert_eq!(
870            SpaceRoomList::compare_rooms(
871                &make_space_room(
872                    owned_room_id!("!Sâmbotina:a.b"),
873                    None,
874                    Some(1),
875                    &mut children_state
876                ),
877                &make_space_room(
878                    owned_room_id!("!Dumana:a.b"),
879                    Some("Some pasture"),
880                    Some(1),
881                    &mut children_state
882                ),
883                &children_state,
884            ),
885            Ordering::Less
886        );
887    }
888
889    fn make_space_room(
890        room_id: OwnedRoomId,
891        order: Option<&str>,
892        origin_server_ts: Option<u32>,
893        children_state: &mut HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
894    ) -> SpaceRoom {
895        if let Some(origin_server_ts) = origin_server_ts {
896            children_state.insert(
897                room_id.clone(),
898                hierarchy_space_child_event(&room_id, order, origin_server_ts),
899            );
900        }
901        SpaceRoom {
902            room_id,
903            canonical_alias: None,
904            name: Some("New room name".to_owned()),
905            display_name: "Empty room".to_owned(),
906            topic: None,
907            avatar_url: None,
908            room_type: None,
909            num_joined_members: 0,
910            join_rule: None,
911            world_readable: None,
912            guest_can_join: false,
913            is_direct: None,
914            children_count: 0,
915            state: None,
916            heroes: None,
917            via: vec![],
918        }
919    }
920
921    fn hierarchy_space_child_event(
922        room_id: &RoomId,
923        order: Option<&str>,
924        origin_server_ts: u32,
925    ) -> HierarchySpaceChildEvent {
926        let json = json!({
927            "content": {
928                "order": order.unwrap_or(""),
929                "via": []
930            },
931            "origin_server_ts": origin_server_ts,
932            "sender": "@bob:a.b",
933            "state_key": room_id.to_string(),
934            "type": "m.space.child"
935        });
936
937        from_value::<HierarchySpaceChildEvent>(json).unwrap()
938    }
939}