mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-08 07:56:55 -04:00
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:
@@ -54,6 +54,7 @@
|
||||
pub mod filters;
|
||||
mod room;
|
||||
mod room_list;
|
||||
pub mod sorters;
|
||||
mod state;
|
||||
|
||||
use std::{
|
||||
|
||||
38
crates/matrix-sdk-ui/src/room_list_service/sorters/mod.rs
Normal file
38
crates/matrix-sdk-ui/src/room_list_service/sorters/mod.rs
Normal 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>;
|
||||
129
crates/matrix-sdk-ui/src/room_list_service/sorters/name.rs
Normal file
129
crates/matrix-sdk-ui/src/room_list_service/sorters/name.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
crates/matrix-sdk-ui/src/room_list_service/sorters/or.rs
Normal file
104
crates/matrix-sdk-ui/src/room_list_service/sorters/or.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
290
crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs
Normal file
290
crates/matrix-sdk-ui/src/room_list_service/sorters/recency.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user