use std::cmp::Ordering;
use matrix_sdk::{Room, RoomHero, RoomState};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedServerName,
OwnedSpaceChildOrder, RoomId,
events::{
room::{guest_access::GuestAccess, history_visibility::HistoryVisibility},
space::child::HierarchySpaceChildEvent,
},
room::{JoinRuleSummary, RoomSummary, RoomType},
};
#[derive(Debug, Clone, PartialEq)]
pub struct SpaceRoom {
pub room_id: OwnedRoomId,
pub canonical_alias: Option<OwnedRoomAliasId>,
pub name: Option<String>,
pub display_name: String,
pub topic: Option<String>,
pub avatar_url: Option<OwnedMxcUri>,
pub room_type: Option<RoomType>,
pub num_joined_members: u64,
pub join_rule: Option<JoinRuleSummary>,
pub world_readable: Option<bool>,
pub guest_can_join: bool,
pub is_direct: Option<bool>,
pub children_count: u64,
pub state: Option<RoomState>,
pub heroes: Option<Vec<RoomHero>>,
pub via: Vec<OwnedServerName>,
pub suggested: bool,
pub is_dm: Option<bool>,
}
impl SpaceRoom {
pub(crate) fn new_from_summary(
summary: &RoomSummary,
known_room: Option<Room>,
children_count: u64,
via: Vec<OwnedServerName>,
suggested: bool,
) -> Self {
let display_name = matrix_sdk_base::Room::compute_display_name_with_fields(
summary.name.clone(),
summary.canonical_alias.as_deref(),
known_room.as_ref().map(|r| r.heroes().to_vec()).unwrap_or_default(),
summary.num_joined_members.into(),
)
.to_string();
Self {
room_id: summary.room_id.clone(),
canonical_alias: summary.canonical_alias.clone(),
name: summary.name.clone(),
display_name,
topic: summary.topic.clone(),
avatar_url: summary.avatar_url.clone(),
room_type: summary.room_type.clone(),
num_joined_members: summary.num_joined_members.into(),
join_rule: Some(summary.join_rule.clone()),
world_readable: Some(summary.world_readable),
guest_can_join: summary.guest_can_join,
is_direct: known_room.as_ref().map(|r| r.direct_targets_length() != 0),
children_count,
state: known_room.as_ref().map(|r| r.state()),
heroes: known_room.as_ref().map(|r| r.heroes()),
via,
suggested,
is_dm: known_room.as_ref().map(|r| r.is_dm()),
}
}
pub(crate) fn new_from_known(known_room: &Room, children_count: u64) -> Self {
let room_info = known_room.clone_info();
let name = room_info.name().map(ToOwned::to_owned);
let display_name = matrix_sdk_base::Room::compute_display_name_with_fields(
name.clone(),
room_info.canonical_alias(),
room_info.heroes().to_vec(),
known_room.joined_members_count(),
)
.to_string();
Self {
room_id: room_info.room_id().to_owned(),
canonical_alias: room_info.canonical_alias().map(ToOwned::to_owned),
name,
display_name,
topic: room_info.topic().map(ToOwned::to_owned),
avatar_url: room_info.avatar_url().map(ToOwned::to_owned),
room_type: room_info.room_type().cloned(),
num_joined_members: known_room.joined_members_count(),
join_rule: room_info.join_rule().cloned().map(Into::into),
world_readable: room_info
.history_visibility()
.map(|vis| *vis == HistoryVisibility::WorldReadable),
guest_can_join: known_room.guest_access() == GuestAccess::CanJoin,
is_direct: Some(known_room.direct_targets_length() != 0),
children_count,
state: Some(known_room.state()),
heroes: Some(room_info.heroes().to_vec()),
via: vec![],
suggested: false,
is_dm: Some(known_room.is_dm()),
}
}
pub(crate) fn compare_rooms(
a: (&RoomId, Option<&SpaceRoomChildState>),
b: (&RoomId, Option<&SpaceRoomChildState>),
) -> Ordering {
let (a_room_id, a_state) = a;
let (b_room_id, b_state) = b;
match (a_state, b_state) {
(Some(a_state), Some(b_state)) => match (&a_state.order, &b_state.order) {
(Some(a_order), Some(b_order)) => a_order
.cmp(b_order)
.then(a_state.origin_server_ts.cmp(&b_state.origin_server_ts))
.then(a_room_id.cmp(b_room_id)),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => a_state
.origin_server_ts
.cmp(&b_state.origin_server_ts)
.then(a_room_id.cmp(b_room_id)),
},
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(None, None) => a_room_id.cmp(b_room_id),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct SpaceRoomChildState {
pub(crate) order: Option<OwnedSpaceChildOrder>,
pub(crate) origin_server_ts: MilliSecondsSinceUnixEpoch,
}
impl From<&HierarchySpaceChildEvent> for SpaceRoomChildState {
fn from(event: &HierarchySpaceChildEvent) -> Self {
SpaceRoomChildState {
order: event.content.order.clone(),
origin_server_ts: event.origin_server_ts,
}
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use matrix_sdk_test::async_test;
use proptest::prelude::*;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, SpaceChildOrder, UInt, room_id, uint,
};
use crate::spaces::{SpaceRoom, room::SpaceRoomChildState};
#[async_test]
async fn test_room_list_sorting() {
assert_eq!(
SpaceRoom::compare_rooms((room_id!("!A:a.b"), None), (room_id!("!B:a.b"), None),),
Ordering::Less
);
assert_eq!(
SpaceRoom::compare_rooms(
(room_id!("!Marțolea:a.b"), None),
(room_id!("!Luana:a.b"), None),
),
Ordering::Greater
);
assert_eq!(
SpaceRoom::compare_rooms(
(
room_id!("!Luana:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
order: None
})
),
(
room_id!("!Marțolea:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
order: None
})
)
),
Ordering::Greater
);
assert_eq!(
SpaceRoom::compare_rooms(
(
room_id!("!Joiana:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(123)),
order: Some(SpaceChildOrder::parse("second").unwrap())
})
),
(
room_id!("!Mioara:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(234)),
order: Some(SpaceChildOrder::parse("first").unwrap())
})
),
),
Ordering::Greater
);
assert_eq!(
SpaceRoom::compare_rooms(
(
room_id!("!Joiana:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
})
),
(
room_id!("!Mioara:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
})
),
),
Ordering::Greater
);
assert_eq!(
SpaceRoom::compare_rooms(
(
room_id!("!Joiana:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
})
),
(
room_id!("!Mioara:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
order: Some(SpaceChildOrder::parse("Same pasture").unwrap())
})
),
),
Ordering::Less
);
assert_eq!(
SpaceRoom::compare_rooms(
(room_id!("!Viola:a.b"), None),
(
room_id!("!Sâmbotina:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
order: None
})
),
),
Ordering::Greater
);
assert_eq!(
SpaceRoom::compare_rooms(
(
room_id!("!Sâmbotina:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
order: None
})
),
(
room_id!("!Dumana:a.b"),
Some(&SpaceRoomChildState {
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)),
order: Some(SpaceChildOrder::parse("Some pasture").unwrap())
})
),
),
Ordering::Greater
);
}
#[test]
fn test_compare_rooms_minimal_transitive_failure() {
let (a_room_id, a_state) = (room_id!("!Q"), None);
let (b_room_id, b_state) = (
room_id!("!A"),
Some(SpaceRoomChildState {
order: None,
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10)),
}),
);
let (c_room_id, c_state) = (
room_id!("!a"),
Some(SpaceRoomChildState {
order: None,
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(0)),
}),
);
let a = (a_room_id, a_state.as_ref());
let b = (b_room_id, b_state.as_ref());
let c = (c_room_id, c_state.as_ref());
let ab = SpaceRoom::compare_rooms(a, b);
let bc = SpaceRoom::compare_rooms(b, c);
let ac = SpaceRoom::compare_rooms(a, c);
assert_eq!(ab, Ordering::Greater, "a > b should hold");
assert_eq!(bc, Ordering::Greater, "b > c should hold");
assert_eq!(ac, Ordering::Greater, "therefore a > c should be true as well");
}
fn any_room_id_and_space_room_order()
-> impl Strategy<Value = (OwnedRoomId, Option<SpaceRoomChildState>)> {
let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
});
let timestamp = any::<u8>().prop_map(|t| MilliSecondsSinceUnixEpoch(UInt::from(t)));
let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
});
let state = (order, timestamp)
.prop_map(|(o, t)| SpaceRoomChildState { order: o, origin_server_ts: t });
let state = prop::option::of(state);
(room_id, state)
}
proptest! {
#[test]
fn test_sort_space_room_children_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
v.sort_by(|a, b| {
let (a_room_id, a_state) = a;
let (b_room_id, b_state) = b;
let a = (a_room_id.as_ref(), a_state.as_ref());
let b = (b_room_id.as_ref(), b_state.as_ref());
SpaceRoom::compare_rooms(a, b)
})
}
#[test]
fn test_compare_rooms_reflexive(a in any_room_id_and_space_room_order()) {
let (a_room_id, a_state) = a;
let a = (a_room_id.as_ref(), a_state.as_ref());
prop_assert_eq!(SpaceRoom::compare_rooms(a, a), Ordering::Equal);
}
#[test]
fn test_compare_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
let (a_room_id, a_state) = a;
let (b_room_id, b_state) = b;
let a = (a_room_id.as_ref(), a_state.as_ref());
let b = (b_room_id.as_ref(), b_state.as_ref());
let ab = SpaceRoom::compare_rooms(a, b);
let ba = SpaceRoom::compare_rooms(b, a);
prop_assert_eq!(ab, ba.reverse());
}
#[test]
fn test_compare_rooms_transitive(
a in any_room_id_and_space_room_order(),
b in any_room_id_and_space_room_order(),
c in any_room_id_and_space_room_order()
) {
let (a_room_id, a_state) = a;
let (b_room_id, b_state) = b;
let (c_room_id, c_state) = c;
let a = (a_room_id.as_ref(), a_state.as_ref());
let b = (b_room_id.as_ref(), b_state.as_ref());
let c = (c_room_id.as_ref(), c_state.as_ref());
let ab = SpaceRoom::compare_rooms(a, b);
let bc = SpaceRoom::compare_rooms(b, c);
let ac = SpaceRoom::compare_rooms(a, c);
if ab == Ordering::Less && bc == Ordering::Less {
prop_assert_eq!(ac, Ordering::Less);
}
if ab == Ordering::Equal && bc == Ordering::Equal {
prop_assert_eq!(ac, Ordering::Equal);
}
if ab == Ordering::Greater && bc == Ordering::Greater {
prop_assert_eq!(ac, Ordering::Greater);
}
}
}
}