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