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