diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index b647cdcc03c..1911a924ae2 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -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 }, NonLeft, Unread, + Category { expect: RoomListFilterCategory }, None, NormalizedMatchRoomName { pattern: String }, FuzzyMatchRoomName { pattern: String }, } +#[derive(uniffi::Enum)] +pub enum RoomListFilterCategory { + Group, + People, +} + +impl From 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))) diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/category.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/category.rs new file mode 100644 index 00000000000..6fc07eb8a85 --- /dev/null +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/category.rs @@ -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 +where + F: Fn(&RoomListEntry) -> Option, +{ + /// _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 CategoryRoomMatcher +where + F: Fn(&RoomListEntry) -> Option, +{ + 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()); + } +} diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs index 9686d1a685f..794a2ddff41 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs @@ -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;