Skip to content

Commit 8bb9a3b

Browse files
committed
Setup a simple space service and some unit tests
1 parent 683f0f4 commit 8bb9a3b

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

bindings/matrix-sdk-ffi/src/client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ use matrix_sdk_ui::{
4848
NotificationClient as MatrixNotificationClient,
4949
NotificationProcessSetup as MatrixNotificationProcessSetup,
5050
},
51+
spaces::SpaceService as UISpaceService,
5152
unable_to_decrypt_hook::UtdHookManager,
5253
};
5354
use mime::Mime;
@@ -106,6 +107,7 @@ use crate::{
106107
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
107108
},
108109
runtime::get_runtime_handle,
110+
spaces::SpaceService,
109111
sync_service::{SyncService, SyncServiceBuilder},
110112
task_handle::TaskHandle,
111113
utd::{UnableToDecryptDelegate, UtdHook},
@@ -1259,6 +1261,11 @@ impl Client {
12591261
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
12601262
}
12611263

1264+
pub fn spaces_service(&self) -> Arc<SpaceService> {
1265+
let inner = UISpaceService::new((*self.inner).clone());
1266+
Arc::new(SpaceService::new(inner))
1267+
}
1268+
12621269
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
12631270
let inner = self.inner.notification_settings().await;
12641271

bindings/matrix-sdk-ffi/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod room_preview;
2525
mod ruma;
2626
mod runtime;
2727
mod session_verification;
28+
mod spaces;
2829
mod sync_service;
2930
mod task_handle;
3031
mod timeline;

bindings/matrix-sdk-ffi/src/spaces.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use matrix_sdk_ui::spaces::SpaceService as UISpaceService;
16+
use ruma::RoomId;
17+
18+
use crate::error::ClientError;
19+
20+
#[derive(uniffi::Object)]
21+
pub struct SpaceService {
22+
inner: UISpaceService,
23+
}
24+
25+
impl SpaceService {
26+
/// Create a new instance of `SpaceService`.
27+
pub fn new(inner: UISpaceService) -> Self {
28+
Self { inner }
29+
}
30+
}
31+
32+
#[matrix_sdk_ffi_macros::export]
33+
impl SpaceService {
34+
/// Get the list of joined spaces.
35+
pub fn joined_spaces(&self) -> Vec<String> {
36+
self.inner.joined_spaces().into_iter().map(|id| id.to_string()).collect()
37+
}
38+
39+
/// Get the top-level children for a given space.
40+
pub async fn top_level_children_for(
41+
&self,
42+
space_id: String,
43+
) -> Result<Vec<String>, ClientError> {
44+
let space_id = RoomId::parse(space_id)?;
45+
let children = self.inner.top_level_children_for(space_id).await?;
46+
Ok(children.into_iter().map(|id| id.to_string()).collect())
47+
}
48+
}

