matrix_sdk_ui/spaces/
mod.rs

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