Skip to main content

matrix_sdk_ui/spaces/
mod.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
15//! High level interfaces for working with Spaces
16//!
17//! The `SpaceService` is an UI oriented, high-level interface for working with
18//! [Matrix Spaces](https://spec.matrix.org/latest/client-server-api/#spaces).
19//! It provides methods to retrieve joined spaces, subscribe
20//! to updates, and navigate space hierarchies.
21//!
22//! It consists of 3 main components:
23//! - `SpaceService`: The main service for managing spaces. It
24//! - `SpaceGraph`: An utility that maps the `m.space.parent` and
25//!   `m.space.child` fields into a graph structure, removing cycles and
26//!   providing access to top level parents.
27//! - `SpaceRoomList`: A component for retrieving a space's children rooms and
28//!   their details.
29
30use std::{cmp::Ordering, collections::HashMap, sync::Arc};
31
32use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
33use futures_util::pin_mut;
34use imbl::Vector;
35use itertools::Itertools;
36use matrix_sdk::{
37    Client, Error as SDKError, Room, deserialized_responses::SyncOrStrippedState,
38    task_monitor::BackgroundTaskHandle,
39};
40use ruma::{
41    OwnedRoomId, RoomId, SpaceChildOrder,
42    events::{
43        self, StateEventType, SyncStateEvent,
44        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
45    },
46};
47use thiserror::Error;
48use tokio::sync::Mutex as AsyncMutex;
49use tracing::{error, trace, warn};
50
51use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle, room::SpaceRoomChildState};
52pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
53
54pub mod graph;
55pub mod leave;
56pub mod room;
57pub mod room_list;
58
59/// Possible [`SpaceService`] errors.
60#[derive(Debug, Error)]
61pub enum Error {
62    /// The user ID was not available from the client.
63    #[error("User ID not available from client")]
64    UserIdNotFound,
65
66    /// The requested room was not found.
67    #[error("Room `{0}` not found")]
68    RoomNotFound(OwnedRoomId),
69
70    /// The space parent/child state was missing.
71    #[error("Missing `{0}` for `{1}`")]
72    MissingState(StateEventType, OwnedRoomId),
73
74    /// Failed to set either of the m.space.parent or m.space.child state
75    /// events.
76    #[error("Failed to set either of the m.space.parent or m.space.child state events")]
77    UpdateRelationship(SDKError),
78
79    /// Failed to set the expected m.space.parent state event (but any
80    /// m.space.child changes were successful).
81    #[error(
82        "Failed to set the expected m.space.parent state event (but any m.space.child changes were successful)"
83    )]
84    UpdateInverseRelationship(SDKError),
85
86    /// Failed to leave a space.
87    #[error("Failed to leave space")]
88    LeaveSpace(SDKError),
89
90    /// Failed to load members.
91    #[error("Failed to load members")]
92    LoadRoomMembers(SDKError),
93}
94
95struct SpaceState {
96    graph: SpaceGraph,
97    top_level_joined_spaces: ObservableVector<SpaceRoom>,
98    space_filters: ObservableVector<SpaceFilter>,
99}
100
101/// The main entry point into the Spaces facilities.
102///
103/// The spaces service is responsible for retrieving one's joined rooms,
104/// building a graph out of their `m.space.parent` and `m.space.child` state
105/// events, and providing access to the top-level spaces and their children.
106///
107/// # Examples
108///
109/// ```no_run
110/// use futures_util::StreamExt;
111/// use matrix_sdk::Client;
112/// use matrix_sdk_ui::spaces::SpaceService;
113/// use ruma::owned_room_id;
114///
115/// # async {
116/// # let client: Client = todo!();
117/// let space_service = SpaceService::new(client.clone()).await;
118///
119/// // Get a list of all the joined spaces
120/// let joined_spaces = space_service.top_level_joined_spaces().await;
121///
122/// // And subscribe to changes on them
123/// // `initial_values` is equal to `top_level_joined_spaces` if nothing changed meanwhile
124/// let (initial_values, stream) =
125///     space_service.subscribe_to_top_level_joined_spaces().await;
126///
127/// while let Some(diffs) = stream.next().await {
128///     println!("Received joined spaces updates: {diffs:?}");
129/// }
130///
131/// // Get a list of all the rooms in a particular space
132/// let room_list = space_service
133///     .space_room_list(owned_room_id!("!some_space:example.org"))
134///     .await;
135///
136/// // Which can be used to retrieve information about the children rooms
137/// let children = room_list.rooms();
138/// # anyhow::Ok(()) };
139/// ```
140pub struct SpaceService {
141    client: Client,
142
143    space_state: Arc<AsyncMutex<SpaceState>>,
144
145    _room_update_handle: AsyncMutex<BackgroundTaskHandle>,
146}
147
148impl SpaceService {
149    /// Creates a new `SpaceService` instance.
150    pub async fn new(client: Client) -> Self {
151        let space_state = Arc::new(AsyncMutex::new(SpaceState {
152            graph: SpaceGraph::new(),
153            top_level_joined_spaces: ObservableVector::new(),
154            space_filters: ObservableVector::new(),
155        }));
156
157        let room_update_handle = client
158            .task_monitor()
159            .spawn_infinite_task("space_service", {
160                let client = client.clone();
161                let space_state = Arc::clone(&space_state);
162                let all_room_updates_receiver = client.subscribe_to_all_room_updates();
163
164                async move {
165                    pin_mut!(all_room_updates_receiver);
166
167                    loop {
168                        match all_room_updates_receiver.recv().await {
169                            Ok(updates) => {
170                                if updates.is_empty() {
171                                    continue;
172                                }
173
174                                let (spaces, filters, graph) =
175                                    Self::build_space_state(&client).await;
176                                Self::update_space_state_if_needed(
177                                    Vector::from(spaces),
178                                    Vector::from(filters),
179                                    graph,
180                                    &space_state,
181                                )
182                                .await;
183                            }
184                            Err(err) => {
185                                error!("error when listening to room updates: {err}");
186                            }
187                        }
188                    }
189                }
190            })
191            .abort_on_drop();
192
193        // Make sure to also update the currently joined spaces for the initial values.
194        let (spaces, filters, graph) = Self::build_space_state(&client).await;
195        Self::update_space_state_if_needed(
196            Vector::from(spaces),
197            Vector::from(filters),
198            graph,
199            &space_state,
200        )
201        .await;
202
203        Self { client, space_state, _room_update_handle: AsyncMutex::new(room_update_handle) }
204    }
205
206    /// Subscribes to updates on the joined spaces list. If space rooms are
207    /// joined or left, the stream will yield diffs that reflect the changes.
208    pub async fn subscribe_to_top_level_joined_spaces(
209        &self,
210    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
211        self.space_state
212            .lock()
213            .await
214            .top_level_joined_spaces
215            .subscribe()
216            .into_values_and_batched_stream()
217    }
218
219    /// Returns a list of all the top-level joined spaces. It will eagerly
220    /// compute the latest version and also notify subscribers if there were
221    /// any changes.
222    pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
223        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
224
225        Self::update_space_state_if_needed(
226            Vector::from(top_level_joined_spaces.clone()),
227            Vector::from(filters),
228            graph,
229            &self.space_state,
230        )
231        .await;
232
233        top_level_joined_spaces
234    }
235
236    /// Space filters provide access to a custom subset of the space graph that
237    /// can be used in tandem with the [`crate::RoomListService`] to narrow
238    /// down the presented rooms. A [`crate::room_list_service::RoomList`]'s
239    /// [`crate::room_list_service::RoomListDynamicEntriesController`] can take
240    /// a filter, which in this case can be a
241    /// [`crate::room_list_service::filters::new_filter_identifiers`]
242    /// pointing to the space descendants retrieved from the filters.
243    ///
244    /// They are limited to the first 2 levels of the graph, with the first
245    /// level only containing direct descendants while the second holds the rest
246    /// of them recursively.
247    ///
248    /// # Examples
249    ///
250    /// ```no_run
251    /// use futures_util::StreamExt;
252    /// use matrix_sdk::Client;
253    /// use matrix_sdk_ui::{
254    ///     room_list_service::{RoomListService, filters},
255    ///     spaces::SpaceService,
256    /// };
257    /// use ruma::owned_room_id;
258    ///
259    /// # async {
260    /// # let client: Client = todo!();
261    /// let space_service = SpaceService::new(client.clone()).await;
262    /// let room_list_service = RoomListService::new(client.clone()).await?;
263    ///
264    /// // Get the list of filters derived from the space hierarchy.
265    /// let space_filters = space_service.space_filters().await;
266    /// // Pick a filter/space
267    /// let space_filter = space_filters.first().unwrap();
268    ///
269    /// // Create a room list stream and a controller that accepts filters.
270    /// let all_rooms = room_list_service.all_rooms().await?;
271    /// let (_, controller) = all_rooms.entries_with_dynamic_adapters(25);
272    ///
273    /// // Apply an identifiers filter built from the space filter descendants.
274    /// controller.set_filter(Box::new(filters::new_filter_identifiers(
275    ///     space_filter.descendants.clone(),
276    /// )));
277    ///
278    /// # anyhow::Ok(()) };
279    /// ```
280    pub async fn space_filters(&self) -> Vec<SpaceFilter> {
281        let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
282
283        Self::update_space_state_if_needed(
284            Vector::from(top_level_joined_spaces),
285            Vector::from(filters.clone()),
286            graph,
287            &self.space_state,
288        )
289        .await;
290
291        filters
292    }
293
294    /// Subscribe to changes or updates to the space filters.
295    pub async fn subscribe_to_space_filters(
296        &self,
297    ) -> (Vector<SpaceFilter>, VectorSubscriberBatchedStream<SpaceFilter>) {
298        self.space_state.lock().await.space_filters.subscribe().into_values_and_batched_stream()
299    }
300
301    /// Returns a flattened list containing all the spaces where the user has
302    /// permission to send `m.space.child` state events.
303    ///
304    /// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
305    /// recompute graph, nor does it notify subscribers about changes.
306    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
307        let Some(user_id) = self.client.user_id() else {
308            return vec![];
309        };
310
311        let graph = &self.space_state.lock().await.graph;
312        let rooms = self.client.joined_space_rooms();
313
314        let mut editable_spaces = Vec::new();
315        for room in &rooms {
316            if let Ok(power_levels) = room.power_levels().await
317                && power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
318            {
319                let room_id = room.room_id();
320                editable_spaces
321                    .push(SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64));
322            }
323        }
324
325        editable_spaces
326    }
327
328    /// Returns a `SpaceRoomList` for the given space ID.
329    pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
330        SpaceRoomList::new(self.client.clone(), space_id).await
331    }
332
333    /// Returns all known direct-parents of a given space room ID.
334    pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
335        let graph = &self.space_state.lock().await.graph;
336
337        graph
338            .parents_of(child_id)
339            .into_iter()
340            .filter_map(|parent_id| self.client.get_room(parent_id))
341            .map(|room| {
342                SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64)
343            })
344            .collect()
345    }
346
347    /// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
348    /// if it isn't known.
349    pub async fn get_space_room(&self, room_id: &RoomId) -> Option<SpaceRoom> {
350        let graph = &self.space_state.lock().await.graph;
351
352        if graph.has_node(room_id)
353            && let Some(room) = self.client.get_room(room_id)
354        {
355            Some(SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64))
356        } else {
357            None
358        }
359    }
360
361    pub async fn add_child_to_space(
362        &self,
363        child_id: OwnedRoomId,
364        space_id: OwnedRoomId,
365    ) -> Result<(), Error> {
366        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
367        let space_room =
368            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
369        let child_room =
370            self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
371        let child_power_levels = child_room
372            .power_levels()
373            .await
374            .map_err(|error| Error::UpdateRelationship(matrix_sdk::Error::from(error)))?;
375
376        // Add the child to the space.
377        let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
378        space_room
379            .send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
380            .await
381            .map_err(Error::UpdateRelationship)?;
382
383        // Add the space as parent of the child if allowed.
384        if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) {
385            let parent_route =
386                space_room.route().await.map_err(Error::UpdateInverseRelationship)?;
387            child_room
388                .send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
389                .await
390                .map_err(Error::UpdateInverseRelationship)?;
391        } else {
392            warn!("The current user doesn't have permission to set the child's parent.");
393        }
394
395        Ok(())
396    }
397
398    pub async fn remove_child_from_space(
399        &self,
400        child_id: OwnedRoomId,
401        space_id: OwnedRoomId,
402    ) -> Result<(), Error> {
403        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
404        let space_room =
405            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
406
407        if let Ok(Some(_)) =
408            space_room.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id).await
409        {
410            // Redacting state is a "weird" thing to do, so send {} instead.
411            // https://github.com/matrix-org/matrix-spec/issues/2252
412            //
413            // Specifically, "The redaction of the state doesn't participate in state
414            // resolution so behaves quite differently from e.g. sending an empty form of
415            // that state events".
416            space_room
417                .send_state_event_raw("m.space.child", child_id.as_str(), serde_json::json!({}))
418                .await
419                .map_err(Error::UpdateRelationship)?;
420        } else {
421            warn!("A space child event wasn't found on the parent, ignoring.");
422        }
423
424        if let Some(child_room) = self.client.get_room(&child_id) {
425            let power_levels = child_room.power_levels().await.map_err(|error| {
426                Error::UpdateInverseRelationship(matrix_sdk::Error::from(error))
427            })?;
428
429            if power_levels.user_can_send_state(user_id, StateEventType::SpaceParent)
430                && let Ok(Some(_)) = child_room
431                    .get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id)
432                    .await
433            {
434                // Same as the comment above.
435                child_room
436                    .send_state_event_raw(
437                        "m.space.parent",
438                        space_id.as_str(),
439                        serde_json::json!({}),
440                    )
441                    .await
442                    .map_err(Error::UpdateInverseRelationship)?;
443            } else {
444                warn!("A space parent event wasn't found on the child, ignoring.");
445            }
446        } else {
447            warn!("The child room is unknown, skipping m.space.parent removal.");
448        }
449
450        Ok(())
451    }
452
453    /// Start a space leave process returning a [`LeaveSpaceHandle`] from which
454    /// rooms can be retrieved in reversed BFS order starting from the requested
455    /// `space_id` graph node. If the room is unknown then an error will be
456    /// returned.
457    ///
458    /// Once the rooms to be left are chosen the handle can be used to leave
459    /// them.
460    pub async fn leave_space(&self, space_id: &RoomId) -> Result<LeaveSpaceHandle, Error> {
461        let space_state = self.space_state.lock().await;
462
463        if !space_state.graph.has_node(space_id) {
464            return Err(Error::RoomNotFound(space_id.to_owned()));
465        }
466
467        let room_ids = space_state.graph.flattened_bottom_up_subtree(space_id);
468
469        let handle = LeaveSpaceHandle::new(self.client.clone(), room_ids).await;
470
471        Ok(handle)
472    }
473
474    async fn update_space_state_if_needed(
475        new_spaces: Vector<SpaceRoom>,
476        new_filters: Vector<SpaceFilter>,
477        new_graph: SpaceGraph,
478        space_state: &Arc<AsyncMutex<SpaceState>>,
479    ) {
480        let mut space_state = space_state.lock().await;
481
482        if new_spaces != space_state.top_level_joined_spaces.clone() {
483            space_state.top_level_joined_spaces.clear();
484            space_state.top_level_joined_spaces.append(new_spaces);
485        }
486
487        if new_filters != space_state.space_filters.clone() {
488            space_state.space_filters.clear();
489            space_state.space_filters.append(new_filters);
490        }
491
492        space_state.graph = new_graph;
493    }
494
495    async fn build_space_state(client: &Client) -> (Vec<SpaceRoom>, Vec<SpaceFilter>, SpaceGraph) {
496        let joined_spaces = client.joined_space_rooms();
497
498        // Build a graph to hold the parent-child relations
499        let mut graph = SpaceGraph::new();
500
501        // And also store `m.space.child` ordering info for later use
502        let mut space_child_states = HashMap::<OwnedRoomId, SpaceRoomChildState>::new();
503
504        // Iterate over all joined spaces and populate the graph with edges based
505        // on `m.space.parent` and `m.space.child` state events.
506        for space in joined_spaces.iter() {
507            graph.add_node(space.room_id().to_owned());
508
509            if let Ok(parents) = space.get_state_events_static::<SpaceParentEventContent>().await {
510                parents.into_iter()
511                .flat_map(|parent_event| match parent_event.deserialize() {
512                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
513                        Some(e.state_key)
514                    }
515                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
516                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
517                    Err(e) => {
518                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.parent: {e}");
519                        None
520                    }
521                }).for_each(|parent| graph.add_edge(parent, space.room_id().to_owned()));
522            } else {
523                error!(room_id = ?space.room_id(), "Could not get m.space.parent events");
524            }
525
526            if let Ok(children) = space.get_state_events_static::<SpaceChildEventContent>().await {
527                children.into_iter()
528                .filter_map(|child_event| match child_event.deserialize() {
529                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
530                        space_child_states.insert(
531                            e.state_key.to_owned(),
532                            SpaceRoomChildState {
533                                order: e.content.order.clone(),
534                                origin_server_ts: e.origin_server_ts,
535                            },
536                        );
537
538                        Some(e.state_key)
539                    }
540                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
541                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
542                    Err(e) => {
543                        trace!(room_id = ?space.room_id(), "Could not deserialize m.space.child: {e}");
544                        None
545                    }
546                }).for_each(|child| graph.add_edge(space.room_id().to_owned(), child));
547            } else {
548                error!(room_id = ?space.room_id(), "Could not get m.space.child events");
549            }
550        }
551
552        // Remove cycles from the graph. This is important because they are not
553        // enforced backend side.
554        graph.remove_cycles();
555
556        let root_nodes = graph.root_nodes();
557
558        // Proceed with filtering to the top level spaces, sorting them by their
559        // (optional) order field (as defined in MSC3230) and then mapping them
560        // to `SpaceRoom`s.
561        let top_level_space_rooms = joined_spaces
562            .iter()
563            .filter(|room| root_nodes.contains(&room.room_id()))
564            .collect::<Vec<_>>();
565
566        let mut top_level_space_order = HashMap::new();
567        for space in &top_level_space_rooms {
568            if let Ok(Some(raw_event)) =
569                space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
570                && let Ok(event) = raw_event.deserialize()
571            {
572                top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
573            }
574        }
575
576        let top_level_space_rooms = top_level_space_rooms
577            .into_iter()
578            .sorted_by(|a, b| {
579                let a = (a.room_id(), top_level_space_order.get(a.room_id()).map(AsRef::as_ref));
580                let b = (b.room_id(), top_level_space_order.get(b.room_id()).map(AsRef::as_ref));
581
582                compare_top_level_space_rooms(a, b)
583            })
584            .collect::<Vec<_>>();
585
586        let top_level_spaces = top_level_space_rooms
587            .iter()
588            .map(|room| {
589                SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
590            })
591            .collect();
592
593        let space_filters =
594            Self::build_space_filters(client, &graph, top_level_space_rooms, space_child_states);
595
596        (top_level_spaces, space_filters, graph)
597    }
598
599    /// Build the 2 levels required for space filters.
600    /// As per product requirements, the first level space filters only include
601    /// direct descendants while second level ones contain *all* descendants.
602    ///
603    /// The sorting mechanism is different between first level spaces/filters
604    /// and second level ones so while the former are already sorted at this
605    /// point the latter need to be manually taken care of here though the use
606    /// of the collected `m.space.child` state event details.
607    fn build_space_filters(
608        client: &Client,
609        graph: &SpaceGraph,
610        top_level_space_rooms: Vec<&Room>,
611        space_child_states: HashMap<OwnedRoomId, SpaceRoomChildState>,
612    ) -> Vec<SpaceFilter> {
613        let mut filters = Vec::new();
614        for top_level_space in top_level_space_rooms {
615            let children = graph
616                .children_of(top_level_space.room_id())
617                .into_iter()
618                .map(|id| id.to_owned())
619                .collect::<Vec<_>>();
620
621            filters.push(SpaceFilter {
622                space_room: SpaceRoom::new_from_known(top_level_space, children.len() as u64),
623                level: 0,
624                descendants: children.clone(),
625            });
626
627            filters.append(
628                &mut children
629                    .iter()
630                    .filter_map(|id| client.get_room(id))
631                    .filter(|room| room.is_space())
632                    .map(|room| {
633                        SpaceRoom::new_from_known(
634                            &room,
635                            graph.children_of(room.room_id()).len() as u64,
636                        )
637                    })
638                    .sorted_by(|a, b| {
639                        let a_state = space_child_states.get(&a.room_id).cloned();
640                        let b_state = space_child_states.get(&b.room_id).cloned();
641
642                        SpaceRoom::compare_rooms(
643                            (&a.room_id, a_state.as_ref()),
644                            (&b.room_id, b_state.as_ref()),
645                        )
646                    })
647                    .map(|space_room| {
648                        let descendants = graph.flattened_bottom_up_subtree(&space_room.room_id);
649
650                        SpaceFilter { space_room, level: 1, descendants }
651                    })
652                    .collect::<Vec<_>>(),
653            );
654        }
655
656        filters
657    }
658}
659
660// MSC3230: lexicographically by `order` and then by room ID
661fn compare_top_level_space_rooms(
662    a: (&RoomId, Option<&SpaceChildOrder>),
663    b: (&RoomId, Option<&SpaceChildOrder>),
664) -> Ordering {
665    let (a_room_id, a_order) = a;
666    let (b_room_id, b_order) = b;
667
668    match (a_order, b_order) {
669        (Some(a_order), Some(b_order)) => a_order.cmp(b_order).then(a_room_id.cmp(b_room_id)),
670        (Some(_), None) => Ordering::Less,
671        (None, Some(_)) => Ordering::Greater,
672        (None, None) => a_room_id.cmp(b_room_id),
673    }
674}
675
676#[derive(Debug, Clone, PartialEq)]
677pub struct SpaceFilter {
678    /// The underlying [`SpaceRoom`]
679    pub space_room: SpaceRoom,
680
681    /// The level of the space filter in the tree/hierarchy.
682    /// At this point in time the filters are limited to the first 2 levels.
683    pub level: u8,
684
685    /// The room identifiers of the descendants of this space.
686    /// For top level spaces (level 0) these will be direct descendants while
687    /// for first level spaces they will be all other descendants, recursively.
688    pub descendants: Vec<OwnedRoomId>,
689}
690
691#[cfg(test)]
692mod tests {
693    use std::collections::BTreeMap;
694
695    use assert_matches2::assert_let;
696    use eyeball_im::VectorDiff;
697    use futures_util::{StreamExt, pin_mut};
698    use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
699    use matrix_sdk_test::{
700        JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
701    };
702    use proptest::prelude::*;
703    use ruma::{
704        MilliSecondsSinceUnixEpoch, OwnedSpaceChildOrder, RoomVersionId, UserId, event_id,
705        owned_room_id, room_id, serde::Raw,
706    };
707    use serde_json::json;
708    use stream_assert::{assert_next_eq, assert_pending};
709
710    use super::*;
711
712    #[async_test]
713    async fn test_spaces_hierarchy() {
714        let server = MatrixMockServer::new().await;
715        let client = server.client_builder().build().await;
716        let user_id = client.user_id().unwrap();
717        let space_service = SpaceService::new(client.clone()).await;
718        let factory = EventFactory::new();
719
720        server.mock_room_state_encryption().plain().mount().await;
721
722        // Given one parent space with 2 children spaces
723
724        let parent_space_id = room_id!("!parent_space:example.org");
725        let child_space_id_1 = room_id!("!child_space_1:example.org");
726        let child_space_id_2 = room_id!("!child_space_2:example.org");
727
728        add_space_rooms(
729            vec![
730                MockSpaceRoomParameters {
731                    room_id: child_space_id_1,
732                    order: None,
733                    parents: vec![parent_space_id],
734                    children: vec![],
735                    power_level: None,
736                },
737                MockSpaceRoomParameters {
738                    room_id: child_space_id_2,
739                    order: None,
740                    parents: vec![parent_space_id],
741                    children: vec![],
742                    power_level: None,
743                },
744                MockSpaceRoomParameters {
745                    room_id: parent_space_id,
746                    order: None,
747                    parents: vec![],
748                    children: vec![child_space_id_1, child_space_id_2],
749                    power_level: None,
750                },
751            ],
752            &client,
753            &server,
754            &factory,
755            user_id,
756        )
757        .await;
758
759        // Only the parent space is returned
760        assert_eq!(
761            space_service
762                .top_level_joined_spaces()
763                .await
764                .iter()
765                .map(|s| s.room_id.to_owned())
766                .collect::<Vec<_>>(),
767            vec![parent_space_id]
768        );
769
770        // and it has 2 children
771        assert_eq!(
772            space_service
773                .top_level_joined_spaces()
774                .await
775                .iter()
776                .map(|s| s.children_count)
777                .collect::<Vec<_>>(),
778            vec![2]
779        );
780
781        let parent_space = client.get_room(parent_space_id).unwrap();
782        assert!(parent_space.is_space());
783
784        // And the parent space and the two child spaces are linked
785
786        let spaces: Vec<ParentSpace> = client
787            .get_room(child_space_id_1)
788            .unwrap()
789            .parent_spaces()
790            .await
791            .unwrap()
792            .map(Result::unwrap)
793            .collect()
794            .await;
795
796        assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
797        assert_eq!(parent.room_id(), parent_space.room_id());
798
799        let spaces: Vec<ParentSpace> = client
800            .get_room(child_space_id_2)
801            .unwrap()
802            .parent_spaces()
803            .await
804            .unwrap()
805            .map(Result::unwrap)
806            .collect()
807            .await;
808
809        assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
810        assert_eq!(parent.room_id(), parent_space.room_id());
811    }
812
813    #[async_test]
814    async fn test_joined_spaces_updates() {
815        let server = MatrixMockServer::new().await;
816        let client = server.client_builder().build().await;
817        let user_id = client.user_id().unwrap();
818        let factory = EventFactory::new();
819
820        server.mock_room_state_encryption().plain().mount().await;
821
822        let first_space_id = room_id!("!first_space:example.org");
823        let second_space_id = room_id!("!second_space:example.org");
824
825        // Join the first space
826        server
827            .sync_room(
828                &client,
829                JoinedRoomBuilder::new(first_space_id)
830                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type()),
831            )
832            .await;
833
834        // Build the `SpaceService` and expect the room to show up with no updates
835        // pending
836
837        let space_service = SpaceService::new(client.clone()).await;
838
839        let (initial_values, joined_spaces_subscriber) =
840            space_service.subscribe_to_top_level_joined_spaces().await;
841        pin_mut!(joined_spaces_subscriber);
842        assert_pending!(joined_spaces_subscriber);
843
844        assert_eq!(
845            initial_values,
846            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)].into()
847        );
848
849        assert_eq!(
850            space_service.top_level_joined_spaces().await,
851            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
852        );
853
854        // And the stream is still pending as the initial values were
855        // already set.
856        assert_pending!(joined_spaces_subscriber);
857
858        // Join the second space
859
860        server
861            .sync_room(
862                &client,
863                JoinedRoomBuilder::new(second_space_id)
864                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
865                    .add_state_event(
866                        factory
867                            .space_child(
868                                second_space_id.to_owned(),
869                                owned_room_id!("!child:example.org"),
870                            )
871                            .sender(user_id),
872                    ),
873            )
874            .await;
875
876        // And expect the list to update
877        assert_eq!(
878            space_service.top_level_joined_spaces().await,
879            vec![
880                SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
881                SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
882            ]
883        );
884
885        assert_next_eq!(
886            joined_spaces_subscriber,
887            vec![
888                VectorDiff::Clear,
889                VectorDiff::Append {
890                    values: vec![
891                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
892                        SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
893                    ]
894                    .into()
895                },
896            ]
897        );
898
899        server.sync_room(&client, LeftRoomBuilder::new(second_space_id)).await;
900
901        // and when one is left
902        assert_next_eq!(
903            joined_spaces_subscriber,
904            vec![
905                VectorDiff::Clear,
906                VectorDiff::Append {
907                    values: vec![SpaceRoom::new_from_known(
908                        &client.get_room(first_space_id).unwrap(),
909                        0
910                    )]
911                    .into()
912                },
913            ]
914        );
915
916        // but it doesn't when a non-space room gets joined
917        server
918            .sync_room(
919                &client,
920                JoinedRoomBuilder::new(room_id!("!room:example.org"))
921                    .add_state_event(factory.create(user_id, RoomVersionId::V1)),
922            )
923            .await;
924
925        // and the subscriber doesn't yield any updates
926        assert_pending!(joined_spaces_subscriber);
927        assert_eq!(
928            space_service.top_level_joined_spaces().await,
929            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
930        );
931    }
932
933    #[async_test]
934    async fn test_space_filters() {
935        let server = MatrixMockServer::new().await;
936        let client = server.client_builder().build().await;
937
938        server.mock_room_state_encryption().plain().mount().await;
939
940        add_space_rooms(
941            vec![
942                MockSpaceRoomParameters {
943                    room_id: room_id!("!1:a.b"),
944                    order: None,
945                    parents: vec![],
946                    children: vec![],
947                    power_level: None,
948                },
949                MockSpaceRoomParameters {
950                    room_id: room_id!("!1.2:a.b"),
951                    order: None,
952                    parents: vec![room_id!("!1:a.b")],
953                    children: vec![],
954                    power_level: None,
955                },
956                MockSpaceRoomParameters {
957                    room_id: room_id!("!1.2.3:a.b"),
958                    order: None,
959                    parents: vec![room_id!("!1.2:a.b")],
960                    children: vec![],
961                    power_level: None,
962                },
963                MockSpaceRoomParameters {
964                    room_id: room_id!("!1.2.3.4:a.b"),
965                    order: None,
966                    parents: vec![room_id!("!1.2.3:a.b")],
967                    children: vec![],
968                    power_level: None,
969                },
970            ],
971            &client,
972            &server,
973            &EventFactory::new(),
974            client.user_id().unwrap(),
975        )
976        .await;
977
978        let space_service = SpaceService::new(client.clone()).await;
979
980        let filters = space_service.space_filters().await;
981        assert_eq!(filters.len(), 2);
982        assert_eq!(filters[0].space_room.room_id, room_id!("!1:a.b"));
983        assert_eq!(filters[0].level, 0);
984        assert_eq!(filters[0].descendants.len(), 1); //
985        assert_eq!(filters[1].space_room.room_id, room_id!("!1.2:a.b"));
986        assert_eq!(filters[1].level, 1);
987        assert_eq!(filters[1].descendants.len(), 3);
988
989        let (initial_values, space_filters_subscriber) =
990            space_service.subscribe_to_space_filters().await;
991        pin_mut!(space_filters_subscriber);
992        assert_pending!(space_filters_subscriber);
993
994        assert_eq!(initial_values, filters.into());
995
996        add_space_rooms(
997            vec![MockSpaceRoomParameters {
998                room_id: room_id!("!1.2.3.4.5:a.b"),
999                order: None,
1000                parents: vec![room_id!("!1.2.3.4:a.b")],
1001                children: vec![],
1002                power_level: None,
1003            }],
1004            &client,
1005            &server,
1006            &EventFactory::new(),
1007            client.user_id().unwrap(),
1008        )
1009        .await;
1010
1011        space_filters_subscriber.next().await;
1012
1013        let filters = space_service.space_filters().await;
1014        assert_eq!(filters[0].descendants.len(), 1);
1015        assert_eq!(filters[1].descendants.len(), 4);
1016    }
1017
1018    #[async_test]
1019    async fn test_top_level_space_order() {
1020        let server = MatrixMockServer::new().await;
1021        let client = server.client_builder().build().await;
1022
1023        server.mock_room_state_encryption().plain().mount().await;
1024
1025        add_space_rooms(
1026            vec![
1027                MockSpaceRoomParameters {
1028                    room_id: room_id!("!2:a.b"),
1029                    order: Some("2"),
1030                    parents: vec![],
1031                    children: vec![],
1032                    power_level: None,
1033                },
1034                MockSpaceRoomParameters {
1035                    room_id: room_id!("!4:a.b"),
1036                    order: None,
1037                    parents: vec![],
1038                    children: vec![],
1039                    power_level: None,
1040                },
1041                MockSpaceRoomParameters {
1042                    room_id: room_id!("!3:a.b"),
1043                    order: None,
1044                    parents: vec![],
1045                    children: vec![],
1046                    power_level: None,
1047                },
1048                MockSpaceRoomParameters {
1049                    room_id: room_id!("!1:a.b"),
1050                    order: Some("1"),
1051                    parents: vec![],
1052                    children: vec![],
1053                    power_level: None,
1054                },
1055            ],
1056            &client,
1057            &server,
1058            &EventFactory::new(),
1059            client.user_id().unwrap(),
1060        )
1061        .await;
1062
1063        let space_service = SpaceService::new(client.clone()).await;
1064
1065        // Space with an `order` field set should come first in lexicographic
1066        // order and rest sorted by room ID.
1067        assert_eq!(
1068            space_service.top_level_joined_spaces().await,
1069            vec![
1070                SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0),
1071                SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0),
1072                SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0),
1073                SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0),
1074            ]
1075        );
1076    }
1077
1078    #[async_test]
1079    async fn test_editable_spaces() {
1080        // Given a space hierarchy where the user is admin of some spaces and subspaces.
1081        let server = MatrixMockServer::new().await;
1082        let client = server.client_builder().build().await;
1083        let user_id = client.user_id().unwrap();
1084        let factory = EventFactory::new();
1085
1086        server.mock_room_state_encryption().plain().mount().await;
1087
1088        let admin_space_id = room_id!("!admin_space:example.org");
1089        let admin_subspace_id = room_id!("!admin_subspace:example.org");
1090        let regular_space_id = room_id!("!regular_space:example.org");
1091        let regular_subspace_id = room_id!("!regular_subspace:example.org");
1092
1093        add_space_rooms(
1094            vec![
1095                MockSpaceRoomParameters {
1096                    room_id: admin_space_id,
1097                    order: None,
1098                    parents: vec![],
1099                    children: vec![regular_subspace_id],
1100                    power_level: Some(100),
1101                },
1102                MockSpaceRoomParameters {
1103                    room_id: admin_subspace_id,
1104                    order: None,
1105                    parents: vec![regular_space_id],
1106                    children: vec![],
1107                    power_level: Some(100),
1108                },
1109                MockSpaceRoomParameters {
1110                    room_id: regular_space_id,
1111                    order: None,
1112                    parents: vec![],
1113                    children: vec![admin_subspace_id],
1114                    power_level: Some(0),
1115                },
1116                MockSpaceRoomParameters {
1117                    room_id: regular_subspace_id,
1118                    order: None,
1119                    parents: vec![admin_space_id],
1120                    children: vec![],
1121                    power_level: Some(0),
1122                },
1123            ],
1124            &client,
1125            &server,
1126            &factory,
1127            user_id,
1128        )
1129        .await;
1130
1131        let space_service = SpaceService::new(client.clone()).await;
1132
1133        // When retrieving all editable joined spaces.
1134        let editable_spaces = space_service.editable_spaces().await;
1135
1136        // Then only the spaces where the user is admin are returned.
1137        assert_eq!(
1138            editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
1139            vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
1140        );
1141    }
1142
1143    #[async_test]
1144    async fn test_joined_parents_of_child() {
1145        // Given a space with three parent spaces, two of which are joined.
1146        let server = MatrixMockServer::new().await;
1147        let client = server.client_builder().build().await;
1148        let user_id = client.user_id().unwrap();
1149        let factory = EventFactory::new();
1150
1151        server.mock_room_state_encryption().plain().mount().await;
1152
1153        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
1154        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
1155        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
1156        let child_space_id = room_id!("!child_space:example.org");
1157
1158        add_space_rooms(
1159            vec![
1160                MockSpaceRoomParameters {
1161                    room_id: child_space_id,
1162                    order: None,
1163                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
1164                    children: vec![],
1165                    power_level: None,
1166                },
1167                MockSpaceRoomParameters {
1168                    room_id: parent_space_id_1,
1169                    order: None,
1170                    parents: vec![],
1171                    children: vec![child_space_id],
1172                    power_level: None,
1173                },
1174                MockSpaceRoomParameters {
1175                    room_id: parent_space_id_2,
1176                    order: None,
1177                    parents: vec![],
1178                    children: vec![child_space_id],
1179                    power_level: None,
1180                },
1181            ],
1182            &client,
1183            &server,
1184            &factory,
1185            user_id,
1186        )
1187        .await;
1188
1189        let space_service = SpaceService::new(client.clone()).await;
1190
1191        // When retrieving the joined parents of the child space
1192        let parents = space_service.joined_parents_of_child(child_space_id).await;
1193
1194        // Then both parent spaces are returned
1195        assert_eq!(
1196            parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
1197            vec![parent_space_id_1, parent_space_id_2]
1198        );
1199    }
1200
1201    #[async_test]
1202    async fn test_get_space_room_for_id() {
1203        let server = MatrixMockServer::new().await;
1204        let client = server.client_builder().build().await;
1205        let user_id = client.user_id().unwrap();
1206        let factory = EventFactory::new();
1207
1208        server.mock_room_state_encryption().plain().mount().await;
1209
1210        let space_id = room_id!("!single_space:example.org");
1211
1212        add_space_rooms(
1213            vec![MockSpaceRoomParameters {
1214                room_id: space_id,
1215                order: None,
1216                parents: vec![],
1217                children: vec![],
1218                power_level: None,
1219            }],
1220            &client,
1221            &server,
1222            &factory,
1223            user_id,
1224        )
1225        .await;
1226
1227        let space_service = SpaceService::new(client.clone()).await;
1228
1229        let found = space_service.get_space_room(space_id).await;
1230        assert!(found.is_some());
1231
1232        let expected = SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0);
1233        assert_eq!(found.unwrap(), expected);
1234    }
1235
1236    #[async_test]
1237    async fn test_add_child_to_space() {
1238        // Given a space and child room where the user is admin of both.
1239        let server = MatrixMockServer::new().await;
1240        let client = server.client_builder().build().await;
1241        let user_id = client.user_id().unwrap();
1242        let factory = EventFactory::new();
1243
1244        server.mock_room_state_encryption().plain().mount().await;
1245
1246        let space_child_event_id = event_id!("$1");
1247        let space_parent_event_id = event_id!("$2");
1248        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1249        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1250
1251        let space_id = room_id!("!my_space:example.org");
1252        let child_id = room_id!("!my_child:example.org");
1253
1254        add_space_rooms(
1255            vec![
1256                MockSpaceRoomParameters {
1257                    room_id: space_id,
1258                    order: None,
1259                    parents: vec![],
1260                    children: vec![],
1261                    power_level: Some(100),
1262                },
1263                MockSpaceRoomParameters {
1264                    room_id: child_id,
1265                    order: None,
1266                    parents: vec![],
1267                    children: vec![],
1268                    power_level: Some(100),
1269                },
1270            ],
1271            &client,
1272            &server,
1273            &factory,
1274            user_id,
1275        )
1276        .await;
1277
1278        let space_service = SpaceService::new(client.clone()).await;
1279
1280        // When adding the child to the space.
1281        let result =
1282            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1283
1284        // Then both space child and parent events are set successfully.
1285        assert!(result.is_ok());
1286    }
1287
1288    #[async_test]
1289    async fn test_add_child_to_space_without_space_admin() {
1290        // Given a space and child room where the user is a regular member of both.
1291        let server = MatrixMockServer::new().await;
1292        let client = server.client_builder().build().await;
1293        let user_id = client.user_id().unwrap();
1294        let factory = EventFactory::new();
1295
1296        server.mock_room_state_encryption().plain().mount().await;
1297
1298        server.mock_set_space_child().unauthorized().expect(1).mount().await;
1299        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1300
1301        let space_id = room_id!("!my_space:example.org");
1302        let child_id = room_id!("!my_child:example.org");
1303
1304        add_space_rooms(
1305            vec![
1306                MockSpaceRoomParameters {
1307                    room_id: space_id,
1308                    order: None,
1309                    parents: vec![],
1310                    children: vec![],
1311                    power_level: Some(0),
1312                },
1313                MockSpaceRoomParameters {
1314                    room_id: child_id,
1315                    order: None,
1316                    parents: vec![],
1317                    children: vec![],
1318                    power_level: Some(0),
1319                },
1320            ],
1321            &client,
1322            &server,
1323            &factory,
1324            user_id,
1325        )
1326        .await;
1327
1328        let space_service = SpaceService::new(client.clone()).await;
1329
1330        // When adding the child to the space.
1331        let result =
1332            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1333
1334        // Then the operation fails when trying to set the space child event and the
1335        // parent event is not attempted.
1336        assert!(result.is_err());
1337    }
1338
1339    #[async_test]
1340    async fn test_add_child_to_space_without_child_admin() {
1341        // Given a space and child room where the user is admin of the space but not of
1342        // the child.
1343        let server = MatrixMockServer::new().await;
1344        let client = server.client_builder().build().await;
1345        let user_id = client.user_id().unwrap();
1346        let factory = EventFactory::new();
1347
1348        server.mock_room_state_encryption().plain().mount().await;
1349
1350        let space_child_event_id = event_id!("$1");
1351        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1352        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1353
1354        let space_id = room_id!("!my_space:example.org");
1355        let child_id = room_id!("!my_child:example.org");
1356
1357        add_space_rooms(
1358            vec![
1359                MockSpaceRoomParameters {
1360                    room_id: space_id,
1361                    order: None,
1362                    parents: vec![],
1363                    children: vec![],
1364                    power_level: Some(100),
1365                },
1366                MockSpaceRoomParameters {
1367                    room_id: child_id,
1368                    order: None,
1369                    parents: vec![],
1370                    children: vec![],
1371                    power_level: Some(0),
1372                },
1373            ],
1374            &client,
1375            &server,
1376            &factory,
1377            user_id,
1378        )
1379        .await;
1380
1381        let space_service = SpaceService::new(client.clone()).await;
1382
1383        // When adding the child to the space.
1384        let result =
1385            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1386
1387        error!("result: {:?}", result);
1388        // Then the operation succeeds in setting the space child event and the parent
1389        // event is not attempted.
1390        assert!(result.is_ok());
1391    }
1392
1393    #[async_test]
1394    async fn test_remove_child_from_space() {
1395        // Given a space and child room where the user is admin of both.
1396        let server = MatrixMockServer::new().await;
1397        let client = server.client_builder().build().await;
1398        let user_id = client.user_id().unwrap();
1399        let factory = EventFactory::new();
1400
1401        server.mock_room_state_encryption().plain().mount().await;
1402
1403        let space_child_event_id = event_id!("$1");
1404        let space_parent_event_id = event_id!("$2");
1405        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1406        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1407
1408        let parent_id = room_id!("!parent_space:example.org");
1409        let child_id = room_id!("!child_space:example.org");
1410
1411        add_space_rooms(
1412            vec![
1413                MockSpaceRoomParameters {
1414                    room_id: parent_id,
1415                    order: None,
1416                    parents: vec![],
1417                    children: vec![child_id],
1418                    power_level: None,
1419                },
1420                MockSpaceRoomParameters {
1421                    room_id: child_id,
1422                    order: None,
1423                    parents: vec![parent_id],
1424                    children: vec![],
1425                    power_level: None,
1426                },
1427            ],
1428            &client,
1429            &server,
1430            &factory,
1431            user_id,
1432        )
1433        .await;
1434
1435        let space_service = SpaceService::new(client.clone()).await;
1436
1437        // When removing the child from the space.
1438        let result =
1439            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1440
1441        // Then both space child and parent events are removed successfully.
1442        assert!(result.is_ok());
1443    }
1444
1445    #[async_test]
1446    async fn test_remove_child_from_space_without_parent_event() {
1447        // Given a space with a child where the m.space.parent event wasn't set.
1448        let server = MatrixMockServer::new().await;
1449        let client = server.client_builder().build().await;
1450        let user_id = client.user_id().unwrap();
1451        let factory = EventFactory::new();
1452
1453        server.mock_room_state_encryption().plain().mount().await;
1454
1455        let space_child_event_id = event_id!("$1");
1456        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1457        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1458
1459        let parent_id = room_id!("!parent_space:example.org");
1460        let child_id = room_id!("!child_space:example.org");
1461
1462        add_space_rooms(
1463            vec![
1464                MockSpaceRoomParameters {
1465                    room_id: parent_id,
1466                    order: None,
1467                    parents: vec![],
1468                    children: vec![child_id],
1469                    power_level: None,
1470                },
1471                MockSpaceRoomParameters {
1472                    room_id: child_id,
1473                    order: None,
1474                    parents: vec![],
1475                    children: vec![],
1476                    power_level: None,
1477                },
1478            ],
1479            &client,
1480            &server,
1481            &factory,
1482            user_id,
1483        )
1484        .await;
1485
1486        let space_service = SpaceService::new(client.clone()).await;
1487
1488        // When removing the child from the space.
1489        let result =
1490            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1491
1492        // Then the child event is removed successfully and the parent event removal is
1493        // not attempted.
1494        assert!(result.is_ok());
1495    }
1496
1497    #[async_test]
1498    async fn test_remove_child_from_space_without_child_event() {
1499        // Given a space with a child where the space's m.space.child event wasn't set.
1500        let server = MatrixMockServer::new().await;
1501        let client = server.client_builder().build().await;
1502        let user_id = client.user_id().unwrap();
1503        let factory = EventFactory::new();
1504
1505        server.mock_room_state_encryption().plain().mount().await;
1506
1507        let space_parent_event_id = event_id!("$2");
1508        server.mock_set_space_child().unauthorized().expect(0).mount().await;
1509        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1510
1511        let parent_id = room_id!("!parent_space:example.org");
1512        let child_id = room_id!("!child_space:example.org");
1513
1514        add_space_rooms(
1515            vec![
1516                MockSpaceRoomParameters {
1517                    room_id: parent_id,
1518                    order: None,
1519                    parents: vec![],
1520                    children: vec![],
1521                    power_level: None,
1522                },
1523                MockSpaceRoomParameters {
1524                    room_id: child_id,
1525                    order: None,
1526                    parents: vec![parent_id],
1527                    children: vec![],
1528                    power_level: None,
1529                },
1530            ],
1531            &client,
1532            &server,
1533            &factory,
1534            user_id,
1535        )
1536        .await;
1537
1538        let space_service = SpaceService::new(client.clone()).await;
1539
1540        // When removing the child from the space.
1541        let result =
1542            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1543
1544        // Then the parent event is removed successfully and the child event removal is
1545        // not attempted.
1546        assert!(result.is_ok());
1547    }
1548
1549    #[async_test]
1550    async fn test_remove_unknown_child_from_space() {
1551        // Given a space with a child room that is unknown (not in the client store).
1552        let server = MatrixMockServer::new().await;
1553        let client = server.client_builder().build().await;
1554        let user_id = client.user_id().unwrap();
1555        let factory = EventFactory::new();
1556
1557        server.mock_room_state_encryption().plain().mount().await;
1558
1559        let space_child_event_id = event_id!("$1");
1560        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1561        // The parent event should not be attempted since the child room is unknown.
1562        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1563
1564        let parent_id = room_id!("!parent_space:example.org");
1565        let unknown_child_id = room_id!("!unknown_child:example.org");
1566
1567        // Only add the parent space, not the child room.
1568        add_space_rooms(
1569            vec![MockSpaceRoomParameters {
1570                room_id: parent_id,
1571                order: None,
1572                parents: vec![],
1573                children: vec![unknown_child_id],
1574                power_level: None,
1575            }],
1576            &client,
1577            &server,
1578            &factory,
1579            user_id,
1580        )
1581        .await;
1582
1583        // Verify that the child room is indeed unknown.
1584        assert!(client.get_room(unknown_child_id).is_none());
1585
1586        let space_service = SpaceService::new(client.clone()).await;
1587
1588        // When removing the unknown child from the space.
1589        let result = space_service
1590            .remove_child_from_space(unknown_child_id.to_owned(), parent_id.to_owned())
1591            .await;
1592
1593        // Then the operation succeeds: the child event is removed from the space,
1594        // and the parent event removal is skipped since the child room is unknown.
1595        assert!(result.is_ok());
1596    }
1597
1598    #[async_test]
1599    async fn test_space_child_updates() {
1600        // Test child updates received via sync.
1601        let server = MatrixMockServer::new().await;
1602        let client = server.client_builder().build().await;
1603        let user_id = client.user_id().unwrap();
1604        let factory = EventFactory::new();
1605
1606        server.mock_room_state_encryption().plain().mount().await;
1607
1608        let space_id = room_id!("!space:localhost");
1609        let first_child_id = room_id!("!first_child:localhost");
1610        let second_child_id = room_id!("!second_child:localhost");
1611
1612        // The space is joined.
1613        server
1614            .sync_room(
1615                &client,
1616                JoinedRoomBuilder::new(space_id)
1617                    .add_state_event(factory.create(user_id, RoomVersionId::V11).with_space_type()),
1618            )
1619            .await;
1620
1621        // Build the `SpaceService` and expect the room to show up with no updates
1622        // pending
1623        let space_service = SpaceService::new(client.clone()).await;
1624
1625        let (initial_values, joined_spaces_subscriber) =
1626            space_service.subscribe_to_top_level_joined_spaces().await;
1627        pin_mut!(joined_spaces_subscriber);
1628        assert_pending!(joined_spaces_subscriber);
1629
1630        assert_eq!(
1631            initial_values,
1632            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0)].into()
1633        );
1634
1635        assert_eq!(
1636            space_service.top_level_joined_spaces().await,
1637            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0)]
1638        );
1639
1640        // Two children are added.
1641        server
1642            .sync_room(
1643                &client,
1644                JoinedRoomBuilder::new(space_id)
1645                    .add_state_event(
1646                        factory
1647                            .space_child(space_id.to_owned(), first_child_id.to_owned())
1648                            .sender(user_id),
1649                    )
1650                    .add_state_event(
1651                        factory
1652                            .space_child(space_id.to_owned(), second_child_id.to_owned())
1653                            .sender(user_id),
1654                    ),
1655            )
1656            .await;
1657
1658        // And expect the list to update.
1659        assert_eq!(
1660            space_service.top_level_joined_spaces().await,
1661            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2)]
1662        );
1663        assert_next_eq!(
1664            joined_spaces_subscriber,
1665            vec![
1666                VectorDiff::Clear,
1667                VectorDiff::Append {
1668                    values: vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2)]
1669                        .into()
1670                },
1671            ]
1672        );
1673
1674        // Then remove a child by replacing the state event with an empty one.
1675        server
1676            .sync_room(
1677                &client,
1678                JoinedRoomBuilder::new(space_id).add_state_bulk([Raw::new(&json!({
1679                    "content": {},
1680                    "type": "m.space.child",
1681                    "event_id": "$cancelsecondchild",
1682                    "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
1683                    "sender": user_id,
1684                    "state_key": second_child_id,
1685                }))
1686                .unwrap()
1687                .cast_unchecked()]),
1688            )
1689            .await;
1690
1691        // And expect the list to update.
1692        assert_eq!(
1693            space_service.top_level_joined_spaces().await,
1694            vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1)]
1695        );
1696        assert_next_eq!(
1697            joined_spaces_subscriber,
1698            vec![
1699                VectorDiff::Clear,
1700                VectorDiff::Append {
1701                    values: vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1)]
1702                        .into()
1703                },
1704            ]
1705        );
1706    }
1707
1708    async fn add_space_rooms(
1709        rooms: Vec<MockSpaceRoomParameters>,
1710        client: &Client,
1711        server: &MatrixMockServer,
1712        factory: &EventFactory,
1713        user_id: &UserId,
1714    ) {
1715        for parameters in rooms {
1716            let mut builder = JoinedRoomBuilder::new(parameters.room_id)
1717                .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
1718
1719            if let Some(order) = parameters.order {
1720                builder = builder.add_account_data(factory.space_order(order));
1721            }
1722
1723            for parent_id in parameters.parents {
1724                builder = builder.add_state_event(
1725                    factory
1726                        .space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
1727                        .sender(user_id),
1728                );
1729            }
1730
1731            for child_id in parameters.children {
1732                builder = builder.add_state_event(
1733                    factory
1734                        .space_child(parameters.room_id.to_owned(), child_id.to_owned())
1735                        .sender(user_id),
1736                );
1737            }
1738
1739            let mut power_levels = if let Some(power_level) = parameters.power_level {
1740                BTreeMap::from([(user_id.to_owned(), power_level.into())])
1741            } else {
1742                BTreeMap::from([(user_id.to_owned(), 100.into())])
1743            };
1744
1745            builder = builder.add_state_event(
1746                factory.power_levels(&mut power_levels).state_key("").sender(user_id),
1747            );
1748
1749            server.sync_room(client, builder).await;
1750        }
1751    }
1752
1753    struct MockSpaceRoomParameters {
1754        room_id: &'static RoomId,
1755        order: Option<&'static str>,
1756        parents: Vec<&'static RoomId>,
1757        children: Vec<&'static RoomId>,
1758        power_level: Option<i32>,
1759    }
1760
1761    fn any_room_id_and_space_room_order()
1762    -> impl Strategy<Value = (OwnedRoomId, Option<OwnedSpaceChildOrder>)> {
1763        let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
1764            RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
1765        });
1766
1767        let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
1768            order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
1769        });
1770
1771        (room_id, order)
1772    }
1773
1774    proptest! {
1775        #[test]
1776        fn sort_top_level_space_room_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
1777            v.sort_by(|a, b| {
1778                let (a_room_id, a_order) = a;
1779                let (b_room_id, b_order) = b;
1780
1781                let a = (a_room_id.as_ref(), a_order.as_deref());
1782                let b = (b_room_id.as_ref(), b_order.as_deref());
1783
1784                compare_top_level_space_rooms(a, b)
1785            })
1786        }
1787
1788        #[test]
1789        fn test_compare_top_level_rooms_reflexive(a in any_room_id_and_space_room_order()) {
1790            let (a_room_id, a_order) = a;
1791            let a = (a_room_id.as_ref(), a_order.as_deref());
1792
1793            prop_assert_eq!(compare_top_level_space_rooms(a, a), Ordering::Equal);
1794        }
1795
1796        #[test]
1797        fn test_compare_top_level_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
1798            let (a_room_id, a_order) = a;
1799            let (b_room_id, b_order) = b;
1800
1801            let a = (a_room_id.as_ref(), a_order.as_deref());
1802            let b = (b_room_id.as_ref(), b_order.as_deref());
1803
1804            let ab = compare_top_level_space_rooms(a, b);
1805            let ba = compare_top_level_space_rooms(b, a);
1806
1807            prop_assert_eq!(ab, ba.reverse());
1808        }
1809
1810        #[test]
1811        fn test_compare_top_level_rooms_transitive(
1812            a in any_room_id_and_space_room_order(),
1813            b in any_room_id_and_space_room_order(),
1814            c in any_room_id_and_space_room_order()
1815        ) {
1816            let (a_room_id, a_order) = a;
1817            let (b_room_id, b_order) = b;
1818            let (c_room_id, c_order) = c;
1819
1820            let a = (a_room_id.as_ref(), a_order.as_deref());
1821            let b = (b_room_id.as_ref(), b_order.as_deref());
1822            let c = (c_room_id.as_ref(), c_order.as_deref());
1823
1824            let ab = compare_top_level_space_rooms(a, b);
1825            let bc = compare_top_level_space_rooms(b, c);
1826            let ac = compare_top_level_space_rooms(a, c);
1827
1828            if ab == Ordering::Less && bc == Ordering::Less {
1829                prop_assert_eq!(ac, Ordering::Less);
1830            }
1831
1832            if ab == Ordering::Equal && bc == Ordering::Equal {
1833                prop_assert_eq!(ac, Ordering::Equal);
1834            }
1835
1836            if ab == Ordering::Greater && bc == Ordering::Greater {
1837                prop_assert_eq!(ac, Ordering::Greater);
1838            }
1839        }
1840    }
1841}