Skip to main content

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