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, pin::pin, sync::Arc};
16
17use eyeball::{ObservableWriteGuard, SharedObservable, Subscriber};
18use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
19use futures_util::future::join_all;
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 that belong
44/// 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 and
51/// 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().await.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().await;
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<AsyncMutex<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(AsyncMutex::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_infinite_task("space_room_list::room_updates", {
134                let client = client.clone();
135                let rooms = rooms.clone();
136
137                async move {
138                    let mut all_room_updates_receiver = pin!(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().await;
148
149                                for updated_room_id in updates.iter_all_room_ids() {
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                                            .await,
163                                        );
164                                    }
165                                }
166                            }
167                            Err(err) => {
168                                error!("error when listening to room updates: {err}");
169                            }
170                        }
171                    }
172                }
173            })
174            .abort_on_drop();
175
176        let space_observable = SharedObservable::new(None);
177
178        let (space_room, space_update_handle) = if let Some(parent) = client.get_room(&space_id) {
179            let children_count = parent
180                .get_state_events_static::<SpaceChildEventContent>()
181                .await
182                .map_or(0, |c| c.len() as u64);
183
184            let mut subscriber = parent.subscribe_info();
185            let space_update_handle = client
186                .task_monitor()
187                .spawn_infinite_task("space_room_list::space_update", {
188                    let client = client.clone();
189                    let space_id = space_id.clone();
190                    let space_observable = space_observable.clone();
191                    async move {
192                        while subscriber.next().await.is_some() {
193                            if let Some(room) = client.get_room(&space_id) {
194                                space_observable.set(Some(
195                                    SpaceRoom::new_from_known(&room, children_count).await,
196                                ));
197                            }
198                        }
199                    }
200                })
201                .abort_on_drop();
202
203            (
204                Some(SpaceRoom::new_from_known(&parent, children_count).await),
205                Some(space_update_handle),
206            )
207        } else {
208            (None, None)
209        };
210
211        space_observable.set(space_room);
212
213        Self {
214            client,
215            space_id,
216            space: space_observable,
217            children_state: Mutex::new(None),
218            token: AsyncMutex::new(None.into()),
219            pagination_state: SharedObservable::new(SpaceRoomListPaginationState::Idle {
220                end_reached: false,
221            }),
222            rooms,
223            _space_update_handle: space_update_handle,
224            _room_update_handle: room_update_handle,
225        }
226    }
227
228    /// Returns the space of the room list if known.
229    pub fn space(&self) -> Option<SpaceRoom> {
230        self.space.get()
231    }
232
233    /// Subscribe to space updates.
234    pub fn subscribe_to_space_updates(&self) -> Subscriber<Option<SpaceRoom>> {
235        self.space.subscribe()
236    }
237
238    /// Returns if the room list is currently paginating or not.
239    pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
240        self.pagination_state.get()
241    }
242
243    /// Subscribe to pagination updates.
244    pub fn subscribe_to_pagination_state_updates(
245        &self,
246    ) -> Subscriber<SpaceRoomListPaginationState> {
247        self.pagination_state.subscribe()
248    }
249
250    /// Return the current list of rooms.
251    pub async fn rooms(&self) -> Vec<SpaceRoom> {
252        self.rooms.lock().await.iter().cloned().collect_vec()
253    }
254
255    /// Subscribes to room list updates.
256    pub async fn subscribe_to_room_updates(
257        &self,
258    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
259        self.rooms.lock().await.subscribe().into_values_and_batched_stream()
260    }
261
262    /// Ask the list to retrieve the next page if the end hasn't been reached
263    /// yet. Otherwise it no-ops.
264    pub async fn paginate(&self) -> Result<(), Error> {
265        {
266            let mut pagination_state = self.pagination_state.write();
267
268            match *pagination_state {
269                SpaceRoomListPaginationState::Idle { end_reached } if end_reached => {
270                    return Ok(());
271                }
272                SpaceRoomListPaginationState::Loading => {
273                    return Ok(());
274                }
275                _ => {}
276            }
277
278            ObservableWriteGuard::set(&mut pagination_state, SpaceRoomListPaginationState::Loading);
279        }
280
281        let mut request = get_hierarchy::v1::Request::new(self.space_id.clone());
282        request.max_depth = Some(uint!(1)); // We only want the immediate children of the space
283
284        let mut pagination_token = self.token.lock().await;
285
286        if let PaginationToken::HasMore(ref token) = *pagination_token {
287            request.from = Some(token.clone());
288        }
289
290        match self.client.send(request).await {
291            Ok(result) => {
292                *pagination_token = match &result.next_batch {
293                    Some(val) => PaginationToken::HasMore(val.clone()),
294                    None => PaginationToken::HitEnd,
295                };
296
297                // The space is part of the /hierarchy response. Partition the
298                // room array so we can use its details but also filter it out
299                // of the room list
300                let (space, children): (Vec<_>, Vec<_>) =
301                    result.rooms.into_iter().partition(|f| f.summary.room_id == self.space_id);
302
303                if let Some(room) = space.first() {
304                    let mut children_state =
305                        HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
306                    for child_state in &room.children_state {
307                        match child_state.deserialize() {
308                            Ok(child) => {
309                                children_state.insert(child.state_key.clone(), child);
310                            }
311                            Err(error) => {
312                                warn!("Failed deserializing space child event: {error}");
313                            }
314                        }
315                    }
316                    *self.children_state.lock() = Some(children_state);
317
318                    let space_is_none = self.space.read().is_none();
319                    if space_is_none {
320                        let space_room = SpaceRoom::new_from_summary(
321                            &room.summary,
322                            self.client.get_room(&room.summary.room_id),
323                            room.children_state.len() as u64,
324                            vec![],
325                            false,
326                        )
327                        .await;
328                        let mut space = self.space.write();
329                        ObservableWriteGuard::set(&mut space, Some(space_room));
330                    }
331                }
332
333                let children_state = (*self.children_state.lock()).clone().unwrap_or_default();
334                let mut rooms = self.rooms.lock().await;
335
336                join_all(children.into_iter().map(|room| {
337                    let children_state = children_state.clone();
338                    async move {
339                        let child_state = children_state.get(&room.summary.room_id);
340                        let via =
341                            child_state.map(|state| state.content.via.clone()).unwrap_or_default();
342                        let suggested =
343                            child_state.map(|state| state.content.suggested).unwrap_or(false);
344                        SpaceRoom::new_from_summary(
345                            &room.summary,
346                            self.client.get_room(&room.summary.room_id),
347                            room.children_state.len() as u64,
348                            via,
349                            suggested,
350                        )
351                        .await
352                    }
353                }))
354                .await
355                .into_iter()
356                .sorted_by(|a, b| Self::compare_rooms(a, b, &children_state))
357                .for_each(|room| rooms.push_back(room));
358
359                self.pagination_state.set(SpaceRoomListPaginationState::Idle {
360                    end_reached: result.next_batch.is_none(),
361                });
362
363                Ok(())
364            }
365            Err(err) => {
366                self.pagination_state
367                    .set(SpaceRoomListPaginationState::Idle { end_reached: false });
368                Err(err.into())
369            }
370        }
371    }
372
373    /// Clears the room list back to its initial state so that any new changes
374    /// to the hierarchy will be included the next time [`Self::paginate`] is
375    /// called.
376    ///
377    /// This is useful when you've added or removed children from the space as
378    /// the list is based on a cached state that lives server-side, meaning the
379    /// /hierarchy request needs to be restarted from scratch to pick up the
380    /// changes.
381    pub async fn reset(&self) {
382        let mut pagination_token = self.token.lock().await;
383        *pagination_token = None.into();
384
385        self.rooms.lock().await.clear();
386        self.children_state.lock().take();
387
388        self.pagination_state.set(SpaceRoomListPaginationState::Idle { end_reached: false });
389    }
390
391    /// Sorts space rooms by various criteria as defined in
392    /// https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
393    fn compare_rooms(
394        a: &SpaceRoom,
395        b: &SpaceRoom,
396        children_state: &HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
397    ) -> Ordering {
398        let a_state = children_state.get(&a.room_id);
399        let b_state = children_state.get(&b.room_id);
400
401        SpaceRoom::compare_rooms(
402            (&a.room_id, a_state.map(Into::into).as_ref()),
403            (&b.room_id, b_state.map(Into::into).as_ref()),
404        )
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use std::{assert_matches, cmp::Ordering, collections::HashMap, pin::pin};
411
412    use eyeball_im::VectorDiff;
413    use matrix_sdk::{RoomState, test_utils::mocks::MatrixMockServer};
414    use matrix_sdk_test::{
415        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
416    };
417    use ruma::{
418        MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId,
419        events::space::child::HierarchySpaceChildEvent,
420        owned_room_id, owned_server_name,
421        room::{JoinRuleSummary, RoomSummary},
422        room_id, server_name, uint,
423    };
424    use serde_json::{from_value, json};
425    use strass::assert_let;
426    use stream_assert::{assert_next_eq, assert_next_matches, assert_pending, assert_ready};
427
428    use crate::spaces::{
429        SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState,
430    };
431
432    #[async_test]
433    async fn test_room_list_pagination() {
434        let server = MatrixMockServer::new().await;
435        let client = server.client_builder().build().await;
436        let user_id = client.user_id().unwrap();
437        let space_service = SpaceService::new(client.clone()).await;
438        let factory = EventFactory::new();
439
440        server.mock_room_state_encryption().plain().mount().await;
441
442        let parent_space_id = room_id!("!parent_space:example.org");
443        let child_space_id_1 = room_id!("!1:example.org");
444        let child_space_id_2 = room_id!("!2:example.org");
445
446        server
447            .sync_room(
448                &client,
449                JoinedRoomBuilder::new(parent_space_id)
450                    .add_state_event(
451                        factory
452                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
453                            .sender(user_id),
454                    )
455                    .add_state_event(
456                        factory
457                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
458                            .sender(user_id),
459                    ),
460            )
461            .await;
462
463        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
464
465        // The space parent is known to the client and should be populated
466        // accordingly
467        assert_let!(Some(parent_space) = room_list.space());
468        assert_eq!(parent_space.children_count, 2);
469
470        // Start off idle
471        assert_matches!(
472            room_list.pagination_state(),
473            SpaceRoomListPaginationState::Idle { end_reached: false }
474        );
475
476        // without any rooms
477        assert_eq!(room_list.rooms().await, vec![]);
478
479        // and with pending subscribers
480
481        let pagination_state_subscriber = room_list.subscribe_to_pagination_state_updates();
482        let mut pagination_state_subscriber = pin!(pagination_state_subscriber);
483        assert_pending!(pagination_state_subscriber);
484
485        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
486        let mut rooms_subscriber = pin!(rooms_subscriber);
487        assert_pending!(rooms_subscriber);
488
489        // Paginating the room list
490        server
491            .mock_get_hierarchy()
492            .ok_with_room_ids_and_children_state(
493                vec![child_space_id_1, child_space_id_2],
494                vec![(room_id!("!child:example.org"), vec![])],
495            )
496            .mount()
497            .await;
498
499        room_list.paginate().await.unwrap();
500
501        // informs that the pagination reached the end
502        assert_next_matches!(
503            pagination_state_subscriber,
504            SpaceRoomListPaginationState::Idle { end_reached: true }
505        );
506
507        // and yields results
508        assert_next_eq!(
509            rooms_subscriber,
510            vec![
511                VectorDiff::PushBack {
512                    value: SpaceRoom::new_from_summary(
513                        &RoomSummary::new(
514                            child_space_id_1.to_owned(),
515                            JoinRuleSummary::Public,
516                            false,
517                            uint!(1),
518                            false,
519                        ),
520                        None,
521                        1,
522                        vec![],
523                        false,
524                    )
525                    .await
526                },
527                VectorDiff::PushBack {
528                    value: SpaceRoom::new_from_summary(
529                        &RoomSummary::new(
530                            child_space_id_2.to_owned(),
531                            JoinRuleSummary::Public,
532                            false,
533                            uint!(1),
534                            false,
535                        ),
536                        None,
537                        1,
538                        vec![],
539                        false,
540                    )
541                    .await,
542                }
543            ]
544        );
545    }
546
547    #[async_test]
548    async fn test_room_state_updates() {
549        let server = MatrixMockServer::new().await;
550        let client = server.client_builder().build().await;
551        let space_service = SpaceService::new(client.clone()).await;
552
553        let parent_space_id = room_id!("!parent_space:example.org");
554        let child_room_id_1 = room_id!("!1:example.org");
555        let child_room_id_2 = room_id!("!2:example.org");
556
557        server
558            .mock_get_hierarchy()
559            .ok_with_room_ids(vec![child_room_id_1, child_room_id_2])
560            .mount()
561            .await;
562
563        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
564
565        room_list.paginate().await.unwrap();
566
567        // This space contains 2 rooms
568        assert_eq!(room_list.rooms().await.first().unwrap().room_id, child_room_id_1);
569        assert_eq!(room_list.rooms().await.last().unwrap().room_id, child_room_id_2);
570
571        // and we don't know about either of them
572        assert_eq!(room_list.rooms().await.first().unwrap().state, None);
573        assert_eq!(room_list.rooms().await.last().unwrap().state, None);
574
575        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
576        let mut rooms_subscriber = pin!(rooms_subscriber);
577        assert_pending!(rooms_subscriber);
578
579        // Joining one of them though
580        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_1)).await;
581
582        // Results in an update being pushed through
583        assert_ready!(rooms_subscriber);
584        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Joined));
585        assert_eq!(room_list.rooms().await.last().unwrap().state, None);
586
587        // Same for the second one
588        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_2)).await;
589        assert_ready!(rooms_subscriber);
590        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Joined));
591        assert_eq!(room_list.rooms().await.last().unwrap().state, Some(RoomState::Joined));
592
593        // And when leaving them
594        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_1)).await;
595        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_2)).await;
596        assert_ready!(rooms_subscriber);
597        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Left));
598        assert_eq!(room_list.rooms().await.last().unwrap().state, Some(RoomState::Left));
599    }
600
601    #[async_test]
602    async fn test_parent_space_updates() {
603        let server = MatrixMockServer::new().await;
604        let client = server.client_builder().build().await;
605        let user_id = client.user_id().unwrap();
606        let space_service = SpaceService::new(client.clone()).await;
607        let factory = EventFactory::new();
608
609        server.mock_room_state_encryption().plain().mount().await;
610
611        let parent_space_id = room_id!("!parent_space:example.org");
612        let child_space_id_1 = room_id!("!1:example.org");
613        let child_space_id_2 = room_id!("!2:example.org");
614
615        // Parent space is unknown to the client and thus not populated yet
616        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
617        assert!(room_list.space().is_none());
618
619        let parent_space_subscriber = room_list.subscribe_to_space_updates();
620        let mut parent_space_subscriber = pin!(parent_space_subscriber);
621        assert_pending!(parent_space_subscriber);
622
623        server
624            .mock_get_hierarchy()
625            .ok_with_room_ids_and_children_state(
626                vec![parent_space_id, child_space_id_1, child_space_id_2],
627                vec![(
628                    room_id!("!child:example.org"),
629                    vec![server_name!("matrix-client.example.org")],
630                )],
631            )
632            .mount()
633            .await;
634
635        // Pagination will however fetch and populate it from /hierarchy
636        room_list.paginate().await.unwrap();
637        assert_let!(Some(parent_space) = room_list.space());
638        assert_eq!(parent_space.room_id, parent_space_id);
639
640        // And the subscription is informed about the change
641        assert_next_eq!(parent_space_subscriber, Some(parent_space));
642
643        // If the room is already known to the client then the space parent is
644        // populated directly on creation
645        server
646            .sync_room(
647                &client,
648                JoinedRoomBuilder::new(parent_space_id)
649                    .add_state_event(
650                        factory
651                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
652                            .sender(user_id),
653                    )
654                    .add_state_event(
655                        factory
656                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
657                            .sender(user_id),
658                    ),
659            )
660            .await;
661
662        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
663
664        // The parent space is known to the client and should be populated
665        // accordingly
666        assert_let!(Some(parent_space) = room_list.space());
667        assert_eq!(parent_space.children_count, 2);
668    }
669
670    #[async_test]
671    async fn test_parent_space_room_info_update() {
672        let server = MatrixMockServer::new().await;
673        let client = server.client_builder().build().await;
674        let user_id = client.user_id().unwrap();
675        let space_service = SpaceService::new(client.clone()).await;
676        let factory = EventFactory::new();
677
678        server.mock_room_state_encryption().plain().mount().await;
679
680        let parent_space_id = room_id!("!parent_space:example.org");
681
682        server.sync_room(&client, JoinedRoomBuilder::new(parent_space_id)).await;
683
684        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
685        assert_let!(Some(parent_space) = room_list.space());
686
687        // The parent space is known to the client
688        let parent_space_subscriber = room_list.subscribe_to_space_updates();
689        let mut parent_space_subscriber = pin!(parent_space_subscriber);
690        assert_pending!(parent_space_subscriber);
691
692        // So any room info changes are automatically published
693        server
694            .sync_room(
695                &client,
696                JoinedRoomBuilder::new(parent_space_id)
697                    .add_state_event(factory.room_topic("New room topic").sender(user_id))
698                    .add_state_event(factory.room_name("New room name").sender(user_id)),
699            )
700            .await;
701
702        let mut updated_parent_space = parent_space.clone();
703        updated_parent_space.topic = Some("New room topic".to_owned());
704        updated_parent_space.name = Some("New room name".to_owned());
705        updated_parent_space.display_name = "New room name".to_owned();
706
707        // And the subscription is informed about the change
708        assert_next_eq!(parent_space_subscriber, Some(updated_parent_space));
709    }
710
711    #[async_test]
712    async fn test_via_retrieval() {
713        let server = MatrixMockServer::new().await;
714        let client = server.client_builder().build().await;
715        let space_service = SpaceService::new(client.clone()).await;
716
717        server.mock_room_state_encryption().plain().mount().await;
718
719        let parent_space_id = room_id!("!parent_space:example.org");
720        let child_space_id_1 = room_id!("!1:example.org");
721        let child_space_id_2 = room_id!("!2:example.org");
722
723        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
724
725        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
726        let mut rooms_subscriber = pin!(rooms_subscriber);
727
728        // When retrieving the parent and children via /hierarchy
729        server
730            .mock_get_hierarchy()
731            .ok_with_room_ids_and_children_state(
732                vec![parent_space_id, child_space_id_1, child_space_id_2],
733                vec![
734                    (child_space_id_1, vec![server_name!("matrix-client.example.org")]),
735                    (child_space_id_2, vec![server_name!("other-matrix-client.example.org")]),
736                ],
737            )
738            .mount()
739            .await;
740
741        room_list.paginate().await.unwrap();
742
743        // The parent `children_state` is used to populate children via params
744        assert_next_eq!(
745            rooms_subscriber,
746            vec![
747                VectorDiff::PushBack {
748                    value: SpaceRoom::new_from_summary(
749                        &RoomSummary::new(
750                            child_space_id_1.to_owned(),
751                            JoinRuleSummary::Public,
752                            false,
753                            uint!(1),
754                            false,
755                        ),
756                        None,
757                        2,
758                        vec![owned_server_name!("matrix-client.example.org")],
759                        false,
760                    )
761                    .await
762                },
763                VectorDiff::PushBack {
764                    value: SpaceRoom::new_from_summary(
765                        &RoomSummary::new(
766                            child_space_id_2.to_owned(),
767                            JoinRuleSummary::Public,
768                            false,
769                            uint!(1),
770                            false,
771                        ),
772                        None,
773                        2,
774                        vec![owned_server_name!("other-matrix-client.example.org")],
775                        false,
776                    )
777                    .await,
778                }
779            ]
780        );
781    }
782
783    #[async_test]
784    async fn test_suggested_field() {
785        let server = MatrixMockServer::new().await;
786        let client = server.client_builder().build().await;
787        let space_service = SpaceService::new(client.clone()).await;
788
789        server.mock_room_state_encryption().plain().mount().await;
790
791        let parent_space_id = room_id!("!parent_space:example.org");
792        let suggested_child = room_id!("!suggested:example.org");
793        let not_suggested_child = room_id!("!not_suggested:example.org");
794
795        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
796
797        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
798        let mut rooms_subscriber = pin!(rooms_subscriber);
799
800        // Mock a /hierarchy response where one child is suggested and the other
801        // is not.
802        let children_state = vec![
803            json!({
804                "type": "m.space.child",
805                "state_key": suggested_child,
806                "content": { "via": ["example.org"], "suggested": true },
807                "sender": "@admin:example.org",
808                "origin_server_ts": MilliSecondsSinceUnixEpoch::now()
809            }),
810            json!({
811                "type": "m.space.child",
812                "state_key": not_suggested_child,
813                "content": { "via": ["example.org"], "suggested": false },
814                "sender": "@admin:example.org",
815                "origin_server_ts": MilliSecondsSinceUnixEpoch::now()
816            }),
817        ];
818
819        server
820            .mock_get_hierarchy()
821            .ok_json(json!({
822                "rooms": [
823                    {
824                        "room_id": parent_space_id,
825                        "num_joined_members": 1,
826                        "world_readable": false,
827                        "guest_can_join": false,
828                        "children_state": children_state
829                    },
830                    {
831                        "room_id": suggested_child,
832                        "num_joined_members": 5,
833                        "world_readable": false,
834                        "guest_can_join": false,
835                        "children_state": []
836                    },
837                    {
838                        "room_id": not_suggested_child,
839                        "num_joined_members": 3,
840                        "world_readable": false,
841                        "guest_can_join": false,
842                        "children_state": []
843                    },
844                ]
845            }))
846            .mount()
847            .await;
848
849        room_list.paginate().await.unwrap();
850
851        let rooms_diff = assert_next_matches!(rooms_subscriber, diff => diff);
852        assert_eq!(rooms_diff.len(), 2);
853
854        // Collect the rooms into a map for easier assertion.
855        let mut rooms_by_id = HashMap::new();
856        for diff in rooms_diff {
857            if let VectorDiff::PushBack { value } = diff {
858                rooms_by_id.insert(value.room_id.clone(), value);
859            } else {
860                panic!("Expected PushBack, got {:?}", diff);
861            }
862        }
863
864        // The child with "suggested": true should have suggested == true.
865        let suggested_room = rooms_by_id.get(suggested_child).expect("suggested child not found");
866        assert!(
867            suggested_room.suggested,
868            "Room with 'suggested: true' should have suggested == true"
869        );
870
871        // The child with "suggested": false should have suggested == false.
872        let not_suggested_room =
873            rooms_by_id.get(not_suggested_child).expect("not-suggested child not found");
874        assert!(
875            !not_suggested_room.suggested,
876            "Room with 'suggested: false' should have suggested == false"
877        );
878    }
879
880    #[async_test]
881    async fn test_room_list_sorting() {
882        let mut children_state = HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
883
884        // Rooms not present in the `children_state` should be sorted by their
885        // room ID
886        assert_eq!(
887            SpaceRoomList::compare_rooms(
888                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
889                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
890                &children_state,
891            ),
892            Ordering::Less
893        );
894
895        assert_eq!(
896            SpaceRoomList::compare_rooms(
897                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
898                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
899                &children_state,
900            ),
901            Ordering::Greater
902        );
903
904        // Rooms without an order provided through the `children_state` should
905        // be sorted by their `m.space.child` `origin_server_ts`
906        assert_eq!(
907            SpaceRoomList::compare_rooms(
908                &make_space_room(owned_room_id!("!Luana:a.b"), None, Some(1), &mut children_state),
909                &make_space_room(
910                    owned_room_id!("!Marțolea:a.b"),
911                    None,
912                    Some(0),
913                    &mut children_state
914                ),
915                &children_state,
916            ),
917            Ordering::Greater
918        );
919
920        // The `m.space.child` `content.order` field should be used if provided
921        assert_eq!(
922            SpaceRoomList::compare_rooms(
923                &make_space_room(
924                    owned_room_id!("!Joiana:a.b"),
925                    Some("last"),
926                    Some(123),
927                    &mut children_state
928                ),
929                &make_space_room(
930                    owned_room_id!("!Mioara:a.b"),
931                    Some("first"),
932                    Some(234),
933                    &mut children_state
934                ),
935                &children_state,
936            ),
937            Ordering::Greater
938        );
939
940        // The timestamp should be used when the `order` is the same
941        assert_eq!(
942            SpaceRoomList::compare_rooms(
943                &make_space_room(
944                    owned_room_id!("!Joiana:a.b"),
945                    Some("Same pasture"),
946                    Some(1),
947                    &mut children_state
948                ),
949                &make_space_room(
950                    owned_room_id!("!Mioara:a.b"),
951                    Some("Same pasture"),
952                    Some(0),
953                    &mut children_state
954                ),
955                &children_state,
956            ),
957            Ordering::Greater
958        );
959
960        // And the `room_id` should be used when both the `order` and the
961        // `timestamp` are equal
962        assert_eq!(
963            SpaceRoomList::compare_rooms(
964                &make_space_room(
965                    owned_room_id!("!Joiana:a.b"),
966                    Some("same_pasture"),
967                    Some(0),
968                    &mut children_state
969                ),
970                &make_space_room(
971                    owned_room_id!("!Mioara:a.b"),
972                    Some("same_pasture"),
973                    Some(0),
974                    &mut children_state
975                ),
976                &children_state,
977            ),
978            Ordering::Less
979        );
980
981        // When one of the rooms is missing `children_state` data the other one
982        // should take precedence
983        assert_eq!(
984            SpaceRoomList::compare_rooms(
985                &make_space_room(owned_room_id!("!Viola:a.b"), None, None, &mut children_state),
986                &make_space_room(
987                    owned_room_id!("!Sâmbotina:a.b"),
988                    None,
989                    Some(0),
990                    &mut children_state
991                ),
992                &children_state,
993            ),
994            Ordering::Greater
995        );
996
997        // If the `order` is missing from one of the rooms but `children_state`
998        // is present then the other one should come first
999        assert_eq!(
1000            SpaceRoomList::compare_rooms(
1001                &make_space_room(
1002                    owned_room_id!("!Sâmbotina:a.b"),
1003                    None,
1004                    Some(1),
1005                    &mut children_state
1006                ),
1007                &make_space_room(
1008                    owned_room_id!("!Dumana:a.b"),
1009                    Some("Some pasture"),
1010                    Some(1),
1011                    &mut children_state
1012                ),
1013                &children_state,
1014            ),
1015            Ordering::Greater
1016        );
1017    }
1018
1019    #[async_test]
1020    async fn test_reset() {
1021        let server = MatrixMockServer::new().await;
1022        let client = server.client_builder().build().await;
1023        let space_service = SpaceService::new(client.clone()).await;
1024
1025        let parent_space_id = room_id!("!parent_space:example.org");
1026        let child_space_id_1 = room_id!("!1:example.org");
1027
1028        server
1029            .mock_get_hierarchy()
1030            .ok_with_room_ids(vec![child_space_id_1])
1031            .expect(2)
1032            .mount()
1033            .await;
1034
1035        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
1036
1037        room_list.paginate().await.unwrap();
1038
1039        // This space contains 1 room
1040        assert_eq!(room_list.rooms().await.len(), 1);
1041
1042        // Resetting the room list
1043        room_list.reset().await;
1044
1045        // Clears the rooms and pagination token
1046        assert_eq!(room_list.rooms().await.len(), 0);
1047        assert_matches!(
1048            room_list.pagination_state(),
1049            SpaceRoomListPaginationState::Idle { end_reached: false }
1050        );
1051
1052        // Allows paginating again
1053        room_list.paginate().await.unwrap();
1054        assert_eq!(room_list.rooms().await.len(), 1);
1055    }
1056
1057    fn make_space_room(
1058        room_id: OwnedRoomId,
1059        order: Option<&str>,
1060        origin_server_ts: Option<u32>,
1061        children_state: &mut HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
1062    ) -> SpaceRoom {
1063        if let Some(origin_server_ts) = origin_server_ts {
1064            children_state.insert(
1065                room_id.clone(),
1066                hierarchy_space_child_event(&room_id, order, origin_server_ts),
1067            );
1068        }
1069        SpaceRoom {
1070            room_id,
1071            canonical_alias: None,
1072            name: Some("New room name".to_owned()),
1073            display_name: "Empty room".to_owned(),
1074            topic: None,
1075            avatar_url: None,
1076            room_type: None,
1077            num_joined_members: 0,
1078            join_rule: None,
1079            world_readable: None,
1080            guest_can_join: false,
1081            is_direct: None,
1082            children_count: 0,
1083            state: None,
1084            heroes: None,
1085            via: vec![],
1086            suggested: false,
1087            is_dm: None,
1088        }
1089    }
1090
1091    fn hierarchy_space_child_event(
1092        room_id: &RoomId,
1093        order: Option<&str>,
1094        origin_server_ts: u32,
1095    ) -> HierarchySpaceChildEvent {
1096        let mut json = json!({
1097            "content": {
1098                "via": []
1099            },
1100            "origin_server_ts": origin_server_ts,
1101            "sender": "@bob:a.b",
1102            "state_key": room_id.to_string(),
1103            "type": "m.space.child"
1104        });
1105
1106        if let Some(order) = order {
1107            json["content"]["order"] = json!(order);
1108        }
1109
1110        from_value::<HierarchySpaceChildEvent>(json).unwrap()
1111    }
1112}