diff --git a/Cargo.lock b/Cargo.lock index 7eaf1bef7..17a58e470 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1748,6 +1748,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2911,6 +2920,7 @@ dependencies = [ "eyeball-im-util", "futures-core", "futures-util", + "fuzzy-matcher", "imbl", "indexmap 2.0.0", "itertools 0.11.0", diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index d99af4b96..72ce7edee 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -25,6 +25,7 @@ eyeball-im = { workspace = true } eyeball-im-util = { workspace = true } futures-core = { workspace = true } futures-util = { workspace = true } +fuzzy-matcher = "0.3.7" imbl = { version = "2.0.0", features = ["serde"] } indexmap = "2.0.0" itertools = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs new file mode 100644 index 000000000..b889c93b4 --- /dev/null +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs @@ -0,0 +1,78 @@ +pub use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher as _}; +use matrix_sdk::{Client, RoomListEntry}; + +struct FuzzyMatcher { + matcher: SkimMatcherV2, +} + +impl FuzzyMatcher { + fn new() -> Self { + Self { matcher: SkimMatcherV2::default().smart_case().use_cache(true) } + } + + fn fuzzy_match(&self, subject: &str, pattern: &str) -> bool { + self.matcher.fuzzy_match(subject, pattern).is_some() + } +} + +pub fn new_filter( + client: &Client, + pattern: String, +) -> impl Fn(&RoomListEntry) -> bool + Send + Sync + 'static { + let searcher = FuzzyMatcher::new(); + let client = client.clone(); + + move |room_list_entry| -> bool { + let Some(room_id) = room_list_entry.as_room_id() else { return false }; + 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, &pattern) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Not; + + use super::*; + + #[test] + fn test_literal() { + let matcher = FuzzyMatcher::new(); + + assert!(matcher.fuzzy_match("matrix", "mtx")); + assert!(matcher.fuzzy_match("matrix", "mxt").not()); + } + + #[test] + fn test_ignore_case() { + let matcher = FuzzyMatcher::new(); + + assert!(matcher.fuzzy_match("MaTrIX", "mtx")); + assert!(matcher.fuzzy_match("MaTrIX", "mxt").not()); + } + + #[test] + fn test_smart_case() { + let matcher = FuzzyMatcher::new(); + + assert!(matcher.fuzzy_match("Matrix", "mtx")); + assert!(matcher.fuzzy_match("Matrix", "mtx")); + assert!(matcher.fuzzy_match("MatriX", "Mtx").not()); + } + + // This is not supported yet. + /* + #[test] + fn test_transliteration_and_normalization() { + let matcher = FuzzyMatcher::new(); + + assert!(matcher.fuzzy_match("un bel été", "été")); + assert!(matcher.fuzzy_match("un bel été", "ete")); + assert!(matcher.fuzzy_match("un bel été", "éte")); + assert!(matcher.fuzzy_match("un bel été", "étè").not()); + assert!(matcher.fuzzy_match("Ștefan", "stef")); + } + */ +} 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 new file mode 100644 index 000000000..5b9b55589 --- /dev/null +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs @@ -0,0 +1,3 @@ +mod fuzzy_match_room_name; + +pub use fuzzy_match_room_name::new_filter as new_filter_fuzzy_match_room_name; diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 26cb980f5..58b145cfa 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -62,6 +62,7 @@ //! [`RoomListService::state`] provides a way to get a stream of the state //! machine's state, which can be pretty helpful for the client app. +pub mod filters; mod room; mod room_list; mod state; diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 737ffde99..1aaa73de3 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -4,12 +4,13 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; use futures_util::{pin_mut, FutureExt, StreamExt}; use imbl::vector; +use matrix_sdk::Client; use matrix_sdk_test::async_test; use matrix_sdk_ui::{ room_list_service::{ - Error, Input, InputResult, RoomListEntry, RoomListLoadingState, State, - ALL_ROOMS_LIST_NAME as ALL_ROOMS, INVITES_LIST_NAME as INVITES, - VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS, + filters::new_filter_fuzzy_match_room_name, Error, Input, InputResult, RoomListEntry, + RoomListLoadingState, State, ALL_ROOMS_LIST_NAME as ALL_ROOMS, + INVITES_LIST_NAME as INVITES, VISIBLE_ROOMS_LIST_NAME as VISIBLE_ROOMS, }, timeline::{TimelineItemKind, VirtualTimelineItem}, RoomListService, @@ -30,11 +31,11 @@ use crate::{ timeline::sliding_sync::{assert_timeline_stream, timeline_event}, }; -async fn new_room_list_service() -> Result<(MockServer, RoomListService), Error> { +async fn new_room_list_service() -> Result<(Client, MockServer, RoomListService), Error> { let (client, server) = logged_in_client().await; - let room_list = RoomListService::new(client).await?; + let room_list = RoomListService::new(client.clone()).await?; - Ok((server, room_list)) + Ok((client, server, room_list)) } // Same macro as in the main, with additional checking that the state @@ -215,7 +216,7 @@ macro_rules! assert_entries_stream { #[async_test] async fn test_sync_all_states() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -471,7 +472,7 @@ async fn test_sync_all_states() -> Result<(), Error> { #[async_test] async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; // Start a sync, and drop it at the end of the block. { @@ -590,7 +591,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> { #[async_test] async fn test_sync_resumes_from_error() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -891,7 +892,7 @@ async fn test_sync_resumes_from_error() -> Result<(), Error> { #[async_test] async fn test_sync_resumes_from_terminated() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; // Let's stop the sync before actually syncing (we never know!). // We get an error, obviously. @@ -1129,7 +1130,7 @@ async fn test_sync_resumes_from_terminated() -> Result<(), Error> { #[async_test] async fn test_loading_states() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1237,7 +1238,7 @@ async fn test_loading_states() -> Result<(), Error> { #[async_test] async fn test_entries_stream() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1372,7 +1373,7 @@ async fn test_entries_stream() -> Result<(), Error> { #[async_test] async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (client, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1410,7 +1411,7 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { }, "rooms": { "!r0:bar.org": { - "name": "Room #0", + "name": "Matrix Foobar", "initial": true, "timeline": [], }, @@ -1426,12 +1427,8 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { pending; }; - let (previous_entries, entries_stream) = all_rooms.entries_filtered(|room_list_entry| { - matches!( - room_list_entry.as_room_id(), - Some(room_id) if room_id.server_name() == "bar.org" - ) - }); + let (previous_entries, entries_stream) = + all_rooms.entries_filtered(new_filter_fuzzy_match_room_name(&client, "mat ba".to_string())); pin_mut!(entries_stream); sync_then_assert_request_and_fake_response! { @@ -1461,8 +1458,8 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { "range": [1, 4], "room_ids": [ "!r1:bar.org", - "!r2:qux.org", - "!r3:qux.org", + "!r2:bar.org", + "!r3:bar.org", "!r4:bar.org", ], }, @@ -1477,22 +1474,22 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { }, "rooms": { "!r1:bar.org": { - "name": "Room #1", + "name": "Matrix Bar", "initial": true, "timeline": [], }, - "!r2:qux.org": { - "name": "Room #2", + "!r2:bar.org": { + "name": "Hello", "initial": true, "timeline": [], }, - "!r3:qux.org": { - "name": "Room #3", + "!r3:bar.org": { + "name": "Matrix Qux", "initial": true, "timeline": [], }, "!r4:bar.org": { - "name": "Room #4", + "name": "Matrix Baz", "initial": true, "timeline": [], }, @@ -1513,7 +1510,7 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { #[async_test] async fn test_invites_stream() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1666,7 +1663,7 @@ async fn test_invites_stream() -> Result<(), Error> { #[async_test] async fn test_room() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1749,7 +1746,7 @@ async fn test_room() -> Result<(), Error> { #[async_test] async fn test_room_not_found() -> Result<(), Error> { - let (_server, room_list) = new_room_list_service().await?; + let (_, _, room_list) = new_room_list_service().await?; let room_id = room_id!("!foo:bar.org"); @@ -1765,7 +1762,7 @@ async fn test_room_not_found() -> Result<(), Error> { #[async_test] async fn test_room_subscription() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1886,7 +1883,7 @@ async fn test_room_subscription() -> Result<(), Error> { #[async_test] async fn test_room_unread_notifications() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -1973,7 +1970,7 @@ async fn test_room_unread_notifications() -> Result<(), Error> { #[async_test] async fn test_room_timeline() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -2057,7 +2054,7 @@ async fn test_room_timeline() -> Result<(), Error> { #[async_test] async fn test_room_latest_event() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); @@ -2148,7 +2145,7 @@ async fn test_room_latest_event() -> Result<(), Error> { #[async_test] async fn test_input_viewport() -> Result<(), Error> { - let (server, room_list) = new_room_list_service().await?; + let (_, server, room_list) = new_room_list_service().await?; let sync = room_list.sync(); pin_mut!(sync); diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index 18a3c68cf..c0d8a1779 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -373,6 +373,7 @@ impl SlidingSyncListInner { #[instrument(skip(self), fields(name = self.name))] fn request(&self, ranges: Ranges, txn_id: &mut LazyTransactionId) -> v4::SyncRequestList { use ruma::UInt; + let ranges = ranges.into_iter().map(|r| (UInt::from(*r.start()), UInt::from(*r.end()))).collect();