feat(ui): Moaaar filters: add all, any, not, unread and category filters

feat(ui): Moaaar filters: add `all`, `any`, `not`, `unread` and `category` filters
This commit is contained in:
Ivan Enderlin
2024-02-08 14:28:18 +01:00
committed by GitHub
16 changed files with 665 additions and 91 deletions

View File

@@ -13,9 +13,13 @@ use matrix_sdk::{
RoomListEntry as MatrixRoomListEntry,
};
use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
new_filter_normalized_match_room_name,
room_list_service::{
filters::{
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,
},
timeline::default_event_filter,
};
@@ -391,19 +395,8 @@ impl RoomListDynamicEntriesController {
#[uniffi::export]
impl RoomListDynamicEntriesController {
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
use RoomListEntriesDynamicFilterKind as Kind;
match kind {
Kind::All => self.inner.set_filter(new_filter_all()),
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
Kind::None => self.inner.set_filter(new_filter_none()),
Kind::NormalizedMatchRoomName { pattern } => {
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
}
Kind::FuzzyMatchRoomName { pattern } => {
self.inner.set_filter(new_filter_fuzzy_match_room_name(&self.client, &pattern))
}
}
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
self.inner.set_filter(filter)
}
fn add_one_page(&self) {
@@ -417,13 +410,60 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All,
AllNonLeft,
All { filters: Vec<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);
impl FilterWrapper {
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
use RoomListEntriesDynamicFilterKind as Kind;
match value {
Kind::All { filters } => Self(Box::new(new_filter_all(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
Kind::Any { filters } => Self(Box::new(new_filter_any(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
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)))
}
Kind::FuzzyMatchRoomName { pattern } => {
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
}
}
}
}
#[derive(uniffi::Object)]
pub struct RoomListItem {
inner: Arc<matrix_sdk_ui::room_list_service::Room>,

View File

@@ -31,7 +31,7 @@ pub mod latest_event;
pub mod media;
mod rooms;
mod read_receipts;
pub mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;

View File

@@ -15,9 +15,9 @@
//! # Client-side read receipts computation
//!
//! While Matrix servers have the ability to provide basic information about the
//! unread status of rooms, via [`matrix_sdk::ruma::UnreadNotificationCounts`],
//! it's not reliable for encrypted rooms. Indeed, the server doesn't have
//! access to the content of encrypted events, so it can only makes guesses when
//! unread status of rooms, via [`crate::sync::UnreadNotificationsCount`], it's
//! not reliable for encrypted rooms. Indeed, the server doesn't have access to
//! the content of encrypted events, so it can only makes guesses when
//! estimating unread and highlight counts.
//!
//! Instead, this module provides facilities to compute the number of unread
@@ -36,8 +36,8 @@
//! `marks_as_unread` function shows the opiniated set of rules that will filter
//! out uninterested events.
//!
//! The only public method in that module is [`compute_unread_counts`], which
//! updates the `RoomInfo` in place according to the new counts.
//! The only `pub(crate)` method in that module is `compute_unread_counts`,
//! which updates the `RoomInfo` in place according to the new counts.
//!
//! ## Implementation details: How to get the latest receipt?
//!

View File

@@ -347,6 +347,12 @@ impl Room {
self.inner.read().base_info.dm_targets.clone()
}
/// If this room is a direct message, returns the number of members that
/// we're sharing the room with.
pub fn direct_targets_length(&self) -> usize {
self.inner.read().base_info.dm_targets.len()
}
/// Is the room encrypted.
pub fn is_encrypted(&self) -> bool {
self.inner.read().is_encrypted()

View File

@@ -1,10 +1,9 @@
use matrix_sdk::RoomListEntry;
use super::{super::room_list::BoxedFilterFn, Filter};
/// Create a new filter that will accept all filled or invalidated entries.
pub fn new_filter() -> impl Fn(&RoomListEntry) -> bool {
|room_list_entry| -> bool {
matches!(room_list_entry, RoomListEntry::Filled(_) | RoomListEntry::Invalidated(_))
}
/// Create a new filter that will run multiple filters. It returns `false` if at
/// least one of the filter returns `false`.
pub fn new_filter(filters: Vec<BoxedFilterFn>) -> impl Filter {
move |room_list_entry| -> bool { filters.iter().all(|filter| filter(room_list_entry)) }
}
#[cfg(test)]
@@ -17,11 +16,58 @@ mod tests {
use super::new_filter;
#[test]
fn test_all_kind_of_room_list_entry() {
let all = new_filter();
fn test_one_filter() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
assert!(all(&RoomListEntry::Empty).not());
assert!(all(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(all(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));
{
let filter = |_: &_| true;
let all = new_filter(vec![Box::new(filter)]);
assert!(all(&room_list_entry));
}
{
let filter = |_: &_| false;
let all = new_filter(vec![Box::new(filter)]);
assert!(all(&room_list_entry).not());
}
}
#[test]
fn test_two_filters() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
{
let filter1 = |_: &_| true;
let filter2 = |_: &_| true;
let all = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(all(&room_list_entry));
}
{
let filter1 = |_: &_| true;
let filter2 = |_: &_| false;
let all = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(all(&room_list_entry).not());
}
{
let filter1 = |_: &_| false;
let filter2 = |_: &_| true;
let all = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(all(&room_list_entry).not());
}
{
let filter1 = |_: &_| false;
let filter2 = |_: &_| false;
let all = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(all(&room_list_entry).not());
}
}
}

View File

@@ -0,0 +1,81 @@
use super::{super::room_list::BoxedFilterFn, Filter};
/// Create a new filter that will run multiple filters. It returns `true` if at
/// least one of the filter returns `true`.
pub fn new_filter(filters: Vec<BoxedFilterFn>) -> impl Filter {
move |room_list_entry| -> bool { filters.iter().any(|filter| filter(room_list_entry)) }
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use matrix_sdk::RoomListEntry;
use ruma::room_id;
use super::new_filter;
#[test]
fn test_one_filter_is_true() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter = |_: &_| true;
let any = new_filter(vec![Box::new(filter)]);
assert!(any(&room_list_entry));
}
#[test]
fn test_one_filter_is_false() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter = |_: &_| false;
let any = new_filter(vec![Box::new(filter)]);
assert!(any(&room_list_entry).not());
}
#[test]
fn test_two_filters_with_true_true() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter1 = |_: &_| true;
let filter2 = |_: &_| true;
let any = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(any(&room_list_entry));
}
#[test]
fn test_two_filters_with_true_false() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter1 = |_: &_| true;
let filter2 = |_: &_| false;
let any = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(any(&room_list_entry));
}
#[test]
fn test_two_filters_with_false_true() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter1 = |_: &_| false;
let filter2 = |_: &_| true;
let any = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(any(&room_list_entry));
}
#[test]
fn test_two_filters_with_false_false() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter1 = |_: &_| false;
let filter2 = |_: &_| false;
let any = new_filter(vec![Box::new(filter1), Box::new(filter2)]);
assert!(any(&room_list_entry).not());
}
}

View File

@@ -0,0 +1,186 @@
use matrix_sdk::{Client, RoomListEntry};
use super::Filter;
/// An enum to represent whether a room is about “people” (strictly 2 users) or
/// “group” (1 or more than 2 users).
///
/// Ideally, we would only want to rely on the
/// [`matrix_sdk::BaseRoom::is_direct`] method, but the rules are a little bit
/// different for this high-level UI API.
///
/// 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, and
/// if the associated rooms fit in the `expected_category`. The category is
/// defined by [`RoomCategory`], see this type to learn more.
pub fn new_filter(client: &Client, expected_category: 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_category) }
}
#[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,7 +1,7 @@
pub use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher as _};
use matrix_sdk::{Client, RoomListEntry};
use matrix_sdk::Client;
use super::normalize_string;
use super::{normalize_string, Filter};
struct FuzzyMatcher {
matcher: SkimMatcherV2,
@@ -19,7 +19,7 @@ impl FuzzyMatcher {
self
}
fn fuzzy_match(&self, subject: &str) -> bool {
fn matches(&self, subject: &str) -> bool {
// No pattern means there is a match.
let Some(pattern) = self.pattern.as_ref() else { return true };
@@ -31,7 +31,7 @@ impl FuzzyMatcher {
///
/// Rooms are fetched from the `Client`. The pattern and the room names are
/// normalized with `normalize_string`.
pub fn new_filter(client: &Client, pattern: &str) -> impl Fn(&RoomListEntry) -> bool {
pub fn new_filter(client: &Client, pattern: &str) -> impl Filter {
let searcher = FuzzyMatcher::new().with_pattern(pattern);
let client = client.clone();
@@ -41,7 +41,7 @@ pub fn new_filter(client: &Client, pattern: &str) -> impl Fn(&RoomListEntry) ->
let Some(room) = client.get_room(room_id) else { return false };
let Some(room_name) = room.name() else { return false };
searcher.fuzzy_match(&room_name)
searcher.matches(&room_name)
}
}
@@ -55,14 +55,14 @@ mod tests {
fn test_no_pattern() {
let matcher = FuzzyMatcher::new();
assert!(matcher.fuzzy_match("hello"));
assert!(matcher.matches("hello"));
}
#[test]
fn test_empty_pattern() {
let matcher = FuzzyMatcher::new();
assert!(matcher.fuzzy_match("hello"));
assert!(matcher.matches("hello"));
}
#[test]
@@ -70,10 +70,10 @@ mod tests {
let matcher = FuzzyMatcher::new();
let matcher = matcher.with_pattern("mtx");
assert!(matcher.fuzzy_match("matrix"));
assert!(matcher.matches("matrix"));
let matcher = matcher.with_pattern("mxt");
assert!(matcher.fuzzy_match("matrix").not());
assert!(matcher.matches("matrix").not());
}
#[test]
@@ -81,10 +81,10 @@ mod tests {
let matcher = FuzzyMatcher::new();
let matcher = matcher.with_pattern("mtx");
assert!(matcher.fuzzy_match("MaTrIX"));
assert!(matcher.matches("MaTrIX"));
let matcher = matcher.with_pattern("mxt");
assert!(matcher.fuzzy_match("MaTrIX").not());
assert!(matcher.matches("MaTrIX").not());
}
#[test]
@@ -92,12 +92,12 @@ mod tests {
let matcher = FuzzyMatcher::new();
let matcher = matcher.with_pattern("mtx");
assert!(matcher.fuzzy_match("matrix"));
assert!(matcher.fuzzy_match("Matrix"));
assert!(matcher.matches("matrix"));
assert!(matcher.matches("Matrix"));
let matcher = matcher.with_pattern("Mtx");
assert!(matcher.fuzzy_match("matrix").not());
assert!(matcher.fuzzy_match("Matrix"));
assert!(matcher.matches("matrix").not());
assert!(matcher.matches("Matrix"));
}
#[test]
@@ -110,10 +110,10 @@ mod tests {
assert_eq!(matcher.pattern, Some("ubete".to_owned()));
// Second, assert that the subject is normalized too.
assert!(matcher.fuzzy_match("un bel été"));
assert!(matcher.matches("un bel été"));
// Another concrete test.
let matcher = matcher.with_pattern("stf");
assert!(matcher.fuzzy_match("Ștefan"));
assert!(matcher.matches("Ștefan"));
}
}

View File

@@ -1,15 +1,32 @@
mod all;
mod all_non_left;
mod any;
mod category;
mod fuzzy_match_room_name;
mod non_left;
mod none;
mod normalized_match_room_name;
mod not;
mod unread;
pub use all::new_filter as new_filter_all;
pub use all_non_left::new_filter as new_filter_all_non_left;
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;
pub use none::new_filter as new_filter_none;
pub use normalized_match_room_name::new_filter as new_filter_normalized_match_room_name;
pub use not::new_filter as new_filter_not;
use unicode_normalization::{char::is_combining_mark, UnicodeNormalization};
pub use unread::new_filter as new_filter_unread;
/// A trait “alias” that represents a _filter_.
///
/// A filter is simply a function that receives a `&RoomListEntry` and returns a
/// `bool`.
pub trait Filter: Fn(&RoomListEntry) -> bool {}
impl<F> Filter for F where F: Fn(&RoomListEntry) -> bool {}
/// Normalize a string, i.e. decompose it into NFD (Normalization Form D, i.e. a
/// canonical decomposition, see http://www.unicode.org/reports/tr15/) and

View File

@@ -1,17 +1,25 @@
use matrix_sdk::{Client, RoomListEntry};
use matrix_sdk_base::RoomState;
struct NonLeftRoomMatcher<F: Fn(&RoomListEntry) -> Option<RoomState>> {
get_state: F,
use super::Filter;
struct NonLeftRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<RoomState>,
{
state: F,
}
impl<F: Fn(&RoomListEntry) -> Option<RoomState>> NonLeftRoomMatcher<F> {
impl<F> NonLeftRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<RoomState>,
{
fn matches(&self, room: &RoomListEntry) -> bool {
if !matches!(room, RoomListEntry::Filled(_) | RoomListEntry::Invalidated(_)) {
return false;
}
if let Some(state) = (self.get_state)(room) {
if let Some(state) = (self.state)(room) {
state != RoomState::Left
} else {
false
@@ -21,11 +29,11 @@ impl<F: Fn(&RoomListEntry) -> Option<RoomState>> NonLeftRoomMatcher<F> {
/// Create a new filter that will accept all filled or invalidated entries, but
/// filters out left rooms.
pub fn new_filter(client: &Client) -> impl Fn(&RoomListEntry) -> bool {
pub fn new_filter(client: &Client) -> impl Filter {
let client = client.clone();
let matcher = NonLeftRoomMatcher {
get_state: move |room| {
state: move |room| {
let room_id = room.as_room_id()?;
let room = client.get_room(room_id)?;
Some(room.state())
@@ -41,24 +49,24 @@ mod tests {
use matrix_sdk_base::RoomState;
use ruma::room_id;
use crate::room_list_service::filters::all_non_left::NonLeftRoomMatcher;
use super::NonLeftRoomMatcher;
#[test]
fn test_all_non_left_kind_of_room_list_entry() {
// When we can't figure out the room state, nothing matches.
let matcher = NonLeftRoomMatcher { get_state: |_| None };
let matcher = NonLeftRoomMatcher { state: |_| None };
assert!(!matcher.matches(&RoomListEntry::Empty));
assert!(!matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(!matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));
// When a room has been left, it doesn't match.
let matcher = NonLeftRoomMatcher { get_state: |_| Some(RoomState::Left) };
let matcher = NonLeftRoomMatcher { state: |_| Some(RoomState::Left) };
assert!(!matcher.matches(&RoomListEntry::Empty));
assert!(!matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(!matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));
// When a room has been joined, it does match (unless it's empty).
let matcher = NonLeftRoomMatcher { get_state: |_| Some(RoomState::Joined) };
let matcher = NonLeftRoomMatcher { state: |_| Some(RoomState::Joined) };
assert!(!matcher.matches(&RoomListEntry::Empty));
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));

View File

@@ -1,7 +1,7 @@
use matrix_sdk::RoomListEntry;
use super::Filter;
/// Create a new filter that will reject all entries.
pub fn new_filter() -> impl Fn(&RoomListEntry) -> bool {
pub fn new_filter() -> impl Filter {
|_room_list_entry| -> bool { false }
}

View File

@@ -1,6 +1,6 @@
use matrix_sdk::{Client, RoomListEntry};
use matrix_sdk::Client;
use super::normalize_string;
use super::{normalize_string, Filter};
struct NormalizedMatcher {
pattern: Option<String>,
@@ -17,7 +17,7 @@ impl NormalizedMatcher {
self
}
fn normalized_match(&self, subject: &str) -> bool {
fn matches(&self, subject: &str) -> bool {
// No pattern means there is a match.
let Some(pattern) = self.pattern.as_ref() else { return true };
@@ -31,7 +31,7 @@ impl NormalizedMatcher {
///
/// Rooms are fetched from the `Client`. The pattern and the room names are
/// normalized with `normalize_string`.
pub fn new_filter(client: &Client, pattern: &str) -> impl Fn(&RoomListEntry) -> bool {
pub fn new_filter(client: &Client, pattern: &str) -> impl Filter {
let searcher = NormalizedMatcher::new().with_pattern(pattern);
let client = client.clone();
@@ -41,7 +41,7 @@ pub fn new_filter(client: &Client, pattern: &str) -> impl Fn(&RoomListEntry) ->
let Some(room) = client.get_room(room_id) else { return false };
let Some(room_name) = room.name() else { return false };
searcher.normalized_match(&room_name)
searcher.matches(&room_name)
}
}
@@ -55,14 +55,14 @@ mod tests {
fn test_no_pattern() {
let matcher = NormalizedMatcher::new();
assert!(matcher.normalized_match("hello"));
assert!(matcher.matches("hello"));
}
#[test]
fn test_empty_pattern() {
let matcher = NormalizedMatcher::new();
assert!(matcher.normalized_match("hello"));
assert!(matcher.matches("hello"));
}
#[test]
@@ -70,10 +70,10 @@ mod tests {
let matcher = NormalizedMatcher::new();
let matcher = matcher.with_pattern("matrix");
assert!(matcher.normalized_match("matrix"));
assert!(matcher.matches("matrix"));
let matcher = matcher.with_pattern("matrxi");
assert!(matcher.normalized_match("matrix").not());
assert!(matcher.matches("matrix").not());
}
#[test]
@@ -81,10 +81,10 @@ mod tests {
let matcher = NormalizedMatcher::new();
let matcher = matcher.with_pattern("matrix");
assert!(matcher.normalized_match("MaTrIX"));
assert!(matcher.matches("MaTrIX"));
let matcher = matcher.with_pattern("matrxi");
assert!(matcher.normalized_match("MaTrIX").not());
assert!(matcher.matches("MaTrIX").not());
}
#[test]
@@ -97,10 +97,10 @@ mod tests {
assert_eq!(matcher.pattern, Some("un ete".to_owned()));
// Second, assert that the subject is normalized too.
assert!(matcher.normalized_match("un été magnifique"));
assert!(matcher.matches("un été magnifique"));
// Another concrete test.
let matcher = matcher.with_pattern("stefan");
assert!(matcher.normalized_match("Ștefan"));
assert!(matcher.matches("Ștefan"));
}
}

View File

@@ -0,0 +1,39 @@
use std::ops::Not;
use super::{super::room_list::BoxedFilterFn, Filter};
/// Create a new filter that will negate the inner filter. It returns `false` if
/// the inner filter returns `true`, otherwise it returns `true`.
pub fn new_filter(filter: BoxedFilterFn) -> impl Filter {
move |room_list_entry| -> bool { filter(room_list_entry).not() }
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use matrix_sdk::RoomListEntry;
use ruma::room_id;
use super::new_filter;
#[test]
fn test_true() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter = Box::new(|_: &_| true);
let not = new_filter(filter);
assert!(not(&room_list_entry).not());
}
#[test]
fn test_false() {
let room_list_entry = RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned());
let filter = Box::new(|_: &_| false);
let not = new_filter(filter);
assert!(not(&room_list_entry));
}
}

View File

@@ -0,0 +1,153 @@
use matrix_sdk::{Client, RoomListEntry};
use matrix_sdk_base::read_receipts::RoomReadReceipts;
use super::Filter;
type IsMarkedUnread = bool;
struct UnreadRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<(RoomReadReceipts, IsMarkedUnread)>,
{
read_receipts_and_unread: F,
}
impl<F> UnreadRoomMatcher<F>
where
F: Fn(&RoomListEntry) -> Option<(RoomReadReceipts, IsMarkedUnread)>,
{
fn matches(&self, room_list_entry: &RoomListEntry) -> bool {
if !matches!(room_list_entry, RoomListEntry::Filled(_) | RoomListEntry::Invalidated(_)) {
return false;
}
let Some((read_receipts, is_marked_unread)) =
(self.read_receipts_and_unread)(room_list_entry)
else {
return false;
};
read_receipts.num_notifications > 0 || is_marked_unread
}
}
/// Create a new filter that will accept all filled or invalidated entries, but
/// filters out rooms that have no unread notifications (different from unread
/// messages), or is not marked as unread.
pub fn new_filter(client: &Client) -> impl Filter {
let client = client.clone();
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: move |room| {
let room_id = room.as_room_id()?;
let room = client.get_room(room_id)?;
Some((room.read_receipts(), room.is_marked_unread()))
},
};
move |room_list_entry| -> bool { matcher.matches(room_list_entry) }
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use matrix_sdk::RoomListEntry;
use matrix_sdk_base::read_receipts::RoomReadReceipts;
use ruma::room_id;
use super::UnreadRoomMatcher;
#[test]
fn test_has_unread_notifications() {
for is_marked_as_unread in [true, false] {
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: |_| {
let mut read_receipts = RoomReadReceipts::default();
read_receipts.num_unread = 42;
read_receipts.num_notifications = 42;
Some((read_receipts, is_marked_as_unread))
},
};
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(
matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()))
);
}
}
#[test]
fn test_has_unread_messages_but_no_unread_notifications_and_is_not_marked_as_unread() {
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: |_| {
let mut read_receipts = RoomReadReceipts::default();
read_receipts.num_unread = 42;
read_receipts.num_notifications = 0;
Some((read_receipts, false))
},
};
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())).not());
assert!(matcher
.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()))
.not());
}
#[test]
fn test_has_unread_messages_but_no_unread_notifications_and_is_marked_as_unread() {
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: |_| {
let mut read_receipts = RoomReadReceipts::default();
read_receipts.num_unread = 42;
read_receipts.num_notifications = 0;
Some((read_receipts, true))
},
};
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));
}
#[test]
fn test_has_no_unread_notifications_and_is_not_marked_as_unread() {
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: |_| Some((RoomReadReceipts::default(), false)),
};
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())).not());
assert!(matcher
.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()))
.not());
}
#[test]
fn test_has_no_unread_notifications_and_is_marked_as_unread() {
let matcher = UnreadRoomMatcher {
read_receipts_and_unread: |_| Some((RoomReadReceipts::default(), true)),
};
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())));
assert!(matcher.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned())));
}
#[test]
fn test_read_receipts_cannot_be_found() {
let matcher = UnreadRoomMatcher { read_receipts_and_unread: |_| None };
assert!(matcher.matches(&RoomListEntry::Empty).not());
assert!(matcher.matches(&RoomListEntry::Filled(room_id!("!r0:bar.org").to_owned())).not());
assert!(matcher
.matches(&RoomListEntry::Invalidated(room_id!("!r0:bar.org").to_owned()))
.not());
}
}

