feat(ui,ffi): Implement the category room list filter.

This patch implements the `category` room list filter. It introduces a
new type: `RoomCategory`, to ensure that “group” and “people” are
mutually exclusives.
This commit is contained in:
Ivan Enderlin
2024-02-05 16:06:41 +01:00
parent f950e67b39
commit 5baf078c4b
3 changed files with 203 additions and 2 deletions

View File

@@ -15,8 +15,9 @@ use matrix_sdk::{
use matrix_sdk_ui::{
room_list_service::{
filters::{
new_filter_all, new_filter_any, new_filter_fuzzy_match_room_name, new_filter_non_left,
new_filter_none, new_filter_normalized_match_room_name, new_filter_unread,
new_filter_all, new_filter_any, new_filter_category, new_filter_fuzzy_match_room_name,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_unread, RoomCategory,
},
BoxedFilterFn,
},
@@ -413,11 +414,27 @@ pub enum RoomListEntriesDynamicFilterKind {
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonLeft,
Unread,
Category { expect: RoomListFilterCategory },
None,
NormalizedMatchRoomName { pattern: String },
FuzzyMatchRoomName { pattern: String },
}
#[derive(uniffi::Enum)]
pub enum RoomListFilterCategory {
Group,
People,
}
impl From<RoomListFilterCategory> for RoomCategory {
fn from(value: RoomListFilterCategory) -> Self {
match value {
RoomListFilterCategory::Group => Self::Group,
RoomListFilterCategory::People => Self::People,
}
}
}
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
/// a `BoxedFilterFn`.
struct FilterWrapper(BoxedFilterFn);
@@ -435,6 +452,7 @@ impl FilterWrapper {
))),
Kind::NonLeft => Self(Box::new(new_filter_non_left(client))),
Kind::Unread => Self(Box::new(new_filter_unread(client))),
Kind::Category { expect } => Self(Box::new(new_filter_category(client, expect.into()))),
Kind::None => Self(Box::new(new_filter_none())),
Kind::NormalizedMatchRoomName { pattern } => {
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))

View File

@@ -0,0 +1,181 @@
use matrix_sdk::{Client, RoomListEntry};
use super::Filter;
/// An enum to represent whether a room is about “people” (1 or 2 users) or
/// “group” (more than 2 users).
///
/// This is implemented this way so that it's impossible to filter by “group”
/// and by “people” at the same time: these criteria are mutually
/// exclusive by design per filter.
#[derive(Copy, Clone, PartialEq)]
pub enum RoomCategory {
Group,
People,
}
type DirectTargetsLength = usize;
struct CategoryRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<DirectTargetsLength>,
{
/// _Direct targets_ mean the number of users in a direct room, except us.
/// So if it returns 1, it means there are 2 users in the direct room.
number_of_direct_targets: F,
}
impl<F> CategoryRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<DirectTargetsLength>,
{
fn matches(&self, room_list_entry: &RoomListEntry, expected_kind: RoomCategory) -> bool {
if !matches!(room_list_entry, RoomListEntry::Filled(_) | RoomListEntry::Invalidated(_)) {
return false;
}
let kind = match (self.number_of_direct_targets)(room_list_entry) {
// If 1, we are sure it's a direct room between two users. It's the strict
// definition of the `People` category, all good.
Some(1) => RoomCategory::People,
// If smaller than 1, we are not sure it's a direct room, it's then a `Group`.
// If greater than 1, we are sure it's a direct room but not between
// two users, so it's a `Group` based on our expectation.
Some(_) => RoomCategory::Group,
// Don't know.
None => return false,
};
kind == expected_kind
}
}
/// Create a new filter that will accept all filled or invalidated entries, but
/// filters out rooms that have no unread messages.
pub fn new_filter(client: &Client, expected_kind: RoomCategory) -> impl Filter {
let client = client.clone();
let matcher = CategoryRoomMatcher {
number_of_direct_targets: move |room| {
let room_id = room.as_room_id()?;
let room = client.get_room(room_id)?;
Some(room.direct_targets_length())
},
};
move |room_list_entry| -> bool { matcher.matches(room_list_entry, expected_kind) }
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use matrix_sdk::RoomListEntry;
use ruma::room_id;
use super::{CategoryRoomMatcher, RoomCategory};
#[test]
fn test_kind_is_group() {
let matcher = CategoryRoomMatcher { number_of_direct_targets: |_| Some(42) };
// Expect `People`.
{
let expected_kind = RoomCategory::People;
assert!(matcher.matches(&RoomListEntry::Empty, expected_kind).not());
assert!(
matcher
.matches(
&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned(),),
expected_kind,
)
.not()
);
assert!(matcher
.matches(
&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()),
expected_kind
)
.not());
}
// Expect `Group`.
{
let expected_kind = RoomCategory::Group;
assert!(matcher.matches(&RoomListEntry::Empty, expected_kind).not());
assert!(matcher.matches(
&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned(),),
expected_kind,
));
assert!(matcher.matches(
&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()),
expected_kind,
));
}
}
#[test]
fn test_kind_is_people() {
let matcher = CategoryRoomMatcher { number_of_direct_targets: |_| Some(1) };
// Expect `People`.
{
let expected_kind = RoomCategory::People;
assert!(matcher.matches(&RoomListEntry::Empty, expected_kind).not());
assert!(matcher.matches(
&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned()),
expected_kind,
));
assert!(matcher.matches(
&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()),
expected_kind
));
}
// Expect `Group`.
{
let expected_kind = RoomCategory::Group;
assert!(matcher.matches(&RoomListEntry::Empty, expected_kind).not());
assert!(
matcher
.matches(
&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned(),),
expected_kind,
)
.not()
);
assert!(matcher
.matches(
&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()),
expected_kind,
)
.not());
}
}
#[test]
fn test_room_kind_cannot_be_found() {
let matcher = CategoryRoomMatcher { number_of_direct_targets: |_| None };
assert!(matcher.matches(&RoomListEntry::Empty, RoomCategory::Group).not());
assert!(matcher
.matches(
&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned()),
RoomCategory::Group
)
.not());
assert!(matcher
.matches(
&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()),
RoomCategory::Group
)
.not());
}
}

View File

@@ -1,5 +1,6 @@
mod all;
mod any;
mod category;
mod fuzzy_match_room_name;
mod non_left;
mod none;
@@ -9,6 +10,7 @@ mod unread;
pub use all::new_filter as new_filter_all;
pub use any::new_filter as new_filter_any;
pub use category::{new_filter as new_filter_category, RoomCategory};
pub use fuzzy_match_room_name::new_filter as new_filter_fuzzy_match_room_name;
use matrix_sdk::RoomListEntry;
pub use non_left::new_filter as new_filter_non_left;