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