View File

@@ -26,7 +26,7 @@ use matrix_sdk::{
RoomListEntry, SlidingSync, SlidingSyncList,
};
use super::{Error, State};
use super::{filters::Filter, Error, State};
/// A `RoomList` represents a list of rooms, from a
/// [`RoomListService`](super::RoomListService).
@@ -199,7 +199,8 @@ pub enum RoomListLoadingState {
},
}
type BoxedFilterFn = Box<dyn Fn(&RoomListEntry) -> bool + Send + Sync>;
/// Type alias for a boxed filter function.
pub type BoxedFilterFn = Box<dyn Filter + Send + Sync>;
/// Controller for the [`RoomList`] dynamic entries.
///
@@ -226,17 +227,14 @@ impl RoomListDynamicEntriesController {
///
/// If the associated stream has been dropped, returns `false` to indicate
/// the operation didn't have an effect.
pub fn set_filter(
&self,
filter: impl Fn(&RoomListEntry) -> bool + Send + Sync + 'static,
) -> bool {
pub fn set_filter(&self, filter: BoxedFilterFn) -> bool {
if Arc::strong_count(&self.filter) == 1 {
// there is no other reference to the boxed filter fn, setting it
// would be pointless (no new references can be created from self,
// either)
false
} else {
self.filter.set(Box::new(filter));
self.filter.set(filter);
true
}
}

View File

@@ -12,7 +12,7 @@ use matrix_sdk_base::sync::UnreadNotificationsCount;
use matrix_sdk_test::async_test;
use matrix_sdk_ui::{
room_list_service::{
filters::{new_filter_all, new_filter_fuzzy_match_room_name, new_filter_none},
filters::{new_filter_fuzzy_match_room_name, new_filter_non_left, new_filter_none},
Error, Input, InputResult, RoomListEntry, RoomListLoadingState, State, SyncIndicator,
ALL_ROOMS_LIST_NAME as ALL_ROOMS, INVITES_LIST_NAME as INVITES,
VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS,
@@ -1643,7 +1643,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> {
assert_pending!(dynamic_entries_stream);
// Now, let's define a filter.
dynamic_entries.set_filter(new_filter_fuzzy_match_room_name(&client, "mat ba"));
dynamic_entries.set_filter(Box::new(new_filter_fuzzy_match_room_name(&client, "mat ba")));
// Assert the dynamic entries.
assert_entries_batch! {
@@ -1799,7 +1799,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> {
assert_pending!(dynamic_entries_stream);
// Now, let's change the dynamic entries!
dynamic_entries.set_filter(new_filter_fuzzy_match_room_name(&client, "hell"));
dynamic_entries.set_filter(Box::new(new_filter_fuzzy_match_room_name(&client, "hell")));
// Assert the dynamic entries.
assert_entries_batch! {
@@ -1811,7 +1811,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> {
assert_pending!(dynamic_entries_stream);
// Now, let's change again the dynamic filter!
dynamic_entries.set_filter(new_filter_none());
dynamic_entries.set_filter(Box::new(new_filter_none()));
// Assert the dynamic entries.
assert_entries_batch! {
@@ -1822,7 +1822,7 @@ async fn test_dynamic_entries_stream() -> Result<(), Error> {
};
// Now, let's change again the dynamic filter!
dynamic_entries.set_filter(new_filter_all());
dynamic_entries.set_filter(Box::new(new_filter_non_left(&client)));
// Assert the dynamic entries.
assert_entries_batch! {