feat(ui): Add the recency, name and or sorters for the RoomList.

This patch adds 3 sorters for the `RoomList`: `recency`, `name` and
`or`.
This commit is contained in:
Ivan Enderlin
2024-06-26 14:55:27 +02:00
parent daf878fa7f
commit ec80c6ff7b
5 changed files with 562 additions and 0 deletions

View File

@@ -54,6 +54,7 @@
pub mod filters;
mod room;
mod room_list;
pub mod sorters;
mod state;
use std::{

View File

@@ -0,0 +1,38 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A collection of room sorters.
mod name;
mod or;
mod recency;
use std::cmp::Ordering;
pub use name::new_sorter as new_sorter_name;
pub use or::new_sorter as new_sorter_or;
pub use recency::new_sorter as new_sorter_recency;
use super::Room;
/// A trait “alias” that represents a _sorter_.
///
/// A sorter is simply a function that receives two `&Room`s and returns a
/// [`Ordering`].
pub trait Sorter: Fn(&Room, &Room) -> Ordering {}
impl<F> Sorter for F where F: Fn(&Room, &Room) -> Ordering {}
/// Type alias for a boxed sorter function.
pub type BoxedSorterFn = Box<dyn Sorter + Send + Sync>;

View File

@@ -0,0 +1,129 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::Ordering;
use super::{Room, Sorter};
struct NameMatcher<F>
where
F: Fn(&Room, &Room) -> (Option<String>, Option<String>),
{
names: F,
}
impl<F> NameMatcher<F>
where
F: Fn(&Room, &Room) -> (Option<String>, Option<String>),
{
fn matches(&self, left: &Room, right: &Room) -> Ordering {
let (left_name, right_name) = (self.names)(left, right);
left_name.cmp(&right_name)
}
}
/// Create a new sorter that will sort two [`Room`] by name, i.e. by
/// comparing their display names. A lexicographically ordering is applied, i.e.
/// "a" < "b".
pub fn new_sorter() -> impl Sorter {
let matcher = NameMatcher {
names: move |left, right| (left.cached_display_name(), right.cached_display_name()),
};
move |left, right| -> Ordering { matcher.matches(left, right) }
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::async_test;
use ruma::room_id;
use super::{
super::super::filters::{client_and_server_prelude, new_rooms},
*,
};
#[async_test]
async fn test_with_two_names() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` has a “greater name” than `room_b`.
{
let matcher = NameMatcher {
names: |_left, _right| (Some("Foo".to_owned()), Some("Baz".to_owned())),
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
}
// `room_a` has a “lesser name” than `room_b`.
{
let matcher = NameMatcher {
names: |_left, _right| (Some("Bar".to_owned()), Some("Baz".to_owned())),
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
}
// `room_a` has the same name than `room_b`.
{
let matcher = NameMatcher {
names: |_left, _right| (Some("Baz".to_owned()), Some("Baz".to_owned())),
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
}
}
#[async_test]
async fn test_with_one_name() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` has a name, `room_b` has no name.
{
let matcher = NameMatcher { names: |_left, _right| (Some("Foo".to_owned()), None) };
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
}
// `room_a` has no name, `room_b` has a name.
{
let matcher = NameMatcher { names: |_left, _right| (None, Some("Bar".to_owned())) };
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
}
}
#[async_test]
async fn test_with_zero_name() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` and `room_b` has no name.
{
let matcher = NameMatcher { names: |_left, _right| (None, None) };
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
}
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::Ordering;
use super::{BoxedSorterFn, Sorter};
/// Create a new sorter that will run multiple sorters. When the nth sorter
/// returns [`Ordering::Equal`], the next sorter is called. It stops at soon as
/// a sorter return [`Ordering::Greater`] or [`Ordering::Less`].
pub fn new_sorter(sorters: Vec<BoxedSorterFn>) -> impl Sorter {
move |left, right| -> Ordering {
for sorter in &sorters {
match sorter(left, right) {
result @ Ordering::Greater | result @ Ordering::Less => return result,
Ordering::Equal => continue,
}
}
Ordering::Equal
}
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::async_test;
use ruma::room_id;
use super::{
super::super::filters::{client_and_server_prelude, new_rooms},
*,
};
#[async_test]
async fn test_with_zero_sorter() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
let or = new_sorter(vec![]);
assert_eq!(or(&room_a, &room_b), Ordering::Equal);
}
#[async_test]
async fn test_with_one_sorter() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
let sorter_1 = |_: &_, _: &_| Ordering::Less;
let or = new_sorter(vec![Box::new(sorter_1)]);
assert_eq!(or(&room_a, &room_b), Ordering::Less);
}
#[async_test]
async fn test_with_two_sorters() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
let sorter_1 = |_: &_, _: &_| Ordering::Equal;
let sorter_2 = |_: &_, _: &_| Ordering::Greater;
let or = new_sorter(vec![Box::new(sorter_1), Box::new(sorter_2)]);
assert_eq!(or(&room_a, &room_b), Ordering::Greater);
}
#[async_test]
async fn test_with_more_sorters() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
let sorter_1 = |_: &_, _: &_| Ordering::Equal;
let sorter_2 = |_: &_, _: &_| Ordering::Equal;
let sorter_3 = |_: &_, _: &_| Ordering::Less;
let sorter_4 = |_: &_, _: &_| Ordering::Greater;
let or = new_sorter(vec![
Box::new(sorter_1),
Box::new(sorter_2),
Box::new(sorter_3),
Box::new(sorter_4),
]);
assert_eq!(or(&room_a, &room_b), Ordering::Less);
}
}

View File

@@ -0,0 +1,290 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{cmp::Ordering, ops::Deref};
use matrix_sdk_base::latest_event::LatestEvent;
use super::{Room, Sorter};
struct RecencyMatcher<F>
where
F: Fn(&Room, &Room) -> (Option<LatestEvent>, Option<LatestEvent>),
{
latest_events: F,
}
impl<F> RecencyMatcher<F>
where
F: Fn(&Room, &Room) -> (Option<LatestEvent>, Option<LatestEvent>),
{
fn matches(&self, left: &Room, right: &Room) -> Ordering {
if left.id() == right.id() {
// `left` and `right` are the same room. We are comparing the same
// `LatestEvent`!
//
// The way our `Room` types are implemented makes it so they are sharing the
// same data, because they are all built from the same store. They can be seen
// as shallow clones of each others. In practice it's really great: a `Room` can
// never be outdated. However, for the case of sorting rooms, it breaks the
// search algorithm. `left` and `right` will have the exact same `LatestEvent`,
// so `left` and `right` will always be `Ordering::Equal`. This is wrong: if
// `left` is compared with `right` and if they are both the same room, it means
// that one of them (either `left`, or `right`, it's not important) has received
// an update. The room position is very likely to change. But if they compare to
// `Equal`, the position may not change. It actually depends of the search
// algorithm used by [`eyeball_im_util::SortBy`].
//
// Since this room received an update, it is more recent than the previous one
// we matched against, so return `Ordering::Greater`.
return Ordering::Greater;
}
match (self.latest_events)(left, right) {
(Some(left_latest_event), Some(right_latest_event)) => left_latest_event
.event_origin_server_ts()
.cmp(&right_latest_event.event_origin_server_ts())
.reverse(),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
}
/// Create a new sorter that will sort two [`Room`] by recency, i.e. by
/// comparing their `origin_server_ts` value. The `Room` with the newest
/// `origin_server_ts` comes first, i.e. newest < oldest.
pub fn new_sorter() -> impl Sorter {
let matcher = RecencyMatcher {
latest_events: move |left, right| {
(left.deref().latest_event(), right.deref().latest_event())
},
};
move |left, right| -> Ordering { matcher.matches(left, right) }
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB};
use ruma::room_id;
use super::{
super::super::filters::{client_and_server_prelude, new_rooms},
*,
};
#[async_test]
async fn test_with_two_latest_events() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` has an older latest event than `room_b`.
{
let matcher = RecencyMatcher {
latest_events: |_left, _right| {
(
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "foo",
"msgtype": "m.text",
},
"sender": &*ALICE,
"event_id": "$foo",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "bar",
"msgtype": "m.text",
},
"sender": &*BOB,
"event_id": "$bar",
"origin_server_ts": 2,
"type": "m.room.message",
})
.into(),
)),
)
},
};
// `room_a` is greater than `room_b`, i.e. it must come after `room_b`.
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
}
// `room_b` has an older latest event than `room_a`.
{
let matcher = RecencyMatcher {
latest_events: |_left, _right| {
(
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "foo",
"msgtype": "m.text",
},
"sender": &*ALICE,
"event_id": "$foo",
"origin_server_ts": 2,
"type": "m.room.message",
})
.into(),
)),
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "bar",
"msgtype": "m.text",
},
"sender": &*BOB,
"event_id": "$bar",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
)
},
};
// `room_a` is less than `room_b`, i.e. it must come before `room_b`.
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
}
// `room_a` has an equally old latest event than `room_b`.
{
let matcher = RecencyMatcher {
latest_events: |_left, _right| {
(
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "foo",
"msgtype": "m.text",
},
"sender": &*ALICE,
"event_id": "$foo",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "bar",
"msgtype": "m.text",
},
"sender": &*BOB,
"event_id": "$bar",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
)
},
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
}
}
#[async_test]
async fn test_with_one_latest_event() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` has a latest event, `room_b` has no latest event.
{
let matcher = RecencyMatcher {
latest_events: |_left, _right| {
(
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "foo",
"msgtype": "m.text",
},
"sender": &*ALICE,
"event_id": "$foo",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
None,
)
},
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Less);
}
// `room_a` has no latest event, `room_b` has a latest event.
{
let matcher = RecencyMatcher {
latest_events: |_left, _right| {
(
None,
Some(LatestEvent::new(
sync_timeline_event!({
"content": {
"body": "bar",
"msgtype": "m.text",
},
"sender": &*BOB,
"event_id": "$bar",
"origin_server_ts": 1,
"type": "m.room.message",
})
.into(),
)),
)
},
};
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Greater);
}
}
#[async_test]
async fn test_with_zero_latest_event() {
let (client, server, sliding_sync) = client_and_server_prelude().await;
let [room_a, room_b] =
new_rooms([room_id!("!a:b.c"), room_id!("!d:e.f")], &client, &server, &sliding_sync)
.await;
// `room_a` and `room_b` has no latest event.
{
let matcher = RecencyMatcher { latest_events: |_left, _right| (None, None) };
assert_eq!(matcher.matches(&room_a, &room_b), Ordering::Equal);
}
}
}