Skip to content

Commit

Permalink
feat(ui,ffi): Implement the category room list filter.
Browse files Browse the repository at this point in the history
This patch implements the `category` room list filter. It introduces a
new type: `RoomCategory`, to ensure that “group” and “people” are
mutually exclusives.
  • Loading branch information
Hywan committed Feb 7, 2024
1 parent f950e67 commit 5baf078
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 2 deletions.
22 changes: 20 additions & 2 deletions bindings/matrix-sdk-ffi/src/room_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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);
Expand All @@ -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)))
Expand Down
181 changes: 181 additions & 0 deletions crates/matrix-sdk-ui/src/room_list_service/filters/category.rs
Original file line number Diff line number Diff line change
@@ -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());
}
}
2 changes: 2 additions & 0 deletions crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod all;
mod any;
mod category;
mod fuzzy_match_room_name;
mod non_left;
mod none;
Expand All @@ -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;
Expand Down

0 comments on commit 5baf078

Please sign in to comment.