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