crates/matrix-sdk-ui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use ruma::html::HtmlSanitizerMode;
2121
pub mod encryption_sync_service;
2222
pub mod notification_client;
2323
pub mod room_list_service;
24+
pub mod spaces;
2425
pub mod sync_service;
2526
pub mod timeline;
2627
pub mod unable_to_decrypt_hook;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
//! See [`SpaceDiscoveryService`] for details.
18+
19+
use matrix_sdk::{Client, Error};
20+
use ruma::OwnedRoomId;
21+
use ruma::api::client::space::get_hierarchy;
22+
23+
pub struct SpaceService {
24+
client: Client,
25+
}
26+
27+
impl SpaceService {
28+
pub fn new(client: Client) -> Self {
29+
Self { client }
30+
}
31+
32+
pub fn joined_spaces(&self) -> Vec<OwnedRoomId> {
33+
self.client
34+
.joined_rooms()
35+
.into_iter()
36+
.filter_map(|room| if room.is_space() { Some(room.room_id().to_owned()) } else { None })
37+
.collect()
38+
}
39+
40+
pub async fn top_level_children_for(
41+
&self,
42+
space_id: OwnedRoomId,
43+
) -> Result<Vec<OwnedRoomId>, Error> {
44+
let request = get_hierarchy::v1::Request::new(space_id.clone());
45+
46+
let result = self.client.send(request).await?;
47+
48+
println!("Top level children for space {}: {:?}", space_id, result.rooms);
49+
50+
Ok(vec![])
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::*;
57+
use assert_matches2::assert_let;
58+
use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
59+
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, async_test};
60+
use ruma::room_id;
61+
use tokio_stream::StreamExt;
62+
63+
#[async_test]
64+
async fn test_joined_spaces() {
65+
let server = MatrixMockServer::new().await;
66+
let client = server.client_builder().build().await;
67+
let space_discovery_service = SpaceService::new(client.clone());
68+
69+
server.mock_room_state_encryption().plain().mount().await;
70+
71+
let parent_space_id = room_id!("!3:example.org");
72+
let child_space_id_1 = room_id!("!1:example.org");
73+
let child_space_id_2 = room_id!("!2:example.org");
74+
75+
server
76+
.sync_room(
77+
&client,
78+
JoinedRoomBuilder::new(child_space_id_1)
79+
.add_state_event(StateTestEvent::CreateSpace)
80+
.add_state_event(StateTestEvent::SpaceParent {
81+
parent: parent_space_id.to_owned(),
82+
child: child_space_id_1.to_owned(),
83+
}),
84+
)
85+
.await;
86+
87+
server
88+
.sync_room(
89+
&client,
90+
JoinedRoomBuilder::new(child_space_id_2)
91+
.add_state_event(StateTestEvent::CreateSpace)
92+
.add_state_event(StateTestEvent::SpaceParent {
93+
parent: parent_space_id.to_owned(),
94+
child: child_space_id_2.to_owned(),
95+
}),
96+
)
97+
.await;
98+
server
99+
.sync_room(
100+
&client,
101+
JoinedRoomBuilder::new(parent_space_id)
102+
.add_state_event(StateTestEvent::CreateSpace)
103+
.add_state_event(StateTestEvent::SpaceChild {
104+
parent: parent_space_id.to_owned(),
105+
child: child_space_id_1.to_owned(),
106+
})
107+
.add_state_event(StateTestEvent::SpaceChild {
108+
parent: parent_space_id.to_owned(),
109+
child: child_space_id_2.to_owned(),
110+
}),
111+
)
112+
.await;
113+
114+
assert_eq!(
115+
space_discovery_service.joined_spaces(),
116+
vec![child_space_id_1, child_space_id_2, parent_space_id]
117+
);
118+
119+
let parent_space = client.get_room(parent_space_id).unwrap();
120+
assert!(parent_space.is_space());
121+
122+
let spaces: Vec<ParentSpace> = client
123+
.get_room(child_space_id_1)
124+
.unwrap()
125+
.parent_spaces()
126+
.await
127+
.unwrap()
128+
.map(Result::unwrap)
129+
.collect()
130+
.await;
131+
132+
assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
133+
assert_eq!(parent.room_id(), parent_space.room_id());
134+
135+
let spaces: Vec<ParentSpace> = client
136+
.get_room(child_space_id_2)
137+
.unwrap()
138+
.parent_spaces()
139+
.await
140+
.unwrap()
141+
.map(Result::unwrap)
142+
.collect()
143+
.await;
144+
145+
assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
146+
assert_eq!(parent.room_id(), parent_space.room_id());
147+
}
148+
}

