From a58f63a486f86557156964a0948289ad3874e21f Mon Sep 17 00:00:00 2001 From: smarizvi110 Date: Sun, 16 Feb 2025 22:40:34 +0500 Subject: [PATCH 1/2] Add logic for searching messages in a room --- src/home/room_screen.rs | 77 ++++++++++++++++++++++++++++++--- src/shared/mod.rs | 1 + src/shared/search.rs | 13 ++++++ src/sliding_sync.rs | 94 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 src/shared/search.rs diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 0f8ad193..fb270c45 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -29,12 +29,9 @@ use crate::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, shared::{ - avatar::{AvatarRef, AvatarWidgetRefExt}, - html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, - text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, - typing_animation::TypingAnimationWidgetExt, + avatar::{AvatarRef, AvatarWidgetRefExt}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, search::SearchUpdate, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt }, - sliding_sync::{get_client, submit_async_request, take_timeline_update_receiver, MatrixRequest}, utils::{self, unix_time_millis_to_datetime, MediaFormatConst} + sliding_sync::{get_client, submit_async_request, take_timeline_update_receiver, MatrixRequest, SEARCH_RESULTS_RECEIVER}, utils::{self, unix_time_millis_to_datetime, MediaFormatConst} }; use rangemap::RangeSet; @@ -940,9 +937,71 @@ struct RoomScreen { #[rust] tl_state: Option, /// 5 secs timer when scroll ends #[rust] fully_read_timer: Timer, + /// The current search state for the room. + #[rust] search_state: Option, + /// The last search term used in the room. + #[rust] last_search_term: String, } impl RoomScreen{ + + fn start_message_search(&mut self, room_id: OwnedRoomId, search_term: String) { + let (update_sender, update_receiver) = crossbeam_channel::unbounded(); + let search_state = SearchUiState { + room_id: room_id.clone(), + fully_paginated: false, + items: Vector::new(), + next_batch: None, + update_receiver, + }; + self.search_state = Some(search_state); + + submit_async_request(MatrixRequest::SearchRoomMessages { + room_id, + search_term, + next_batch: None, + }); + } + + fn process_search_updates(&mut self, cx: &mut Cx) { + let Some(search_state) = self.search_state.as_mut() else { return }; + + let Some(receiver) = SEARCH_RESULTS_RECEIVER.get() else { return }; + + while let Ok(update) = receiver.try_recv() { + match update { + SearchUpdate::NewResults { room_id, results, next_batch } => { + if search_state.room_id == room_id { + search_state.items.extend(results); + search_state.next_batch = next_batch.clone(); + + // Stop pagination if no more results + if next_batch.is_none() { + search_state.fully_paginated = true; + } + } + } + } + } + self.redraw(cx); + } + + + fn paginate_search_results(&mut self) { + let Some(search_state) = self.search_state.as_mut() else { return }; + if search_state.fully_paginated { + return; + } + + if let Some(next_batch) = search_state.next_batch.clone() { + submit_async_request(MatrixRequest::SearchRoomMessages { + room_id: search_state.room_id.clone(), + search_term: self.last_search_term.clone(), + next_batch: Some(next_batch), + }); + } + } + fn send_user_read_receipts_based_on_scroll_pos( &mut self, cx: &mut Cx, @@ -1919,6 +1978,14 @@ struct TimelineUiState { marked_fully_read_queue: HashMap, } +struct SearchUiState { + room_id: OwnedRoomId, + fully_paginated: bool, + items: Vector>, + next_batch: Option, + update_receiver: crossbeam_channel::Receiver, +} + /// The item index, scroll position, and optional unique IDs of the first `N` events /// that have been drawn in the most recent draw pass of a timeline's PortalList. #[derive(Debug)] diff --git a/src/shared/mod.rs b/src/shared/mod.rs index ebbd8378..3e944398 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -11,6 +11,7 @@ pub mod search_bar; pub mod styles; pub mod text_or_image; pub mod typing_animation; +pub mod search; pub fn live_design(cx: &mut Cx) { // Order matters here, as some widget definitions depend on others. diff --git a/src/shared/search.rs b/src/shared/search.rs new file mode 100644 index 00000000..e612bc99 --- /dev/null +++ b/src/shared/search.rs @@ -0,0 +1,13 @@ +use matrix_sdk::ruma::OwnedRoomId; +use std::sync::Arc; +use matrix_sdk_ui::timeline::TimelineItem; + +/// Search result updates sent from async worker to UI. +pub enum SearchUpdate { + // A new batch of search results has been received. + NewResults { + room_id: OwnedRoomId, + results: Vec>, // List of matching search results + next_batch: Option, // Token for pagination + }, +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 32d5b064..7fb09f14 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -10,12 +10,14 @@ use matrix_sdk::{ media::MediaRequest, room::{Receipts, RoomMember}, ruma::{ - api::client::{receipt::create_receipt::v3::ReceiptType, session::get_login_types::v3::LoginType}, + api::client::{receipt::create_receipt::v3::ReceiptType, session::get_login_types::v3::LoginType, + search::search_events::v3::{Criteria, Categories, Request,} + }, events::{ receipt::ReceiptThread, room::{ message::{ForwardThread, RoomMessageEventContent}, MediaSource, - }, FullStateEventContent + }, FullStateEventContent, AnyTimelineEvent, }, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, UserId }, @@ -26,12 +28,12 @@ use matrix_sdk::{ use matrix_sdk_ui::{ room_list_service::{self, RoomListLoadingState}, sync_service::{self, SyncService}, - timeline::{AnyOtherFullStateEventContent, EventTimelineItem, LiveBackPaginationStatus, RepliedToInfo, TimelineDetails, TimelineItemContent}, + timeline::{AnyOtherFullStateEventContent, EventTimelineItem, LiveBackPaginationStatus, RepliedToInfo, TimelineDetails, TimelineItem, TimelineItemContent}, Timeline, }; use tokio::{ runtime::Handle, - sync::mpsc::{UnboundedSender, UnboundedReceiver}, + sync::mpsc::{UnboundedSender, UnboundedReceiver, unbounded_channel}, task::JoinHandle, }; use unicode_segmentation::UnicodeSegmentation; @@ -43,7 +45,7 @@ use crate::{ }, media_cache::MediaCacheEntry, persistent_state::{self, ClientSessionPersisted}, profile::{ user_profile::{AvatarState, UserProfile}, user_profile_cache::{enqueue_user_profile_update, UserProfileUpdate}, - }, utils::MEDIA_THUMBNAIL_FORMAT, verification::add_verification_event_handlers_and_sync_client + }, shared::search::SearchUpdate, utils::MEDIA_THUMBNAIL_FORMAT, verification::add_verification_event_handlers_and_sync_client }; @@ -264,9 +266,28 @@ pub enum MatrixRequest { FullyReadReceipt{ room_id: OwnedRoomId, event_id: OwnedEventId, - } + }, + + /// Request to search for messages in a room. + SearchRoomMessages { + room_id: OwnedRoomId, + search_term: String, + next_batch: Option, + }, } +// Create a global sender/receiver pair for search results +pub static SEARCH_RESULTS_SENDER: OnceLock> = OnceLock::new(); +pub static SEARCH_RESULTS_RECEIVER: OnceLock> = OnceLock::new(); + +// Initialize the search channel +pub fn init_search_channel() { + let (sender, receiver) = crossbeam_channel::unbounded(); + SEARCH_RESULTS_SENDER.set(sender).unwrap(); + SEARCH_RESULTS_RECEIVER.set(receiver).unwrap(); +} + + /// Submits a request to the worker thread to be executed asynchronously. pub fn submit_async_request(req: MatrixRequest) { REQUEST_SENDER.get() @@ -671,7 +692,66 @@ async fn async_worker(mut receiver: UnboundedReceiver) -> Result< Err(_e) => error!("Failed to send fully read receipt to room {room_id}, event {event_id}; error: {_e:?}"), } }); - } + }, + + MatrixRequest::SearchRoomMessages { room_id, search_term, next_batch } => { + let mut categories = Categories::new(); + categories.room_events = Some(Criteria::new(search_term)); + + let mut search_request = Request::new(categories); + search_request.next_batch = next_batch; + + let client = get_client().unwrap(); + + match client.send(search_request, None).await { + Ok(response) => { + let room_events = response.search_categories.room_events; + + let event_ids: Vec = room_events + .results + .into_iter() + .filter_map(|search_result| { + search_result.result + .and_then(|raw_event| raw_event.deserialize().ok()) + .and_then(|event| match event { + AnyTimelineEvent::MessageLike(msg) => Some(msg.event_id().to_owned()), + AnyTimelineEvent::State(state) => Some(state.event_id().to_owned()), + _ => None, + }) + }) + .collect(); + + if event_ids.is_empty() { + log!("Search found no events."); + return Ok(()); + } + + // Now that we have event IDs, request pagination of those events. + let timeline = { + let mut all_room_info = ALL_ROOM_INFO.lock().unwrap(); + let Some(room_info) = all_room_info.get_mut(&room_id) else { + log!("Skipping search request for not-yet-known room {room_id}"); + return Ok(()); + }; + room_info.timeline.clone() + }; + + // Fetch events by requesting timeline pagination for each found event. + let _fetch_events_task = Handle::current().spawn(async move { + log!("Fetching {} events for search results in room {}", event_ids.len(), room_id); + + for event_id in event_ids { + let _ = timeline.fetch_details_for_event(&event_id).await; + } + + log!("Finished fetching search results."); + }); + } + Err(e) => { + error!("Search request failed: {:?}", e); + } + } + } } } From 844ed8695fe6491e2a8bd0bf80f2b01f6df6e7ba Mon Sep 17 00:00:00 2001 From: smarizvi110 Date: Fri, 28 Feb 2025 21:42:32 +0500 Subject: [PATCH 2/2] Merge upstream main into search-messages-in-room and resolve conflicts --- src/home/room_screen.rs | 25 +------------------------ src/sliding_sync.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index e037f7e3..a483aa7c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -25,7 +25,7 @@ use crate::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, shared::{ - avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt + avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt, search::SearchUpdate }, sliding_sync::{self, get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineRequestSender, UserPowerLevels, SEARCH_RESULTS_RECEIVER}, utils::{self, unix_time_millis_to_datetime, ImageFormat, MediaFormatConst, MEDIA_THUMBNAIL_FORMAT} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; @@ -2815,29 +2815,6 @@ struct SearchUiState { update_receiver: crossbeam_channel::Receiver, } -/// The item index, scroll position, and optional unique IDs of the first `N` events -/// that have been drawn in the most recent draw pass of a timeline's PortalList. -#[derive(Debug)] -struct FirstDrawnEvents { - index_and_scroll: [ItemIndexScroll; N], - event_ids: [Option; N], -} -impl Default for FirstDrawnEvents { - fn default() -> Self { - Self { - index_and_scroll: std::array::from_fn(|_| ItemIndexScroll::default()), - event_ids: std::array::from_fn(|_| None), - } - } -} - -/// -#[derive(Clone, Copy, Debug, Default)] -struct ItemIndexScroll { - index: usize, - scroll: f64, -} - #[derive(Default, Debug)] enum MessageHighlightAnimationState { Pending { item_id: usize }, diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index da9331b4..c6f522ce 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -7,11 +7,13 @@ use futures_util::{pin_mut, StreamExt}; use imbl::Vector; use makepad_widgets::{error, log, warning, Cx, SignalToUI}; use matrix_sdk::{ - config::RequestConfig, event_handler::EventHandlerDropGuard, media::MediaRequest, room::{editL::EditedContent, RoomMember}, ruma::{ - api::client::receipt::create_receipt::v3::ReceiptType, search::search_events::v3::{Criteria, Categories, Request}, events::{ + config::RequestConfig, event_handler::EventHandlerDropGuard, media::MediaRequest, room::{edit::EditedContent, RoomMember}, ruma::{ + api::client::{ + receipt::create_receipt::v3::ReceiptType, search::search_events::v3::{Criteria, Categories, Request} + }, events::{ receipt::ReceiptThread, room::{ message::{ForwardThread, RoomMessageEventContent}, power_levels::RoomPowerLevels, MediaSource - }, FullStateEventContent, MessageLikeEventType, StateEventType + }, FullStateEventContent, MessageLikeEventType, StateEventType, AnyTimelineEvent }, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId }, sliding_sync::VersionBuilder, Client, ClientBuildError, Error, Room, RoomMemberships }; @@ -21,7 +23,7 @@ use matrix_sdk_ui::{ use robius_open::Uri; use tokio::{ runtime::Handle, - sync::{mpsc::{Receiver, Sender, UnboundedReceiver, unbounded_channel, UnboundedSender}, watch, Notify}, task::JoinHandle, + sync::{mpsc::{Receiver, Sender, UnboundedReceiver, UnboundedSender}, watch, Notify}, task::JoinHandle, }; use unicode_segmentation::UnicodeSegmentation; use url::Url; @@ -33,7 +35,7 @@ use crate::{ }, login::login_screen::LoginAction, media_cache::MediaCacheEntry, persistent_state::{self, ClientSessionPersisted}, profile::{ user_profile::{AvatarState, UserProfile}, user_profile_cache::{enqueue_user_profile_update, UserProfileUpdate}, - }, shared::{jump_to_bottom_button::UnreadMessageCount, popup_list::enqueue_popup_notification}, utils::{self, AVATAR_THUMBNAIL_FORMAT}, verification::add_verification_event_handlers_and_sync_client + }, shared::{search::SearchUpdate, jump_to_bottom_button::UnreadMessageCount, popup_list::enqueue_popup_notification}, utils::{self, AVATAR_THUMBNAIL_FORMAT}, verification::add_verification_event_handlers_and_sync_client }; #[derive(Parser, Debug, Default)]