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