1use std::cmp::Ordering;
16
17use matrix_sdk::{Room, RoomHero, RoomHeroWithProfile, RoomState};
18use ruma::{
19 MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName,
20 OwnedSpaceChildOrder, RoomId,
21 events::{
22 room::{guest_access::GuestAccess, history_visibility::HistoryVisibility},
23 space::child::HierarchySpaceChildEvent,
24 },
25 room::{JoinRuleSummary, RoomSummary, RoomType},
26};
27
28#[derive(Debug, Clone, PartialEq)]
31pub struct SpaceRoom {
32 pub room_id: OwnedRoomId,
34 pub canonical_alias: Option<OwnedRoomAliasId>,
36 pub name: Option<String>,
38 pub display_name: String,
40 pub topic: Option<String>,
42 pub avatar_url: Option<OwnedMxcUri>,
44 pub room_type: Option<RoomType>,
46 pub num_joined_members: u64,
48 pub join_rule: Option<JoinRuleSummary>,
50 pub world_readable: Option<bool>,
52 pub guest_can_join: bool,
54
55 pub is_direct: Option<bool>,
60 pub children_count: u64,
62 pub state: Option<RoomState>,
64 pub heroes: Option<Vec<RoomHeroWithProfile>>,
66 pub via: Vec<OwnedServerName>,
68 pub suggested: bool,
72 pub is_dm: Option<bool>,
76}
77
78impl SpaceRoom {
79 pub(crate) async fn new_from_summary(
82 summary: &RoomSummary,
83 known_room: Option<Room>,
84 children_count: u64,
85 via: Vec<OwnedServerName>,
86 suggested: bool,
87 ) -> Self {
88 let num_joined_service_members = if let Some(known_room) = &known_room {
89 num_joined_service_members_or_default(known_room).await
90 } else {
91 0
92 };
93
94 let heroes =
95 if let Some(known_room) = &known_room { Some(known_room.heroes().await) } else { None };
96
97 let num_joined_members: u64 = summary.num_joined_members.into();
98 let display_name = matrix_sdk_base::Room::compute_display_name_with_fields(
99 summary.name.clone(),
100 summary.canonical_alias.as_deref(),
101 heroes.iter().flatten().map(RoomHero::from).collect(),
102 num_joined_members - num_joined_service_members,
103 )
104 .to_string();
105
106 Self {
107 room_id: summary.room_id.clone(),
108 canonical_alias: summary.canonical_alias.clone(),
109 name: summary.name.clone(),
110 display_name,
111 topic: summary.topic.clone(),
112 avatar_url: summary.avatar_url.clone(),
113 room_type: summary.room_type.clone(),
114 num_joined_members: summary.num_joined_members.into(),
115 join_rule: Some(summary.join_rule.clone()),
116 world_readable: Some(summary.world_readable),
117 guest_can_join: summary.guest_can_join,
118 is_direct: known_room.as_ref().map(|r| r.direct_targets_length() != 0),
119 children_count,
120 state: known_room.as_ref().map(|r| r.state()),
121 heroes,
122 via,
123 suggested,
124 is_dm: known_room.as_ref().map(|r| r.is_dm()),
125 }
126 }
127
128 pub(crate) async fn new_from_known(known_room: &Room, children_count: u64) -> Self {
130 let room_info = known_room.clone_info();
131
132 let name = room_info.name().map(ToOwned::to_owned);
133 let joined_service_members_count = num_joined_service_members_or_default(known_room).await;
134
135 let heroes = known_room.heroes().await;
136
137 let display_name = matrix_sdk_base::Room::compute_display_name_with_fields(
138 name.clone(),
139 room_info.canonical_alias(),
140 heroes.iter().map(RoomHero::from).collect(),
141 known_room.joined_members_count() - joined_service_members_count,
142 )
143 .to_string();
144
145 Self {
146 room_id: room_info.room_id().to_owned(),
147 canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
148 name,
149 display_name,
150 topic: room_info.topic().map(ToOwned::to_owned),
151 avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
152 room_type: room_info.room_type().cloned(),
153 num_joined_members: known_room.joined_members_count(),
154 join_rule: room_info.join_rule().cloned().map(Into::into),
155 world_readable: room_info
156 .history_visibility()
157 .map(|vis| *vis == HistoryVisibility::WorldReadable),
158 guest_can_join: known_room.guest_access() == GuestAccess::CanJoin,
159 is_direct: Some(known_room.direct_targets_length() != 0),
160 children_count,
161 state: Some(known_room.state()),
162 heroes: Some(heroes),
163 via: vec![],
164 suggested: false,
165 is_dm: known_room.compute_is_dm().await.ok(),
166 }
167 }
168
169 pub(crate) fn compare_rooms(
172 a: (&RoomId, Option<&SpaceRoomChildState>),
173 b: (&RoomId, Option<&SpaceRoomChildState>),
174 ) -> Ordering {
175 let (a_room_id, a_state) = a;
176 let (b_room_id, b_state) = b;
177
178 match (a_state, b_state) {
179 (Some(a_state), Some(b_state)) => match (&a_state.order, &b_state.order) {
180 (Some(a_order), Some(b_order)) => a_order
181 .cmp(b_order)
182 .then(a_state.origin_server_ts.cmp(&b_state.origin_server_ts))
183 .then(a_room_id.cmp(b_room_id)),
184 (Some(_), None) => Ordering::Less,
185 (None, Some(_)) => Ordering::Greater,
186 (None, None) => a_state
187 .origin_server_ts
188 .cmp(&b_state.origin_server_ts)
189 .then(a_room_id.cmp(b_room_id)),
190 },
191 (None, Some(_)) => Ordering::Greater,
192 (Some(_), None) => Ordering::Less,
193 (None, None) => a_room_id.cmp(b_room_id),
194 }
195 }
196}
197
198#[derive(Clone, Debug)]
199pub(crate) struct SpaceRoomChildState {
200 pub(crate) order: Option<OwnedSpaceChildOrder>,
201 pub(crate) origin_server_ts: MilliSecondsSinceUnixEpoch,
202}
203
204impl From<&HierarchySpaceChildEvent> for SpaceRoomChildState {
205 fn from(event: &HierarchySpaceChildEvent) -> Self {
206 SpaceRoomChildState {
207 order: event.content.order.clone(),
208 origin_server_ts: event.origin_server_ts,
209 }
210 }
211}
212
213async fn num_joined_service_members_or_default(room: &Room) -> u64 {
214 match room.compute_joined_service_members().await {
215 Ok(Some(service_members)) => service_members.len() as u64,
216 _ => room.service_members().map(|members| members.len() as u64).unwrap_or_default(),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use std::cmp::Ordering;
225
226 use matrix_sdk_test::async_test;
227 use proptest::prelude::*;
228 use ruma::{
229 MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, SpaceChildOrder, UInt, room_id, uint,
230 };
231
232 use crate::spaces::{SpaceRoom, room::SpaceRoomChildState};
233
234 #[async_test]
235 async fn test_room_list_sorting() {
236 assert_eq!(
239 SpaceRoom::compare_rooms((room_id!("!A:a.b"), None), (room_id!("!B:a.b"), None),),
240 Ordering::Less
241 );
242
243 assert_eq!(
244 SpaceRoom::compare_rooms(
245 (room_id!("!Marțolea:a.b"), None),
246 (room_id!("!Luana:a.b"), None),
247 ),
248 Ordering::Greater
249 );
250
251 assert_eq!(
254 SpaceRoom::compare_rooms(
255 (
256 room_id!("!Luana:a.b"),
257 Some(&SpaceRoomChildState {
258 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
259 order: None
260 })
261 ),
262 (
263 room_id!("!Marțolea:a.b"),
264 Some(&SpaceRoomChildState {
265 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
266 order: None
267 })
268 )
269 ),
270 Ordering::Greater
271 );
272
273 assert_eq!(
275 SpaceRoom::compare_rooms(
276 (
277 room_id!("!Joiana:a.b"),
278 Some(&SpaceRoomChildState {
279 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(123)),
280 order: Some(SpaceChildOrder::parse("second").unwrap())
281 })
282 ),
283 (
284 room_id!("!Mioara:a.b"),
285 Some(&SpaceRoomChildState {
286 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(234)),
287 order: Some(SpaceChildOrder::parse("first").unwrap())
288 })
289 ),
290 ),
291 Ordering::Greater
292 );
293
294 assert_eq!(
296 SpaceRoom::compare_rooms(
297 (
298 room_id!("!Joiana:a.b"),
299 Some(&SpaceRoomChildState {
300 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
301 order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
302 })
303 ),
304 (
305 room_id!("!Mioara:a.b"),
306 Some(&SpaceRoomChildState {
307 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
308 order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
309 })
310 ),
311 ),
312 Ordering::Greater
313 );
314
315 assert_eq!(
318 SpaceRoom::compare_rooms(
319 (
320 room_id!("!Joiana:a.b"),
321 Some(&SpaceRoomChildState {
322 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
323 order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
324 })
325 ),
326 (
327 room_id!("!Mioara:a.b"),
328 Some(&SpaceRoomChildState {
329 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
330 order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
331 })
332 ),
333 ),
334 Ordering::Less
335 );
336
337 assert_eq!(
340 SpaceRoom::compare_rooms(
341 (room_id!("!Viola:a.b"), None),
342 (
343 room_id!("!Sâmbotina:a.b"),
344 Some(&SpaceRoomChildState {
345 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
346 order: None
347 })
348 ),
349 ),
350 Ordering::Greater
351 );
352
353 assert_eq!(
356 SpaceRoom::compare_rooms(
357 (
358 room_id!("!Sâmbotina:a.b"),
359 Some(&SpaceRoomChildState {
360 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
361 order: None
362 })
363 ),
364 (
365 room_id!("!Dumana:a.b"),
366 Some(&SpaceRoomChildState {
367 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
368 order: Some(SpaceChildOrder::parse("Some pasture").unwrap())
369 })
370 ),
371 ),
372 Ordering::Greater
373 );
374 }
375
376 #[test]
386 fn test_compare_rooms_minimal_transitive_failure() {
387 let (a_room_id, a_state) = (room_id!("!Q"), None);
388
389 let (b_room_id, b_state) = (
390 room_id!("!A"),
391 Some(SpaceRoomChildState {
392 order: None,
393 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10)),
394 }),
395 );
396
397 let (c_room_id, c_state) = (
398 room_id!("!a"),
399 Some(SpaceRoomChildState {
400 order: None,
401 origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
402 }),
403 );
404
405 let a = (a_room_id, a_state.as_ref());
406 let b = (b_room_id, b_state.as_ref());
407 let c = (c_room_id, c_state.as_ref());
408
409 let ab = SpaceRoom::compare_rooms(a, b);
410 let bc = SpaceRoom::compare_rooms(b, c);
411 let ac = SpaceRoom::compare_rooms(a, c);
412
413 assert_eq!(ab, Ordering::Greater, "a > b should hold");
414 assert_eq!(bc, Ordering::Greater, "b > c should hold");
415 assert_eq!(ac, Ordering::Greater, "therefore a > c should be true as well");
416 }
417
418 fn any_room_id_and_space_room_order()
419 -> impl Strategy<Value = (OwnedRoomId, Option<SpaceRoomChildState>)> {
420 let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
421 RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
422 });
423
424 let timestamp = any::<u8>().prop_map(|t| MilliSecondsSinceUnixEpoch(UInt::from(t)));
425
426 let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
427 order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
428 });
429
430 let state = (order, timestamp)
431 .prop_map(|(o, t)| SpaceRoomChildState { order: o, origin_server_ts: t });
432
433 let state = prop::option::of(state);
434
435 (room_id, state)
436 }
437
438 proptest! {
439 #[test]
440 fn test_sort_space_room_children_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
441 v.sort_by(|a, b| {
442 let (a_room_id, a_state) = a;
443 let (b_room_id, b_state) = b;
444
445 let a = (a_room_id.as_ref(), a_state.as_ref());
446 let b = (b_room_id.as_ref(), b_state.as_ref());
447
448 SpaceRoom::compare_rooms(a, b)
449 })
450 }
451
452 #[test]
453 fn test_compare_rooms_reflexive(a in any_room_id_and_space_room_order()) {
454 let (a_room_id, a_state) = a;
455 let a = (a_room_id.as_ref(), a_state.as_ref());
456
457 prop_assert_eq!(SpaceRoom::compare_rooms(a, a), Ordering::Equal);
458 }
459
460 #[test]
461 fn test_compare_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
462 let (a_room_id, a_state) = a;
463 let (b_room_id, b_state) = b;
464
465 let a = (a_room_id.as_ref(), a_state.as_ref());
466 let b = (b_room_id.as_ref(), b_state.as_ref());
467
468 let ab = SpaceRoom::compare_rooms(a, b);
469 let ba = SpaceRoom::compare_rooms(b, a);
470
471 prop_assert_eq!(ab, ba.reverse());
472 }
473
474 #[test]
475 fn test_compare_rooms_transitive(
476 a in any_room_id_and_space_room_order(),
477 b in any_room_id_and_space_room_order(),
478 c in any_room_id_and_space_room_order()
479 ) {
480 let (a_room_id, a_state) = a;
481 let (b_room_id, b_state) = b;
482 let (c_room_id, c_state) = c;
483
484 let a = (a_room_id.as_ref(), a_state.as_ref());
485 let b = (b_room_id.as_ref(), b_state.as_ref());
486 let c = (c_room_id.as_ref(), c_state.as_ref());
487
488 let ab = SpaceRoom::compare_rooms(a, b);
489 let bc = SpaceRoom::compare_rooms(b, c);
490 let ac = SpaceRoom::compare_rooms(a, c);
491
492 if ab == Ordering::Less && bc == Ordering::Less {
493 prop_assert_eq!(ac, Ordering::Less);
494 }
495
496 if ab == Ordering::Equal && bc == Ordering::Equal {
497 prop_assert_eq!(ac, Ordering::Equal);
498 }
499
500 if ab == Ordering::Greater && bc == Ordering::Greater {
501 prop_assert_eq!(ac, Ordering::Greater);
502 }
503 }
504 }
505}