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