testing/matrix-sdk-test/src/sync_builder/test_event.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use ruma::{
2+
OwnedRoomId,
23
api::client::sync::sync_events::StrippedState,
34
events::{
45
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent,
@@ -15,6 +16,7 @@ pub enum StateTestEvent {
1516
Alias,
1617
Aliases,
1718
Create,
19+
CreateSpace,
1820
Encryption,
1921
HistoryVisibility,
2022
JoinRules,
@@ -31,6 +33,8 @@ pub enum StateTestEvent {
3133
RoomName,
3234
RoomPinnedEvents,
3335
RoomTopic,
36+
SpaceChild { parent: OwnedRoomId, child: OwnedRoomId },
37+
SpaceParent { parent: OwnedRoomId, child: OwnedRoomId },
3438
Custom(JsonValue),
3539
}
3640

@@ -40,6 +44,7 @@ impl From<StateTestEvent> for JsonValue {
4044
StateTestEvent::Alias => test_json::sync_events::ALIAS.to_owned(),
4145
StateTestEvent::Aliases => test_json::sync_events::ALIASES.to_owned(),
4246
StateTestEvent::Create => test_json::sync_events::CREATE.to_owned(),
47+
StateTestEvent::CreateSpace => test_json::sync_events::CREATE_SPACE.to_owned(),
4348
StateTestEvent::Encryption => test_json::sync_events::ENCRYPTION.to_owned(),
4449
StateTestEvent::HistoryVisibility => {
4550
test_json::sync_events::HISTORY_VISIBILITY.to_owned()
@@ -62,6 +67,18 @@ impl From<StateTestEvent> for JsonValue {
6267
StateTestEvent::RoomName => test_json::sync_events::NAME.to_owned(),
6368
StateTestEvent::RoomPinnedEvents => test_json::sync_events::PINNED_EVENTS.to_owned(),
6469
StateTestEvent::RoomTopic => test_json::sync_events::TOPIC.to_owned(),
70+
StateTestEvent::SpaceChild { parent, child } => {
71+
let mut json = test_json::sync_events::SPACE_CHILD.to_owned();
72+
json["room_id"] = JsonValue::String(parent.to_string());
73+
json["state_key"] = JsonValue::String(child.to_string());
74+
json
75+
}
76+
StateTestEvent::SpaceParent { parent, child } => {
77+
let mut json = test_json::sync_events::SPACE_PARENT.to_owned();
78+
json["state_key"] = JsonValue::String(parent.to_string());
79+
json["room_id"] = JsonValue::String(child.to_string());
80+
json
81+
}
6582
StateTestEvent::Custom(json) => json,
6683
}
6784
}

testing/matrix-sdk-test/src/test_json/sync_events.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ pub static CREATE: Lazy<JsonValue> = Lazy::new(|| {
5757
})
5858
});
5959

60+
pub static CREATE_SPACE: Lazy<JsonValue> = Lazy::new(|| {
61+
json!({
62+
"content": {
63+
"creator": "@example:localhost",
64+
"m.federate": true,
65+
"room_version": "1",
66+
"type": "m.space",
67+
},
68+
"event_id": "$151957878228ekrDs:localhost",
69+
"origin_server_ts": 15195787,
70+
"sender": "@example:localhost",
71+
"state_key": "",
72+
"type": "m.room.create",
73+
"unsigned": {
74+
"age": 139298
75+
}
76+
})
77+
});
78+
6079
pub static DIRECT: Lazy<JsonValue> = Lazy::new(|| {
6180
json!({
6281
"content": {
@@ -726,3 +745,43 @@ pub static IGNORED_USER_LIST: Lazy<JsonValue> = Lazy::new(|| {
726745
"type": "m.ignored_user_list",
727746
})
728747
});
748+
749+
pub static SPACE_CHILD: Lazy<JsonValue> = Lazy::new(|| {
750+
json!({
751+
"content": {
752+
"via": [
753+
"example.org",
754+
"other.example.org"
755+
]
756+
},
757+
"event_id": "$space_child_event_id:example.org",
758+
"room_id": "!parent_room_id:example.org",
759+
"state_key": "!child_room_id:example.org",
760+
"origin_server_ts": 151957879,
761+
"sender": "@example:example.org",
762+
"type": "m.space.child",
763+
"unsigned": {
764+
"age": 1234,
765+
}
766+
})
767+
});
768+
769+
pub static SPACE_PARENT: Lazy<JsonValue> = Lazy::new(|| {
770+
json!({
771+
"content": {
772+
"via": [
773+
"example.org",
774+
"other.example.org"
775+
]
776+
},
777+
"event_id": "$space_parent_event_id:example.org",
778+
"room_id": "!child_room_id:example.org",
779+
"state_key": "!parent_room_id:example.org",
780+
"origin_server_ts": 151957879,
781+
"sender": "@example:example.org",
782+
"type": "m.space.parent",
783+
"unsigned": {
784+
"age": 1234,
785+
}
786+
})
787+
});

0 commit comments

Comments
 (0)