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::{future::join_all, 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 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                    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().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};
411
412    use eyeball_im::VectorDiff;
413    use futures_util::pin_mut;
414    use matrix_sdk::{RoomState, test_utils::mocks::MatrixMockServer};
415    use matrix_sdk_test::{
416        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
417    };
418    use ruma::{
419        MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId,
420        events::space::child::HierarchySpaceChildEvent,
421        owned_room_id, owned_server_name,
422        room::{JoinRuleSummary, RoomSummary},
423        room_id, server_name, uint,
424    };
425    use serde_json::{from_value, json};
426    use strass::assert_let;
427    use stream_assert::{assert_next_eq, assert_next_matches, assert_pending, assert_ready};
428    use wiremock::ResponseTemplate;
429
430    use crate::spaces::{
431        SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState,
432    };
433
434    #[async_test]
435    async fn test_room_list_pagination() {
436        let server = MatrixMockServer::new().await;
437        let client = server.client_builder().build().await;
438        let user_id = client.user_id().unwrap();
439        let space_service = SpaceService::new(client.clone()).await;
440        let factory = EventFactory::new();
441
442        server.mock_room_state_encryption().plain().mount().await;
443
444        let parent_space_id = room_id!("!parent_space:example.org");
445        let child_space_id_1 = room_id!("!1:example.org");
446        let child_space_id_2 = room_id!("!2:example.org");
447
448        server
449            .sync_room(
450                &client,
451                JoinedRoomBuilder::new(parent_space_id)
452                    .add_state_event(
453                        factory
454                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
455                            .sender(user_id),
456                    )
457                    .add_state_event(
458                        factory
459                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
460                            .sender(user_id),
461                    ),
462            )
463            .await;
464
465        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
466
467        // The space parent is known to the client and should be populated
468        // accordingly
469        assert_let!(Some(parent_space) = room_list.space());
470        assert_eq!(parent_space.children_count, 2);
471
472        // Start off idle
473        assert_matches!(
474            room_list.pagination_state(),
475            SpaceRoomListPaginationState::Idle { end_reached: false }
476        );
477
478        // without any rooms
479        assert_eq!(room_list.rooms().await, vec![]);
480
481        // and with pending subscribers
482
483        let pagination_state_subscriber = room_list.subscribe_to_pagination_state_updates();
484        pin_mut!(pagination_state_subscriber);
485        assert_pending!(pagination_state_subscriber);
486
487        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
488        pin_mut!(rooms_subscriber);
489        assert_pending!(rooms_subscriber);
490
491        // Paginating the room list
492        server
493            .mock_get_hierarchy()
494            .ok_with_room_ids_and_children_state(
495                vec![child_space_id_1, child_space_id_2],
496                vec![(room_id!("!child:example.org"), vec![])],
497            )
498            .mount()
499            .await;
500
501        room_list.paginate().await.unwrap();
502
503        // informs that the pagination reached the end
504        assert_next_matches!(
505            pagination_state_subscriber,
506            SpaceRoomListPaginationState::Idle { end_reached: true }
507        );
508
509        // and yields results
510        assert_next_eq!(
511            rooms_subscriber,
512            vec![
513                VectorDiff::PushBack {
514                    value: SpaceRoom::new_from_summary(
515                        &RoomSummary::new(
516                            child_space_id_1.to_owned(),
517                            JoinRuleSummary::Public,
518                            false,
519                            uint!(1),
520                            false,
521                        ),
522                        None,
523                        1,
524                        vec![],
525                        false,
526                    )
527                    .await
528                },
529                VectorDiff::PushBack {
530                    value: SpaceRoom::new_from_summary(
531                        &RoomSummary::new(
532                            child_space_id_2.to_owned(),
533                            JoinRuleSummary::Public,
534                            false,
535                            uint!(1),
536                            false,
537                        ),
538                        None,
539                        1,
540                        vec![],
541                        false,
542                    )
543                    .await,
544                }
545            ]
546        );
547    }
548
549    #[async_test]
550    async fn test_room_state_updates() {
551        let server = MatrixMockServer::new().await;
552        let client = server.client_builder().build().await;
553        let space_service = SpaceService::new(client.clone()).await;
554
555        let parent_space_id = room_id!("!parent_space:example.org");
556        let child_room_id_1 = room_id!("!1:example.org");
557        let child_room_id_2 = room_id!("!2:example.org");
558
559        server
560            .mock_get_hierarchy()
561            .ok_with_room_ids(vec![child_room_id_1, child_room_id_2])
562            .mount()
563            .await;
564
565        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
566
567        room_list.paginate().await.unwrap();
568
569        // This space contains 2 rooms
570        assert_eq!(room_list.rooms().await.first().unwrap().room_id, child_room_id_1);
571        assert_eq!(room_list.rooms().await.last().unwrap().room_id, child_room_id_2);
572
573        // and we don't know about either of them
574        assert_eq!(room_list.rooms().await.first().unwrap().state, None);
575        assert_eq!(room_list.rooms().await.last().unwrap().state, None);
576
577        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
578        pin_mut!(rooms_subscriber);
579        assert_pending!(rooms_subscriber);
580
581        // Joining one of them though
582        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_1)).await;
583
584        // Results in an update being pushed through
585        assert_ready!(rooms_subscriber);
586        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Joined));
587        assert_eq!(room_list.rooms().await.last().unwrap().state, None);
588
589        // Same for the second one
590        server.sync_room(&client, JoinedRoomBuilder::new(child_room_id_2)).await;
591        assert_ready!(rooms_subscriber);
592        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Joined));
593        assert_eq!(room_list.rooms().await.last().unwrap().state, Some(RoomState::Joined));
594
595        // And when leaving them
596        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_1)).await;
597        server.sync_room(&client, LeftRoomBuilder::new(child_room_id_2)).await;
598        assert_ready!(rooms_subscriber);
599        assert_eq!(room_list.rooms().await.first().unwrap().state, Some(RoomState::Left));
600        assert_eq!(room_list.rooms().await.last().unwrap().state, Some(RoomState::Left));
601    }
602
603    #[async_test]
604    async fn test_parent_space_updates() {
605        let server = MatrixMockServer::new().await;
606        let client = server.client_builder().build().await;
607        let user_id = client.user_id().unwrap();
608        let space_service = SpaceService::new(client.clone()).await;
609        let factory = EventFactory::new();
610
611        server.mock_room_state_encryption().plain().mount().await;
612
613        let parent_space_id = room_id!("!parent_space:example.org");
614        let child_space_id_1 = room_id!("!1:example.org");
615        let child_space_id_2 = room_id!("!2:example.org");
616
617        // Parent space is unknown to the client and thus not populated yet
618        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
619        assert!(room_list.space().is_none());
620
621        let parent_space_subscriber = room_list.subscribe_to_space_updates();
622        pin_mut!(parent_space_subscriber);
623        assert_pending!(parent_space_subscriber);
624
625        server
626            .mock_get_hierarchy()
627            .ok_with_room_ids_and_children_state(
628                vec![parent_space_id, child_space_id_1, child_space_id_2],
629                vec![(
630                    room_id!("!child:example.org"),
631                    vec![server_name!("matrix-client.example.org")],
632                )],
633            )
634            .mount()
635            .await;
636
637        // Pagination will however fetch and populate it from /hierarchy
638        room_list.paginate().await.unwrap();
639        assert_let!(Some(parent_space) = room_list.space());
640        assert_eq!(parent_space.room_id, parent_space_id);
641
642        // And the subscription is informed about the change
643        assert_next_eq!(parent_space_subscriber, Some(parent_space));
644
645        // If the room is already known to the client then the space parent is
646        // populated directly on creation
647        server
648            .sync_room(
649                &client,
650                JoinedRoomBuilder::new(parent_space_id)
651                    .add_state_event(
652                        factory
653                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned())
654                            .sender(user_id),
655                    )
656                    .add_state_event(
657                        factory
658                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned())
659                            .sender(user_id),
660                    ),
661            )
662            .await;
663
664        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
665
666        // The parent space is known to the client and should be populated
667        // accordingly
668        assert_let!(Some(parent_space) = room_list.space());
669        assert_eq!(parent_space.children_count, 2);
670    }
671
672    #[async_test]
673    async fn test_parent_space_room_info_update() {
674        let server = MatrixMockServer::new().await;
675        let client = server.client_builder().build().await;
676        let user_id = client.user_id().unwrap();
677        let space_service = SpaceService::new(client.clone()).await;
678        let factory = EventFactory::new();
679
680        server.mock_room_state_encryption().plain().mount().await;
681
682        let parent_space_id = room_id!("!parent_space:example.org");
683
684        server.sync_room(&client, JoinedRoomBuilder::new(parent_space_id)).await;
685
686        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
687        assert_let!(Some(parent_space) = room_list.space());
688
689        // The parent space is known to the client
690        let parent_space_subscriber = room_list.subscribe_to_space_updates();
691        pin_mut!(parent_space_subscriber);
692        assert_pending!(parent_space_subscriber);
693
694        // So any room info changes are automatically published
695        server
696            .sync_room(
697                &client,
698                JoinedRoomBuilder::new(parent_space_id)
699                    .add_state_event(factory.room_topic("New room topic").sender(user_id))
700                    .add_state_event(factory.room_name("New room name").sender(user_id)),
701            )
702            .await;
703
704        let mut updated_parent_space = parent_space.clone();
705        updated_parent_space.topic = Some("New room topic".to_owned());
706        updated_parent_space.name = Some("New room name".to_owned());
707        updated_parent_space.display_name = "New room name".to_owned();
708
709        // And the subscription is informed about the change
710        assert_next_eq!(parent_space_subscriber, Some(updated_parent_space));
711    }
712
713    #[async_test]
714    async fn test_via_retrieval() {
715        let server = MatrixMockServer::new().await;
716        let client = server.client_builder().build().await;
717        let space_service = SpaceService::new(client.clone()).await;
718
719        server.mock_room_state_encryption().plain().mount().await;
720
721        let parent_space_id = room_id!("!parent_space:example.org");
722        let child_space_id_1 = room_id!("!1:example.org");
723        let child_space_id_2 = room_id!("!2:example.org");
724
725        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
726
727        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
728        pin_mut!(rooms_subscriber);
729
730        // When retrieving the parent and children via /hierarchy
731        server
732            .mock_get_hierarchy()
733            .ok_with_room_ids_and_children_state(
734                vec![parent_space_id, child_space_id_1, child_space_id_2],
735                vec![
736                    (child_space_id_1, vec![server_name!("matrix-client.example.org")]),
737                    (child_space_id_2, vec![server_name!("other-matrix-client.example.org")]),
738                ],
739            )
740            .mount()
741            .await;
742
743        room_list.paginate().await.unwrap();
744
745        // The parent `children_state` is used to populate children via params
746        assert_next_eq!(
747            rooms_subscriber,
748            vec![
749                VectorDiff::PushBack {
750                    value: SpaceRoom::new_from_summary(
751                        &RoomSummary::new(
752                            child_space_id_1.to_owned(),
753                            JoinRuleSummary::Public,
754                            false,
755                            uint!(1),
756                            false,
757                        ),
758                        None,
759                        2,
760                        vec![owned_server_name!("matrix-client.example.org")],
761                        false,
762                    )
763                    .await
764                },
765                VectorDiff::PushBack {
766                    value: SpaceRoom::new_from_summary(
767                        &RoomSummary::new(
768                            child_space_id_2.to_owned(),
769                            JoinRuleSummary::Public,
770                            false,
771                            uint!(1),
772                            false,
773                        ),
774                        None,
775                        2,
776                        vec![owned_server_name!("other-matrix-client.example.org")],
777                        false,
778                    )
779                    .await,
780                }
781            ]
782        );
783    }
784
785    #[async_test]
786    async fn test_suggested_field() {
787        let server = MatrixMockServer::new().await;
788        let client = server.client_builder().build().await;
789        let space_service = SpaceService::new(client.clone()).await;
790
791        server.mock_room_state_encryption().plain().mount().await;
792
793        let parent_space_id = room_id!("!parent_space:example.org");
794        let suggested_child = room_id!("!suggested:example.org");
795        let not_suggested_child = room_id!("!not_suggested:example.org");
796
797        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
798
799        let (_, rooms_subscriber) = room_list.subscribe_to_room_updates().await;
800        pin_mut!(rooms_subscriber);
801
802        // Mock a /hierarchy response where one child is suggested and the other
803        // is not.
804        let children_state = vec![
805            json!({
806                "type": "m.space.child",
807                "state_key": suggested_child,
808                "content": { "via": ["example.org"], "suggested": true },
809                "sender": "@admin:example.org",
810                "origin_server_ts": MilliSecondsSinceUnixEpoch::now()
811            }),
812            json!({
813                "type": "m.space.child",
814                "state_key": not_suggested_child,
815                "content": { "via": ["example.org"], "suggested": false },
816                "sender": "@admin:example.org",
817                "origin_server_ts": MilliSecondsSinceUnixEpoch::now()
818            }),
819        ];
820
821        server
822            .mock_get_hierarchy()
823            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
824                "rooms": [
825                    {
826                        "room_id": parent_space_id,
827                        "num_joined_members": 1,
828                        "world_readable": false,
829                        "guest_can_join": false,
830                        "children_state": children_state
831                    },
832                    {
833                        "room_id": suggested_child,
834                        "num_joined_members": 5,
835                        "world_readable": false,
836                        "guest_can_join": false,
837                        "children_state": []
838                    },
839                    {
840                        "room_id": not_suggested_child,
841                        "num_joined_members": 3,
842                        "world_readable": false,
843                        "guest_can_join": false,
844                        "children_state": []
845                    },
846                ]
847            })))
848            .mount()
849            .await;
850
851        room_list.paginate().await.unwrap();
852
853        let rooms_diff = assert_next_matches!(rooms_subscriber, diff => diff);
854        assert_eq!(rooms_diff.len(), 2);
855
856        // Collect the rooms into a map for easier assertion.
857        let mut rooms_by_id = HashMap::new();
858        for diff in rooms_diff {
859            if let VectorDiff::PushBack { value } = diff {
860                rooms_by_id.insert(value.room_id.clone(), value);
861            } else {
862                panic!("Expected PushBack, got {:?}", diff);
863            }
864        }
865
866        // The child with "suggested": true should have suggested == true.
867        let suggested_room = rooms_by_id.get(suggested_child).expect("suggested child not found");
868        assert!(
869            suggested_room.suggested,
870            "Room with 'suggested: true' should have suggested == true"
871        );
872
873        // The child with "suggested": false should have suggested == false.
874        let not_suggested_room =
875            rooms_by_id.get(not_suggested_child).expect("not-suggested child not found");
876        assert!(
877            !not_suggested_room.suggested,
878            "Room with 'suggested: false' should have suggested == false"
879        );
880    }
881
882    #[async_test]
883    async fn test_room_list_sorting() {
884        let mut children_state = HashMap::<OwnedRoomId, HierarchySpaceChildEvent>::new();
885
886        // Rooms not present in the `children_state` should be sorted by their
887        // room ID
888        assert_eq!(
889            SpaceRoomList::compare_rooms(
890                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
891                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
892                &children_state,
893            ),
894            Ordering::Less
895        );
896
897        assert_eq!(
898            SpaceRoomList::compare_rooms(
899                &make_space_room(owned_room_id!("!Marțolea:a.b"), None, None, &mut children_state),
900                &make_space_room(owned_room_id!("!Luana:a.b"), None, None, &mut children_state),
901                &children_state,
902            ),
903            Ordering::Greater
904        );
905
906        // Rooms without an order provided through the `children_state` should
907        // be sorted by their `m.space.child` `origin_server_ts`
908        assert_eq!(
909            SpaceRoomList::compare_rooms(
910                &make_space_room(owned_room_id!("!Luana:a.b"), None, Some(1), &mut children_state),
911                &make_space_room(
912                    owned_room_id!("!Marțolea:a.b"),
913                    None,
914                    Some(0),
915                    &mut children_state
916                ),
917                &children_state,
918            ),
919            Ordering::Greater
920        );
921
922        // The `m.space.child` `content.order` field should be used if provided
923        assert_eq!(
924            SpaceRoomList::compare_rooms(
925                &make_space_room(
926                    owned_room_id!("!Joiana:a.b"),
927                    Some("last"),
928                    Some(123),
929                    &mut children_state
930                ),
931                &make_space_room(
932                    owned_room_id!("!Mioara:a.b"),
933                    Some("first"),
934                    Some(234),
935                    &mut children_state
936                ),
937                &children_state,
938            ),
939            Ordering::Greater
940        );
941
942        // The timestamp should be used when the `order` is the same
943        assert_eq!(
944            SpaceRoomList::compare_rooms(
945                &make_space_room(
946                    owned_room_id!("!Joiana:a.b"),
947                    Some("Same pasture"),
948                    Some(1),
949                    &mut children_state
950                ),
951                &make_space_room(
952                    owned_room_id!("!Mioara:a.b"),
953                    Some("Same pasture"),
954                    Some(0),
955                    &mut children_state
956                ),
957                &children_state,
958            ),
959            Ordering::Greater
960        );
961
962        // And the `room_id` should be used when both the `order` and the
963        // `timestamp` are equal
964        assert_eq!(
965            SpaceRoomList::compare_rooms(
966                &make_space_room(
967                    owned_room_id!("!Joiana:a.b"),
968                    Some("same_pasture"),
969                    Some(0),
970                    &mut children_state
971                ),
972                &make_space_room(
973                    owned_room_id!("!Mioara:a.b"),
974                    Some("same_pasture"),
975                    Some(0),
976                    &mut children_state
977                ),
978                &children_state,
979            ),
980            Ordering::Less
981        );
982
983        // When one of the rooms is missing `children_state` data the other one
984        // should take precedence
985        assert_eq!(
986            SpaceRoomList::compare_rooms(
987                &make_space_room(owned_room_id!("!Viola:a.b"), None, None, &mut children_state),
988                &make_space_room(
989                    owned_room_id!("!Sâmbotina:a.b"),
990                    None,
991                    Some(0),
992                    &mut children_state
993                ),
994                &children_state,
995            ),
996            Ordering::Greater
997        );
998
999        // If the `order` is missing from one of the rooms but `children_state`
1000        // is present then the other one should come first
1001        assert_eq!(
1002            SpaceRoomList::compare_rooms(
1003                &make_space_room(
1004                    owned_room_id!("!Sâmbotina:a.b"),
1005                    None,
1006                    Some(1),
1007                    &mut children_state
1008                ),
1009                &make_space_room(
1010                    owned_room_id!("!Dumana:a.b"),
1011                    Some("Some pasture"),
1012                    Some(1),
1013                    &mut children_state
1014                ),
1015                &children_state,
1016            ),
1017            Ordering::Greater
1018        );
1019    }
1020
1021    #[async_test]
1022    async fn test_reset() {
1023        let server = MatrixMockServer::new().await;
1024        let client = server.client_builder().build().await;
1025        let space_service = SpaceService::new(client.clone()).await;
1026
1027        let parent_space_id = room_id!("!parent_space:example.org");
1028        let child_space_id_1 = room_id!("!1:example.org");
1029
1030        server
1031            .mock_get_hierarchy()
1032            .ok_with_room_ids(vec![child_space_id_1])
1033            .expect(2)
1034            .mount()
1035            .await;
1036
1037        let room_list = space_service.space_room_list(parent_space_id.to_owned()).await;
1038
1039        room_list.paginate().await.unwrap();
1040
1041        // This space contains 1 room
1042        assert_eq!(room_list.rooms().await.len(), 1);
1043
1044        // Resetting the room list
1045        room_list.reset().await;
1046
1047        // Clears the rooms and pagination token
1048        assert_eq!(room_list.rooms().await.len(), 0);
1049        assert_matches!(
1050            room_list.pagination_state(),
1051            SpaceRoomListPaginationState::Idle { end_reached: false }
1052        );
1053
1054        // Allows paginating again
1055        room_list.paginate().await.unwrap();
1056        assert_eq!(room_list.rooms().await.len(), 1);
1057    }
1058
1059    fn make_space_room(
1060        room_id: OwnedRoomId,
1061        order: Option<&str>,
1062        origin_server_ts: Option<u32>,
1063        children_state: &mut HashMap<OwnedRoomId, HierarchySpaceChildEvent>,
1064    ) -> SpaceRoom {
1065        if let Some(origin_server_ts) = origin_server_ts {
1066            children_state.insert(
1067                room_id.clone(),
1068                hierarchy_space_child_event(&room_id, order, origin_server_ts),
1069            );
1070        }
1071        SpaceRoom {
1072            room_id,
1073            canonical_alias: None,
1074            name: Some("New room name".to_owned()),
1075            display_name: "Empty room".to_owned(),
1076            topic: None,
1077            avatar_url: None,
1078            room_type: None,
1079            num_joined_members: 0,
1080            join_rule: None,
1081            world_readable: None,
1082            guest_can_join: false,
1083            is_direct: None,
1084            children_count: 0,
1085            state: None,
1086            heroes: None,
1087            via: vec![],
1088            suggested: false,
1089            is_dm: None,
1090        }
1091    }
1092
1093    fn hierarchy_space_child_event(
1094        room_id: &RoomId,
1095        order: Option<&str>,
1096        origin_server_ts: u32,
1097    ) -> HierarchySpaceChildEvent {
1098        let mut json = json!({
1099            "content": {
1100                "via": []
1101            },
1102            "origin_server_ts": origin_server_ts,
1103            "sender": "@bob:a.b",
1104            "state_key": room_id.to_string(),
1105            "type": "m.space.child"
1106        });
1107
1108        if let Some(order) = order {
1109            json["content"]["order"] = json!(order);
1110        }
1111
1112        from_value::<HierarchySpaceChildEvent>(json).unwrap()
1113    }
1114}