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()).await;
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    /// Clears the room list back to its initial state so that any new changes
358    /// to the hierarchy will be included the next time [`Self::paginate`] is
359    /// called.
360    ///
361    /// This is useful when you've added or removed children from the space as
362    /// the list is based on a cached state that lives server-side, meaning
363    /// the /hierarchy request needs to be restarted from scratch to pick up
364    /// the changes.
365    pub async fn reset(&self) {
366        let mut pagination_token = self.token.lock().await;
367        *pagination_token = None.into();
368
369        self.rooms.lock().clear();
370        self.children_state.lock().take();
371
372        self.pagination_state.set(SpaceRoomListPaginationState::Idle { end_reached: false });
373    }
374
375    /// Sorts space rooms by various criteria as defined in
376    /// https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
377    fn compare_rooms(
378        a: &SpaceRoom,
379        b: &SpaceRoom,
380        children_state: &HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
381    ) -> Ordering {
382        let a_state = children_state.get(&a.room_id);
383        let b_state = children_state.get(&b.room_id);
384
385        SpaceRoom::compare_rooms(a, b, a_state.map(Into::into), b_state.map(Into::into))
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use std::{cmp::Ordering, collections::HashMap};
392
393    use assert_matches2::{assert_let, assert_matches};
394    use eyeball_im::VectorDiff;
395    use futures_util::pin_mut;
396    use matrix_sdk::{RoomState, test_utils::mocks::MatrixMockServer};
397    use matrix_sdk_test::{
398        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
399    };
400    use ruma::{
401        OwnedRoomId, RoomId,
402        events::space::child::HierarchySpaceChildEvent,
403        owned_room_id, owned_server_name,
404        room::{JoinRuleSummary, RoomSummary},
405        room_id, server_name, uint,
406    };
407    use serde_json::{from_value, json};
408    use stream_assert::{assert_next_eq, assert_next_matches, assert_pending, assert_ready};
409
410    use crate::spaces::{
411        SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState,
412    };
413
414    #[async_test]
415    async fn test_room_list_pagination() {
416        let server = MatrixMockServer::new().await;
417        let client = server.client_builder().build().await;
418        let user_id = client.user_id().unwrap();
419        let space_service = SpaceService::new(client.clone()).await;
420        let factory = EventFactory::new();
421
422        server.mock_room_state_encryption().plain().mount().await;
423
424        let parent_space_id = room_id!("!parent_space:example.org");
425        let child_space_id_1 = room_id!("!1:example.org");
426        let child_space_id_2 = room_id!("!2:example.org");
427
428        server
429            .sync_room(
430                &client,
431                JoinedRoomBuilder::new(parent_space_id)
432                    .add_state_event(
433                        factory
434                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
435                            .sender(user_id),
436                    )
437                    .add_state_event(
438                        factory
439                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
440                            .sender(user_id),
441                    ),
442            )
443            .await;
444
445        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
446
447        // The space parent is known to the client and should be populated accordingly
448        assert_let!(Some(parent_space) = room_list.space());
449        assert_eq!(parent_space.children_count, 2);
450
451        // Start off idle
452        assert_matches!(
453            room_list.pagination_state(),
454            SpaceRoomListPaginationState::Idle { end_reached: false }
455        );
456
457        // without any rooms
458        assert_eq!(room_list.rooms(), vec![]);
459
460        // and with pending subscribers
461
462        let pagination_state_subscriber = room_list.subscribe_to_pagination_state_updates();
463        pin_mut!(pagination_state_subscriber);
464        assert_pending!(pagination_state_subscriber);
465
466        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
467        pin_mut!(rooms_subscriber);
468        assert_pending!(rooms_subscriber);
469
470        // Paginating the room list
471        server
472            .mock_get_hierarchy()
473            .ok_with_room_ids_and_children_state(
474                vec![child_space_id_1, child_space_id_2],
475                vec![(room_id!("!child:example.org"), vec![])],
476            )
477            .mount()
478            .await;
479
480        room_list.paginate().await.unwrap();
481
482        // informs that the pagination reached the end
483        assert_next_matches!(
484            pagination_state_subscriber,
485            SpaceRoomListPaginationState::Idle { end_reached: true }
486        );
487
488        // and yields results
489        assert_next_eq!(
490            rooms_subscriber,
491            vec![
492                VectorDiff::PushBack {
493                    value: SpaceRoom::new_from_summary(
494                        &RoomSummary::new(
495                            child_space_id_1.to_owned(),
496                            JoinRuleSummary::Public,
497                            false,
498                            uint!(1),
499                            false,
500                        ),
501                        None,
502                        1,
503                        vec![],
504                    )
505                },
506                VectorDiff::PushBack {
507                    value: SpaceRoom::new_from_summary(
508                        &RoomSummary::new(
509                            child_space_id_2.to_owned(),
510                            JoinRuleSummary::Public,
511                            false,
512                            uint!(1),
513                            false,
514                        ),
515                        None,
516                        1,
517                        vec![],
518                    ),
519                }
520            ]
521        );
522    }
523
524    #[async_test]
525    async fn test_room_state_updates() {
526        let server = MatrixMockServer::new().await;
527        let client = server.client_builder().build().await;
528        let space_service = SpaceService::new(client.clone()).await;
529
530        let parent_space_id = room_id!("!parent_space:example.org");
531        let child_room_id_1 = room_id!("!1:example.org");
532        let child_room_id_2 = room_id!("!2:example.org");
533
534        server
535            .mock_get_hierarchy()
536            .ok_with_room_ids(vec![child_room_id_1, child_room_id_2])
537            .mount()
538            .await;
539
540        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
541
542        room_list.paginate().await.unwrap();
543
544        // This space contains 2 rooms
545        assert_eq!(room_list.rooms().first().unwrap().room_id, child_room_id_1);
546        assert_eq!(room_list.rooms().last().unwrap().room_id, child_room_id_2);
547
548        // and we don't know about either of them
549        assert_eq!(room_list.rooms().first().unwrap().state, None);
550        assert_eq!(room_list.rooms().last().unwrap().state, None);
551
552        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
553        pin_mut!(rooms_subscriber);
554        assert_pending!(rooms_subscriber);
555
556        // Joining one of them though
557        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_1)).await;
558
559        // Results in an update being pushed through
560        assert_ready!(rooms_subscriber);
561        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Joined));
562        assert_eq!(room_list.rooms().last().unwrap().state, None);
563
564        // Same for the second one
565        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_2)).await;
566        assert_ready!(rooms_subscriber);
567        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Joined));
568        assert_eq!(room_list.rooms().last().unwrap().state, Some(RoomState::Joined));
569
570        // And when leaving them
571        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_1)).await;
572        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_2)).await;
573        assert_ready!(rooms_subscriber);
574        assert_eq!(room_list.rooms().first().unwrap().state, Some(RoomState::Left));
575        assert_eq!(room_list.rooms().last().unwrap().state, Some(RoomState::Left));
576    }
577
578    #[async_test]
579    async fn test_parent_space_updates() {
580        let server = MatrixMockServer::new().await;
581        let client = server.client_builder().build().await;
582        let user_id = client.user_id().unwrap();
583        let space_service = SpaceService::new(client.clone()).await;
584        let factory = EventFactory::new();
585
586        server.mock_room_state_encryption().plain().mount().await;
587
588        let parent_space_id = room_id!("!parent_space:example.org");
589        let child_space_id_1 = room_id!("!1:example.org");
590        let child_space_id_2 = room_id!("!2:example.org");
591
592        // Parent space is unknown to the client and thus not populated yet
593        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
594        assert!(room_list.space().is_none());
595
596        let parent_space_subscriber = room_list.subscribe_to_space_updates();
597        pin_mut!(parent_space_subscriber);
598        assert_pending!(parent_space_subscriber);
599
600        server
601            .mock_get_hierarchy()
602            .ok_with_room_ids_and_children_state(
603                vec![parent_space_id, child_space_id_1, child_space_id_2],
604                vec![(
605                    room_id!("!child:example.org"),
606                    vec![server_name!("matrix-client.example.org")],
607                )],
608            )
609            .mount()
610            .await;
611
612        // Pagination will however fetch and populate it from /hierarchy
613        room_list.paginate().await.unwrap();
614        assert_let!(Some(parent_space) = room_list.space());
615        assert_eq!(parent_space.room_id, parent_space_id);
616
617        // And the subscription is informed about the change
618        assert_next_eq!(parent_space_subscriber, Some(parent_space));
619
620        // If the room is already known to the client then the space parent
621        // is populated directly on creation
622        server
623            .sync_room(
624                &client,
625                JoinedRoomBuilder::new(parent_space_id)
626                    .add_state_event(
627                        factory
628                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
629                            .sender(user_id),
630                    )
631                    .add_state_event(
632                        factory
633                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
634                            .sender(user_id),
635                    ),
636            )
637            .await;
638
639        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
640
641        // The parent space is known to the client and should be populated accordingly
642        assert_let!(Some(parent_space) = room_list.space());
643        assert_eq!(parent_space.children_count, 2);
644    }
645
646    #[async_test]
647    async fn test_parent_space_room_info_update() {
648        let server = MatrixMockServer::new().await;
649        let client = server.client_builder().build().await;
650        let user_id = client.user_id().unwrap();
651        let space_service = SpaceService::new(client.clone()).await;
652        let factory = EventFactory::new();
653
654        server.mock_room_state_encryption().plain().mount().await;
655
656        let parent_space_id = room_id!("!parent_space:example.org");
657
658        server.sync_room(&client, JoinedRoomBuilder::new(parent_space_id)).await;
659
660        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
661        assert_let!(Some(parent_space) = room_list.space());
662
663        // The parent space is known to the client
664        let parent_space_subscriber = room_list.subscribe_to_space_updates();
665        pin_mut!(parent_space_subscriber);
666        assert_pending!(parent_space_subscriber);
667
668        // So any room info changes are automatically published
669        server
670            .sync_room(
671                &client,
672                JoinedRoomBuilder::new(parent_space_id)
673                    .add_state_event(factory.room_topic("New room topic").sender(user_id))
674                    .add_state_event(factory.room_name("New room name").sender(user_id)),
675            )
676            .await;
677
678        let mut updated_parent_space = parent_space.clone();
679        updated_parent_space.topic = Some("New room topic".to_owned());
680        updated_parent_space.name = Some("New room name".to_owned());
681        updated_parent_space.display_name = "New room name".to_owned();
682
683        // And the subscription is informed about the change
684        assert_next_eq!(parent_space_subscriber, Some(updated_parent_space));
685    }
686
687    #[async_test]
688    async fn test_via_retrieval() {
689        let server = MatrixMockServer::new().await;
690        let client = server.client_builder().build().await;
691        let space_service = SpaceService::new(client.clone()).await;
692
693        server.mock_room_state_encryption().plain().mount().await;
694
695        let parent_space_id = room_id!("!parent_space:example.org");
696        let child_space_id_1 = room_id!("!1:example.org");
697        let child_space_id_2 = room_id!("!2:example.org");
698
699        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
700
701        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates();
702        pin_mut!(rooms_subscriber);
703
704        // When retrieving the parent and children via /hierarchy
705        server
706            .mock_get_hierarchy()
707            .ok_with_room_ids_and_children_state(
708                vec![parent_space_id, child_space_id_1, child_space_id_2],
709                vec![
710                    (child_space_id_1, vec![server_name!("matrix-client.example.org")]),
711                    (child_space_id_2, vec![server_name!("other-matrix-client.example.org")]),
712                ],
713            )
714            .mount()
715            .await;
716
717        room_list.paginate().await.unwrap();
718
719        // The parent `children_state` is used to populate children via params
720        assert_next_eq!(
721            rooms_subscriber,
722            vec![
723                VectorDiff::PushBack {
724                    value: SpaceRoom::new_from_summary(
725                        &RoomSummary::new(
726                            child_space_id_1.to_owned(),
727                            JoinRuleSummary::Public,
728                            false,
729                            uint!(1),
730                            false,
731                        ),
732                        None,
733                        2,
734                        vec![owned_server_name!("matrix-client.example.org")],
735                    )
736                },
737                VectorDiff::PushBack {
738                    value: SpaceRoom::new_from_summary(
739                        &RoomSummary::new(
740                            child_space_id_2.to_owned(),
741                            JoinRuleSummary::Public,
742                            false,
743                            uint!(1),
744                            false,
745                        ),
746                        None,
747                        2,
748                        vec![owned_server_name!("other-matrix-client.example.org")],
749                    ),
750                }
751            ]
752        );
753    }
754
755    #[async_test]
756    async fn test_room_list_sorting() {
757        let mut children_state = HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
758
759        // Rooms not present in the `children_state` should be sorted by their room ID
760        assert_eq!(
761            SpaceRoomList::compare_rooms(
762                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
763                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
764                &children_state,
765            ),
766            Ordering::Less
767        );
768
769        assert_eq!(
770            SpaceRoomList::compare_rooms(
771                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
772                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
773                &children_state,
774            ),
775            Ordering::Greater
776        );
777
778        // Rooms without an order provided through the `children_state` should be
779        // sorted by their `m.space.child` `origin_server_ts`
780        assert_eq!(
781            SpaceRoomList::compare_rooms(
782                &make_space_room(owned_room_id!("!Luana:a.b"), None, Some(1), &mut children_state),
783                &make_space_room(
784                    owned_room_id!("!Marțolea:a.b"),
785                    None,
786                    Some(0),
787                    &mut children_state
788                ),
789                &children_state,
790            ),
791            Ordering::Greater
792        );
793
794        // The `m.space.child` `content.order` field should be used if provided
795        assert_eq!(
796            SpaceRoomList::compare_rooms(
797                &make_space_room(
798                    owned_room_id!("!Joiana:a.b"),
799                    Some("last"),
800                    Some(123),
801                    &mut children_state
802                ),
803                &make_space_room(
804                    owned_room_id!("!Mioara:a.b"),
805                    Some("first"),
806                    Some(234),
807                    &mut children_state
808                ),
809                &children_state,
810            ),
811            Ordering::Greater
812        );
813
814        // The timestamp should be used when the `order` is the same
815        assert_eq!(
816            SpaceRoomList::compare_rooms(
817                &make_space_room(
818                    owned_room_id!("!Joiana:a.b"),
819                    Some("Same pasture"),
820                    Some(1),
821                    &mut children_state
822                ),
823                &make_space_room(
824                    owned_room_id!("!Mioara:a.b"),
825                    Some("Same pasture"),
826                    Some(0),
827                    &mut children_state
828                ),
829                &children_state,
830            ),
831            Ordering::Greater
832        );
833
834        // And the `room_id` should be used when both the `order` and the
835        // `timestamp` are equal
836        assert_eq!(
837            SpaceRoomList::compare_rooms(
838                &make_space_room(
839                    owned_room_id!("!Joiana:a.b"),
840                    Some("same_pasture"),
841                    Some(0),
842                    &mut children_state
843                ),
844                &make_space_room(
845                    owned_room_id!("!Mioara:a.b"),
846                    Some("same_pasture"),
847                    Some(0),
848                    &mut children_state
849                ),
850                &children_state,
851            ),
852            Ordering::Less
853        );
854
855        // When one of the rooms is missing `children_state` data the other one
856        // should take precedence
857        assert_eq!(
858            SpaceRoomList::compare_rooms(
859                &make_space_room(owned_room_id!("!Viola:a.b"), None, None, &mut children_state),
860                &make_space_room(
861                    owned_room_id!("!Sâmbotina:a.b"),
862                    None,
863                    Some(0),
864                    &mut children_state
865                ),
866                &children_state,
867            ),
868            Ordering::Greater
869        );
870
871        // If the `order` is missing from one of the rooms but `children_state`
872        // is present then the other one should come first
873        assert_eq!(
874            SpaceRoomList::compare_rooms(
875                &make_space_room(
876                    owned_room_id!("!Sâmbotina:a.b"),
877                    None,
878                    Some(1),
879                    &mut children_state
880                ),
881                &make_space_room(
882                    owned_room_id!("!Dumana:a.b"),
883                    Some("Some pasture"),
884                    Some(1),
885                    &mut children_state
886                ),
887                &children_state,
888            ),
889            Ordering::Greater
890        );
891    }
892
893    #[async_test]
894    async fn test_reset() {
895        let server = MatrixMockServer::new().await;
896        let client = server.client_builder().build().await;
897        let space_service = SpaceService::new(client.clone()).await;
898
899        let parent_space_id = room_id!("!parent_space:example.org");
900        let child_space_id_1 = room_id!("!1:example.org");
901
902        server
903            .mock_get_hierarchy()
904            .ok_with_room_ids(vec![child_space_id_1])
905            .expect(2)
906            .mount()
907            .await;
908
909        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
910
911        room_list.paginate().await.unwrap();
912
913        // This space contains 1 room
914        assert_eq!(room_list.rooms().len(), 1);
915
916        // Resetting the room list
917        room_list.reset().await;
918
919        // Clears the rooms and pagination token
920        assert_eq!(room_list.rooms().len(), 0);
921        assert_matches!(
922            room_list.pagination_state(),
923            SpaceRoomListPaginationState::Idle { end_reached: false }
924        );
925
926        // Allows paginating again
927        room_list.paginate().await.unwrap();
928        assert_eq!(room_list.rooms().len(), 1);
929    }
930
931    fn make_space_room(
932        room_id: OwnedRoomId,
933        order: Option<&str>,
934        origin_server_ts: Option<u32>,
935        children_state: &mut HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
936    ) -> SpaceRoom {
937        if let Some(origin_server_ts) = origin_server_ts {
938            children_state.insert(
939                room_id.clone(),
940                hierarchy_space_child_event(&room_id, order, origin_server_ts),
941            );
942        }
943        SpaceRoom {
944            room_id,
945            canonical_alias: None,
946            name: Some("New room name".to_owned()),
947            display_name: "Empty room".to_owned(),
948            topic: None,
949            avatar_url: None,
950            room_type: None,
951            num_joined_members: 0,
952            join_rule: None,
953            world_readable: None,
954            guest_can_join: false,
955            is_direct: None,
956            children_count: 0,
957            state: None,
958            heroes: None,
959            via: vec![],
960        }
961    }
962
963    fn hierarchy_space_child_event(
964        room_id: &RoomId,
965        order: Option<&str>,
966        origin_server_ts: u32,
967    ) -> HierarchySpaceChildEvent {
968        let mut json = json!({
969            "content": {
970                "via": []
971            },
972            "origin_server_ts": origin_server_ts,
973            "sender": "@bob:a.b",
974            "state_key": room_id.to_string(),
975            "type": "m.space.child"
976        });
977
978        if let Some(order) = order {
979            json["content"]["order"] = json!(order);
980        }
981
982        from_value::<HierarchySpaceChildEvent>(json).unwrap()
983    }
984}