Skip to content

Commit d117532

Browse files
authored
feat(spaces): add support for MSC3230 and top level space order (matrix-org#5799)
This is an unstable feature but as per [MSC3230](matrix-org/matrix-spec-proposals#3230) each space room might have an optional `m.space_order`/`org.matrix.msc3230.space_order` string field in its room account data defining the lexicographical order in which the spaces should be displayed, with spaces missing this field shown at the bottom and ordered by their room id.
1 parent 34c5e24 commit d117532

File tree

3 files changed

+107
-13
lines changed

3 files changed

+107
-13
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "bbb4a14e14864d364b78d60553
7474
"compat-encrypted-stickers",
7575
"compat-lax-room-create-deser",
7676
"compat-lax-room-topic-deser",
77+
"unstable-msc3230",
7778
"unstable-msc3401",
7879
"unstable-msc3488",
7980
"unstable-msc3489",
@@ -85,7 +86,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "bbb4a14e14864d364b78d60553
8586
"unstable-msc4286",
8687
"unstable-msc4306",
8788
"unstable-msc4308",
88-
"unstable-msc4310"
89+
"unstable-msc4310",
8990
] }
9091
sentry = { version = "0.42.0", default-features = false }
9192
sentry-tracing = "0.42.0"

crates/matrix-sdk-ui/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.
66

77
## [Unreleased] - ReleaseDate
88

9+
### Features
10+
- Add support for top level space ordering through [MSC3230](https://github.com/matrix-org/matrix-spec-proposals/pull/3230)
11+
and `m.space_order` room account data fields ([#5799](https://github.com/matrix-org/matrix-rust-sdk/pull/5799))
12+
913
### Refactor
1014

1115
- `TimelineFocusKind::Event` can now handle both the existing event pagination and thread pagination if the focused

crates/matrix-sdk-ui/src/spaces/mod.rs

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,20 @@
2727
//! - `SpaceRoomList`: A component for retrieving a space's children rooms and
2828
//! their details.
2929
30-
use std::sync::Arc;
30+
use std::{cmp::Ordering, collections::HashMap, sync::Arc};
3131

3232
use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
3333
use futures_util::pin_mut;
3434
use imbl::Vector;
35+
use itertools::Itertools;
3536
use matrix_sdk::{
3637
Client, Error as SDKError, deserialized_responses::SyncOrStrippedState, executor::AbortOnDrop,
3738
};
3839
use matrix_sdk_common::executor::spawn;
3940
use ruma::{
4041
OwnedRoomId, RoomId,
4142
events::{
42-
SyncStateEvent,
43+
self, SyncStateEvent,
4344
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
4445
},
4546
};
@@ -291,20 +292,46 @@ impl SpaceService {
291292

292293
let root_nodes = graph.root_nodes();
293294

294-
let joined_space_rooms = joined_spaces
295+
// Proceed with filtering to the top level spaces, sorting them by their
296+
// (optional) order field (as defined in MSC3230) and then mapping them
297+
// to `SpaceRoom`s.
298+
let top_level_spaces = joined_spaces
295299
.iter()
296-
.filter_map(|room| {
297-
let room_id = room.room_id();
300+
.filter(|room| root_nodes.contains(&room.room_id()))
301+
.collect::<Vec<_>>();
302+
303+
let mut top_level_space_order = HashMap::new();
304+
for space in &top_level_spaces {
305+
if let Ok(Some(raw_event)) =
306+
space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
307+
&& let Ok(event) = raw_event.deserialize()
308+
{
309+
top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
310+
}
311+
}
298312

299-
if root_nodes.contains(&room_id) {
300-
Some(SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64))
301-
} else {
302-
None
313+
let top_level_spaces = top_level_spaces
314+
.iter()
315+
.sorted_by(|a, b| {
316+
// MSC3230: lexicographically by `order` and then by room ID
317+
match (
318+
top_level_space_order.get(a.room_id()),
319+
top_level_space_order.get(b.room_id()),
320+
) {
321+
(Some(a_order), Some(b_order)) => {
322+
a_order.cmp(b_order).then(a.room_id().cmp(b.room_id()))
323+
}
324+
(Some(_), None) => Ordering::Less,
325+
(None, Some(_)) => Ordering::Greater,
326+
(None, None) => a.room_id().cmp(b.room_id()),
303327
}
304328
})
329+
.map(|room| {
330+
SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
331+
})
305332
.collect();
306333

307-
(joined_space_rooms, graph)
334+
(top_level_spaces, graph)
308335
}
309336
}
310337

@@ -315,9 +342,11 @@ mod tests {
315342
use futures_util::{StreamExt, pin_mut};
316343
use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
317344
use matrix_sdk_test::{
318-
JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
345+
JoinedRoomBuilder, LeftRoomBuilder, RoomAccountDataTestEvent, async_test,
346+
event_factory::EventFactory,
319347
};
320-
use ruma::{RoomVersionId, owned_room_id, room_id};
348+
use ruma::{RoomVersionId, UserId, owned_room_id, room_id};
349+
use serde_json::json;
321350
use stream_assert::{assert_next_eq, assert_pending};
322351

323352
use super::*;
@@ -554,4 +583,64 @@ mod tests {
554583
vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
555584
);
556585
}
586+
587+
#[async_test]
588+
async fn test_top_level_space_order() {
589+
let server = MatrixMockServer::new().await;
590+
let client = server.client_builder().build().await;
591+
592+
server.mock_room_state_encryption().plain().mount().await;
593+
594+
add_space_rooms_with(
595+
vec![
596+
(room_id!("!2:a.b"), Some("2")),
597+
(room_id!("!4:a.b"), None),
598+
(room_id!("!3:a.b"), None),
599+
(room_id!("!1:a.b"), Some("1")),
600+
],
601+
&client,
602+
&server,
603+
&EventFactory::new(),
604+
client.user_id().unwrap(),
605+
)
606+
.await;
607+
608+
let space_service = SpaceService::new(client.clone());
609+
610+
// Space with an `order` field set should come first in lexicographic
611+
// order and rest sorted by room ID.
612+
assert_eq!(
613+
space_service.joined_spaces().await,
614+
vec![
615+
SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0),
616+
SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0),
617+
SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0),
618+
SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0),
619+
]
620+
);
621+
}
622+
623+
async fn add_space_rooms_with(
624+
rooms: Vec<(&RoomId, Option<&str>)>,
625+
client: &Client,
626+
server: &MatrixMockServer,
627+
factory: &EventFactory,
628+
user_id: &UserId,
629+
) {
630+
for (room_id, order) in rooms {
631+
let mut builder = JoinedRoomBuilder::new(room_id)
632+
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
633+
634+
if let Some(order) = order {
635+
builder = builder.add_account_data(RoomAccountDataTestEvent::Custom(json!({
636+
"type": "m.space_order",
637+
"content": {
638+
"order": order
639+
}
640+
})));
641+
}
642+
643+
server.sync_room(client, builder).await;
644+
}
645+
}
557646
}

0 commit comments

Comments
 (0)