From 1dc428b550ac6a8b1307fdb9b46dd19caaa7a547 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Mon, 27 Apr 2026 11:23:59 +0800 Subject: [PATCH 1/4] search result --- src/app.rs | 286 ++--------------- src/shared/mod.rs | 2 + src/shared/room_filter_search_results.rs | 375 +++++++++++++++++++++++ 3 files changed, 409 insertions(+), 254 deletions(-) create mode 100644 src/shared/room_filter_search_results.rs diff --git a/src/app.rs b/src/app.rs index ab56d7960..918348142 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,17 +6,17 @@ use std::{fs::{File, OpenOptions}, io::Write, sync::Mutex}; use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId, events::room::message::RoomMessageEventContent}}; +use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, events::room::message::RoomMessageEventContent}}; use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ + avatar_cache::{self, clear_avatar_cache}, home::{ add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt, StartChatModalAction, StartChatModalWidgetRefExt}, bot_binding_modal::{BotBindingModalAction, BotBindingModalWidgetRefExt}, event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt, mark_invite_modal_closed}, invite_screen::{InviteScreenWidgetRefExt, LeaveRoomResultAction}, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, RoomScreenWidgetRefExt, TimelineUpdate, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::FilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, get_client, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::FilterAction, room_filter_search_results::{RoomFilterResultAction, RoomFilterResultTarget, RoomFilterSearchResultsListWidgetRefExt}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, get_client, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -26,61 +26,6 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* - let RoomFilterResultItem = View { - visible: false - width: Fill - height: 48 - flow: Overlay - - row := View { - width: Fill - height: Fill - flow: Right - align: Align{y: 0.5} - spacing: 8 - padding: Inset{left: 8, right: 8, top: 5, bottom: 5} - - avatar := Avatar { width: 30, height: 30 } - - text_col := View { - width: Fill - height: Fit - flow: Down - spacing: 0 - - name_label := Label { - width: Fill - height: Fit - draw_text +: { - color: (COLOR_TEXT) - text_style: REGULAR_TEXT {font_size: 10} - } - } - - id_label := Label { - width: Fill - height: Fit - draw_text +: { - color: (COLOR_TEXT_INPUT_IDLE) - text_style: REGULAR_TEXT {font_size: 8.5} - } - } - } - } - - click_button := RobrixNeutralIconButton { - width: Fill - height: Fill - text: "" - icon_walk: Walk{width: 0, height: 0} - draw_bg +: { - color: #0000 - color_hover: #FFFFFF22 - color_down: #FFFFFF11 - } - } - } - load_all_resources() do #(App::script_component(vm)) { ui: Root { main_window := Window { @@ -244,21 +189,7 @@ script_mod! { } } - search_results_list := View { - width: Fill, - height: Fit, - flow: Down - spacing: 3 - - result_item_0 := RoomFilterResultItem {} - result_item_1 := RoomFilterResultItem {} - result_item_2 := RoomFilterResultItem {} - result_item_3 := RoomFilterResultItem {} - result_item_4 := RoomFilterResultItem {} - result_item_5 := RoomFilterResultItem {} - result_item_6 := RoomFilterResultItem {} - result_item_7 := RoomFilterResultItem {} - } + search_results_list := RoomFilterSearchResultsList {} } } } @@ -334,15 +265,6 @@ script_mod! { app_main!(App); -#[derive(Clone)] -enum RoomFilterResultTarget { - LocalSpace { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, - LocalRoom { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, - RemoteSpace { space_name_id: RoomNameId, avatar_uri: Option }, - RemoteRoom { room_name_id: RoomNameId, avatar_uri: Option }, - RemoteUser(UserProfile), -} - #[derive(Clone, Debug)] pub enum RoomFilterRemoteSearchAction { Results { @@ -378,7 +300,6 @@ pub struct App { /// A stack of previously-selected rooms for mobile navigation. /// When a view is popped off the stack, the previous `selected_room` is restored from here. #[rust] mobile_room_nav_stack: Vec, - #[rust] room_filter_modal_results: Vec, #[rust(Timer::empty())] room_filter_debounce_timer: Timer, #[rust] pending_room_filter_keywords: String, } @@ -652,7 +573,9 @@ impl MatchEvent for App { fn handle_signal(&mut self, cx: &mut Cx) { avatar_cache::process_avatar_updates(cx); - self.refresh_room_filter_modal_result_buttons(cx); + // Redraw search results list to pick up newly-loaded avatars + self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)) + .redraw(cx); } fn handle_timer(&mut self, cx: &mut Cx, event: &TimerEvent) { @@ -681,29 +604,30 @@ impl MatchEvent for App { self.ui.modal(cx, ids!(positive_confirmation_modal)).close(cx); } - if let Some(clicked_index) = self.clicked_room_filter_result_index(cx, actions) { - if let Some(target) = self.room_filter_modal_results.get(clicked_index).cloned() { + // Handle search result clicks from the FlatList-based search results widget + for action in actions { + if let Some(RoomFilterResultAction::Clicked(target)) = action.downcast_ref() { self.ui.modal(cx, ids!(room_filter_modal)).close(cx); match target { RoomFilterResultTarget::LocalSpace { room_name_id: space_name_id, .. } => { - cx.action(NavigationBarAction::GoToSpace { space_name_id }); + cx.action(NavigationBarAction::GoToSpace { space_name_id: space_name_id.clone() }); } RoomFilterResultTarget::LocalRoom { room_name_id, .. } => { - self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id)); + self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); } RoomFilterResultTarget::RemoteSpace { space_name_id, .. } => { self.open_join_from_search_result( cx, - BasicRoomDetails::Name(space_name_id), + BasicRoomDetails::Name(space_name_id.clone()), true, ); } RoomFilterResultTarget::RemoteRoom { room_name_id, .. } => { self.open_join_from_search_result( cx, - BasicRoomDetails::Name(room_name_id), + BasicRoomDetails::Name(room_name_id.clone()), false, ); } @@ -713,7 +637,7 @@ impl MatchEvent for App { user_profile.user_id.as_ref(), current_user_id().as_deref(), ), - user_profile, + user_profile: user_profile.clone(), allow_create: false, }); } @@ -910,30 +834,28 @@ impl MatchEvent for App { if room_filter_input.text().trim() != query.trim() { continue; } - self.room_filter_modal_results.clear(); + let search_results_list = self.ui.room_filter_search_results_list(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); + let mut new_results = Vec::new(); for result in results { match result { RemoteDirectorySearchResult::User(user_profile) => { - self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteUser(user_profile.clone())); + new_results.push(RoomFilterResultTarget::RemoteUser(user_profile.clone())); } RemoteDirectorySearchResult::Room { room_name_id, avatar_uri } => { - self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteRoom { + new_results.push(RoomFilterResultTarget::RemoteRoom { room_name_id: room_name_id.clone(), avatar_uri: avatar_uri.clone(), }); } RemoteDirectorySearchResult::Space { space_name_id, avatar_uri } => { - self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteSpace { + new_results.push(RoomFilterResultTarget::RemoteSpace { space_name_id: space_name_id.clone(), avatar_uri: avatar_uri.clone(), }); } } - if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { - break; - } } - if self.room_filter_modal_results.is_empty() { + if new_results.is_empty() { self.set_room_filter_modal_empty_state( cx, &tr_fmt(self.app_state.app_language, "app.room_filter.no_server_results", &[ @@ -944,7 +866,7 @@ impl MatchEvent for App { } else { self.set_room_filter_modal_empty_state(cx, "", false); } - self.refresh_room_filter_modal_result_buttons(cx); + search_results_list.set_results(cx, new_results); continue; } Some(RoomFilterRemoteSearchAction::Failed { query, kind: _, error }) => { @@ -952,8 +874,8 @@ impl MatchEvent for App { if room_filter_input.text().trim() != query.trim() { continue; } - self.room_filter_modal_results.clear(); - self.refresh_room_filter_modal_result_buttons(cx); + let search_results_list = self.ui.room_filter_search_results_list(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); + search_results_list.clear(cx); self.set_room_filter_modal_empty_state( cx, &tr_fmt(self.app_state.app_language, "app.room_filter.search_remote_failed", &[ @@ -1616,13 +1538,6 @@ impl AppMain for App { } impl App { - const ROOM_FILTER_RESULT_ITEM_IDS: [LiveId; 8] = [ - live_id!(result_item_0), live_id!(result_item_1), - live_id!(result_item_2), live_id!(result_item_3), - live_id!(result_item_4), live_id!(result_item_5), - live_id!(result_item_6), live_id!(result_item_7), - ]; - fn sync_app_language(&self, cx: &mut Cx) { let app_language = self.app_state.app_language; self.ui.label(cx, ids!(room_filter_modal_inner.search_results_title)) @@ -1664,16 +1579,6 @@ impl App { self.ui.view(cx, ids!(home_screen_view)).set_visible(cx, show_home); } - fn clicked_room_filter_result_index(&self, cx: &mut Cx, actions: &Actions) -> Option { - let list_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); - for (index, item_id) in Self::ROOM_FILTER_RESULT_ITEM_IDS.iter().enumerate() { - if list_view.button(cx, &[*item_id, live_id!(click_button)]).clicked(actions) { - return Some(index); - } - } - None - } - fn clicked_room_filter_remote_option(&self, cx: &mut Cx, actions: &Actions) -> Option { let options_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options)); if options_view.button(cx, ids!(remote_search_people_button)).clicked(actions) { @@ -1719,149 +1624,21 @@ impl App { .set_visible(cx, show_remote_options); } - fn set_room_filter_result_avatar( - &self, - cx: &mut Cx, - avatar_ref: &crate::shared::avatar::AvatarRef, - fallback_text: &str, - local_avatar: Option<&FetchedRoomAvatar>, - remote_avatar_uri: Option<&OwnedMxcUri>, - remote_avatar_state: Option<&AvatarState>, - ) { - if let Some(local_avatar) = local_avatar { - match local_avatar { - FetchedRoomAvatar::Text(text) => { - avatar_ref.show_text(cx, None, None, text); - } - FetchedRoomAvatar::Image(image_data) => { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), - ); - if res.is_err() { - avatar_ref.show_text(cx, None, None, fallback_text); - } - } - } - return; - } - - if let Some(avatar_state) = remote_avatar_state { - if let Some(image_data) = avatar_state.data() { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), - ); - if res.is_ok() { - return; - } - } - if let Some(uri) = avatar_state.uri() { - if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), - ); - if res.is_ok() { - return; - } - } - } - } - - if let Some(uri) = remote_avatar_uri { - if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), - ); - if res.is_ok() { - return; - } - } - } - - avatar_ref.show_text(cx, None, None, fallback_text); - } - - fn refresh_room_filter_modal_result_buttons(&self, cx: &mut Cx) { - let list_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); - for (index, item_id) in Self::ROOM_FILTER_RESULT_ITEM_IDS.iter().enumerate() { - let item = list_view.view(cx, &[*item_id]); - if let Some(target) = self.room_filter_modal_results.get(index) { - let (name, raw_id) = match target { - RoomFilterResultTarget::LocalSpace { room_name_id, .. } - | RoomFilterResultTarget::LocalRoom { room_name_id, .. } => { - (room_name_id.to_string(), room_name_id.room_id().to_string()) - } - RoomFilterResultTarget::RemoteSpace { space_name_id, .. } - | RoomFilterResultTarget::RemoteRoom { room_name_id: space_name_id, .. } => { - (space_name_id.to_string(), space_name_id.room_id().to_string()) - } - RoomFilterResultTarget::RemoteUser(user_profile) => { - (user_profile.displayable_name().to_owned(), user_profile.user_id.to_string()) - } - }; - - item.label(cx, ids!(row.text_col.name_label)).set_text(cx, &name); - item.label(cx, ids!(row.text_col.id_label)).set_text(cx, &raw_id); - - let avatar_ref = item.avatar(cx, ids!(row.avatar)); - match target { - RoomFilterResultTarget::LocalSpace { avatar, .. } - | RoomFilterResultTarget::LocalRoom { avatar, .. } => { - self.set_room_filter_result_avatar(cx, &avatar_ref, &name, Some(avatar), None, None); - } - RoomFilterResultTarget::RemoteSpace { avatar_uri, .. } - | RoomFilterResultTarget::RemoteRoom { avatar_uri, .. } => { - self.set_room_filter_result_avatar(cx, &avatar_ref, &name, None, avatar_uri.as_ref(), None); - } - RoomFilterResultTarget::RemoteUser(user_profile) => { - self.set_room_filter_result_avatar( - cx, - &avatar_ref, - &name, - None, - None, - Some(&user_profile.avatar_state), - ); - } - } - - item.set_visible(cx, true); - } else { - item.set_visible(cx, false); - } - } - } - fn update_room_filter_modal_results(&mut self, cx: &mut Cx, keywords: &str) { let keywords = keywords.trim(); - self.room_filter_modal_results.clear(); + let mut results = Vec::new(); if !keywords.is_empty() { let space_items = cx.get_global::() .get_matching_space_items(keywords, 4); let room_items = cx.get_global::() - .get_matching_room_items(keywords, 8); + .get_matching_room_items(keywords, 12); for (room_name_id, avatar) in space_items { - self.room_filter_modal_results.push(RoomFilterResultTarget::LocalSpace { room_name_id, avatar }); - if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { - break; - } + results.push(RoomFilterResultTarget::LocalSpace { room_name_id, avatar }); } - if self.room_filter_modal_results.len() < Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { - for (room_name_id, avatar) in room_items { - self.room_filter_modal_results.push(RoomFilterResultTarget::LocalRoom { room_name_id, avatar }); - if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { - break; - } - } + for (room_name_id, avatar) in room_items { + results.push(RoomFilterResultTarget::LocalRoom { room_name_id, avatar }); } } @@ -1871,7 +1648,7 @@ impl App { tr_key(self.app_state.app_language, "app.room_filter.empty_hint"), false, ); - } else if self.room_filter_modal_results.is_empty() { + } else if results.is_empty() { self.set_room_filter_modal_empty_state( cx, &tr_fmt( @@ -1885,7 +1662,8 @@ impl App { self.set_room_filter_modal_empty_state(cx, "", false); } - self.refresh_room_filter_modal_result_buttons(cx); + let search_results_list = self.ui.room_filter_search_results_list(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); + search_results_list.set_results(cx, results); } /// Navigates to the given `destination_room`, optionally closing the `room_to_close`. diff --git a/src/shared/mod.rs b/src/shared/mod.rs index e9a04b020..a416dbeff 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -13,6 +13,7 @@ pub mod mentionable_text_input; pub mod popup_list; pub mod progress_bar; pub mod room_filter_input_bar; +pub mod room_filter_search_results; pub mod styles; pub mod text_or_image; pub mod timestamp; @@ -35,6 +36,7 @@ pub fn script_mod(vm: &mut ScriptVm) { timestamp::script_mod(vm); room_filter_input_bar::script_mod(vm); avatar::script_mod(vm); + room_filter_search_results::script_mod(vm); text_or_image::script_mod(vm); html_or_plaintext::script_mod(vm); bouncing_dots::script_mod(vm); diff --git a/src/shared/room_filter_search_results.rs b/src/shared/room_filter_search_results.rs new file mode 100644 index 000000000..0f9e3ee4d --- /dev/null +++ b/src/shared/room_filter_search_results.rs @@ -0,0 +1,375 @@ +//! A FlatList-based search results widget for the room filter modal. +//! +//! This module provides: +//! - `RoomFilterSearchResultsList`: A widget containing a FlatList of search results +//! - `RoomFilterSearchResultItem`: Individual clickable search result items +//! - `RoomFilterResultAction`: Actions emitted when results are clicked + +use makepad_widgets::*; +use matrix_sdk::{RoomDisplayName, ruma::{OwnedMxcUri, OwnedRoomId}}; + +use crate::{ + avatar_cache::{self, AvatarCacheEntry}, + profile::user_profile::UserProfile, + room::FetchedRoomAvatar, + shared::avatar::AvatarWidgetExt, + utils::RoomNameId, +}; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + // Individual search result item template + mod.widgets.RoomFilterSearchResultItem = #(RoomFilterSearchResultItem::register_widget(vm)) { + width: Fill + height: 48 + flow: Overlay + + row := View { + width: Fill + height: Fill + flow: Right + align: Align{y: 0.5} + spacing: 8 + padding: Inset{left: 8, right: 8, top: 5, bottom: 5} + + avatar := Avatar { width: 30, height: 30 } + + text_col := View { + width: Fill + height: Fit + flow: Down + spacing: 0 + + name_label := Label { + width: Fill + height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: REGULAR_TEXT {font_size: 10} + } + } + + id_label := Label { + width: Fill + height: Fit + draw_text +: { + color: (COLOR_TEXT_INPUT_IDLE) + text_style: REGULAR_TEXT {font_size: 8.5} + } + } + } + } + + click_button := RobrixNeutralIconButton { + width: Fill + height: Fill + text: "" + icon_walk: Walk{width: 0, height: 0} + draw_bg +: { + color: #0000 + color_hover: #FFFFFF22 + color_down: #FFFFFF11 + } + } + } + + // The FlatList container for search results + mod.widgets.RoomFilterSearchResultsList = #(RoomFilterSearchResultsList::register_widget(vm)) { + width: Fill + height: Fit + flow: Down + spacing: 3 + + list := FlatList { + width: Fill + height: Fit + spacing: 3.0 + flow: Down + + grab_key_focus: false + drag_scrolling: false + scroll_bars +: { show_scroll_x: false, show_scroll_y: false } + + result_item := RoomFilterSearchResultItem {} + } + } +} + +/// The data for a single search result item, passed via Scope. +#[derive(Clone, Debug)] +pub enum RoomFilterResultTarget { + LocalSpace { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, + LocalRoom { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, + RemoteSpace { space_name_id: RoomNameId, avatar_uri: Option }, + RemoteRoom { room_name_id: RoomNameId, avatar_uri: Option }, + RemoteUser(UserProfile), +} + +impl RoomFilterResultTarget { + /// Returns the display name and raw ID for this result. + pub fn name_and_id(&self) -> (String, String) { + match self { + RoomFilterResultTarget::LocalSpace { room_name_id, .. } + | RoomFilterResultTarget::LocalRoom { room_name_id, .. } => { + (room_name_id.to_string(), room_name_id.room_id().to_string()) + } + RoomFilterResultTarget::RemoteSpace { space_name_id, .. } + | RoomFilterResultTarget::RemoteRoom { room_name_id: space_name_id, .. } => { + (space_name_id.to_string(), space_name_id.room_id().to_string()) + } + RoomFilterResultTarget::RemoteUser(user_profile) => { + (user_profile.displayable_name().to_owned(), user_profile.user_id.to_string()) + } + } + } +} + +/// Action emitted when a search result is clicked. +#[derive(Clone, Debug, Default)] +pub enum RoomFilterResultAction { + #[default] + None, + Clicked(RoomFilterResultTarget), +} + +/// Individual search result item widget. +#[derive(Script, ScriptHook, Widget)] +pub struct RoomFilterSearchResultItem { + #[deref] view: View, + #[rust] target: Option, +} + +impl Widget for RoomFilterSearchResultItem { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + if let Event::Actions(actions) = event { + if self.view.button(cx, ids!(click_button)).clicked(actions) { + if let Some(target) = self.target.clone() { + cx.action(RoomFilterResultAction::Clicked(target)); + } + } + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + // Get the target from scope props + if let Some(target) = scope.props.get::() { + self.target = Some(target.clone()); + let (name, raw_id) = target.name_and_id(); + + self.view.label(cx, ids!(row.text_col.name_label)).set_text(cx, &name); + self.view.label(cx, ids!(row.text_col.id_label)).set_text(cx, &raw_id); + + let avatar_ref = self.view.avatar(cx, ids!(row.avatar)); + self.set_avatar(cx, &avatar_ref, &name, target); + } + + self.view.draw_walk(cx, scope, walk) + } +} + +impl RoomFilterSearchResultItem { + fn set_avatar( + &self, + cx: &mut Cx2d, + avatar_ref: &crate::shared::avatar::AvatarRef, + fallback_text: &str, + target: &RoomFilterResultTarget, + ) { + match target { + RoomFilterResultTarget::LocalSpace { avatar, .. } + | RoomFilterResultTarget::LocalRoom { avatar, .. } => { + match avatar { + FetchedRoomAvatar::Text(text) => { + avatar_ref.show_text(cx, None, None, text); + } + FetchedRoomAvatar::Image(image_data) => { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_err() { + avatar_ref.show_text(cx, None, None, fallback_text); + } + } + } + } + RoomFilterResultTarget::RemoteSpace { avatar_uri, .. } + | RoomFilterResultTarget::RemoteRoom { avatar_uri, .. } => { + if let Some(uri) = avatar_uri { + if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_ok() { + return; + } + } + } + avatar_ref.show_text(cx, None, None, fallback_text); + } + RoomFilterResultTarget::RemoteUser(user_profile) => { + if let Some(image_data) = user_profile.avatar_state.data() { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_ok() { + return; + } + } + if let Some(uri) = user_profile.avatar_state.uri() { + if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_ok() { + return; + } + } + } + avatar_ref.show_text(cx, None, None, fallback_text); + } + } + } +} + +/// The FlatList-based search results list widget. +#[derive(Script, ScriptHook, Widget)] +pub struct RoomFilterSearchResultsList { + #[deref] view: View, + #[rust] results: Vec, +} + +impl Widget for RoomFilterSearchResultsList { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { + let flat_list_ref = subview.as_flat_list(); + let Some(mut list) = flat_list_ref.borrow_mut() else { + continue; + }; + + for (index, target) in self.results.iter().enumerate() { + let item_id = LiveId(index as u64); + let item = list.item(cx, item_id, id!(result_item)).unwrap(); + let mut scope = Scope::with_props(target); + item.draw_all(cx, &mut scope); + } + } + DrawStep::done() + } +} + +impl RoomFilterSearchResultsList { + /// Set the search results to display. + pub fn set_results(&mut self, cx: &mut Cx, results: Vec) { + self.results = results; + self.view.redraw(cx); + } + + /// Clear all search results. + pub fn clear(&mut self, cx: &mut Cx) { + self.results.clear(); + self.view.redraw(cx); + } + + /// Check if the list is empty. + pub fn is_empty(&self) -> bool { + self.results.is_empty() + } + + /// Get the number of results. + pub fn len(&self) -> usize { + self.results.len() + } + + /// Populate with default test data for development/testing. + #[allow(dead_code)] + pub fn populate_test_data(&mut self, cx: &mut Cx) { + let test_results = vec![ + RoomFilterResultTarget::LocalRoom { + room_name_id: RoomNameId::new( + RoomDisplayName::Named("General Chat".to_string()), + OwnedRoomId::try_from("!abc123:127.0.0.1:8128").unwrap(), + ), + avatar: FetchedRoomAvatar::Text("GC".to_string()), + }, + RoomFilterResultTarget::LocalRoom { + room_name_id: RoomNameId::new( + RoomDisplayName::Named("Development".to_string()), + OwnedRoomId::try_from("!dev456:127.0.0.1:8128").unwrap(), + ), + avatar: FetchedRoomAvatar::Text("DE".to_string()), + }, + RoomFilterResultTarget::LocalRoom { + room_name_id: RoomNameId::new( + RoomDisplayName::Named("Random".to_string()), + OwnedRoomId::try_from("!rand789:127.0.0.1:8128").unwrap(), + ), + avatar: FetchedRoomAvatar::Text("RA".to_string()), + }, + RoomFilterResultTarget::LocalSpace { + room_name_id: RoomNameId::new( + RoomDisplayName::Named("Project Alpha".to_string()), + OwnedRoomId::try_from("!alpha:127.0.0.1:8128").unwrap(), + ), + avatar: FetchedRoomAvatar::Text("PA".to_string()), + }, + RoomFilterResultTarget::LocalRoom { + room_name_id: RoomNameId::new( + RoomDisplayName::Named("Support".to_string()), + OwnedRoomId::try_from("!support:127.0.0.1:8128").unwrap(), + ), + avatar: FetchedRoomAvatar::Text("SU".to_string()), + }, + ]; + self.set_results(cx, test_results); + } +} + +impl RoomFilterSearchResultsListRef { + /// Set the search results to display. + pub fn set_results(&self, cx: &mut Cx, results: Vec) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_results(cx, results); + } + } + + /// Clear all search results. + pub fn clear(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.clear(cx); + } + } + + /// Check if the list is empty. + pub fn is_empty(&self) -> bool { + self.borrow().map(|inner| inner.is_empty()).unwrap_or(true) + } + + /// Get the number of results. + pub fn len(&self) -> usize { + self.borrow().map(|inner| inner.len()).unwrap_or(0) + } + + /// Populate with default test data for development/testing. + #[allow(dead_code)] + pub fn populate_test_data(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.populate_test_data(cx); + } + } +} From 39c2fa43a3f60c0c27cdb271ea293fbaa945738d Mon Sep 17 00:00:00 2001 From: alanpoon Date: Mon, 27 Apr 2026 15:33:30 +0800 Subject: [PATCH 2/4] roomfilterSearchResultItem change to FlatList --- src/app.rs | 2 +- src/shared/room_filter_search_results.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index e472d4fe2..3d7acc91e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -191,7 +191,7 @@ script_mod! { } } - search_results_list := RoomFilterSearchResultsList {} + search_results_list := mod.widgets.RoomFilterSearchResultsList {} } } } diff --git a/src/shared/room_filter_search_results.rs b/src/shared/room_filter_search_results.rs index 0f9e3ee4d..4a4f362b6 100644 --- a/src/shared/room_filter_search_results.rs +++ b/src/shared/room_filter_search_results.rs @@ -23,7 +23,7 @@ script_mod! { // Individual search result item template mod.widgets.RoomFilterSearchResultItem = #(RoomFilterSearchResultItem::register_widget(vm)) { width: Fill - height: 48 + height: 55 flow: Overlay row := View { @@ -92,7 +92,7 @@ script_mod! { drag_scrolling: false scroll_bars +: { show_scroll_x: false, show_scroll_y: false } - result_item := RoomFilterSearchResultItem {} + result_item := mod.widgets.RoomFilterSearchResultItem {} } } } From db54651051a2ab86139f8341ef7f386e2e795c00 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Mon, 27 Apr 2026 15:34:47 +0800 Subject: [PATCH 3/4] remove log --- src/sliding_sync.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 675dfcd36..8d4ea4e05 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -2243,7 +2243,6 @@ async fn matrix_worker_task( let _search_task = Handle::current().spawn(async move { let query = query.trim().to_owned(); let action_kind = kind.clone(); - log!("Remote directory search request: kind={kind:?}, query=\"{query}\", limit={limit}"); if query.is_empty() { Cx::post_action(RoomFilterRemoteSearchAction::Results { query, From 3cc3b82d2bfcec1cfd00599c8a7b586ee88c4ebe Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 15 May 2026 16:34:38 +0800 Subject: [PATCH 4/4] added confirm_password_input --- src/login/login_screen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 304d3e07f..26ee746af 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -1107,6 +1107,7 @@ impl WidgetMatchEvent for LoginScreen { } // Handle toggling confirm password visibility + let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let show_confirm_pw_button = self.view.button(cx, ids!(show_confirm_password_button)); let hide_confirm_pw_button = self.view.button(cx, ids!(hide_confirm_password_button)); if show_confirm_pw_button.clicked(actions) || hide_confirm_pw_button.clicked(actions) {