diff --git a/Cargo.lock b/Cargo.lock index fad88d2d..1d6fa1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4669,6 +4669,7 @@ dependencies = [ "quinn", "rand 0.8.5", "rangemap", + "regex", "reqwest", "robius-directories", "robius-location", diff --git a/Cargo.toml b/Cargo.toml index aef2e8c4..0ba6f2a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ bytesize = "2.0" bitflags = "2.6.0" indexmap = "2.6.0" blurhash = { version = "0.2.3", default-features = false } +regex = "1.11.1" ## Dependencies for TSP support. tsp_sdk = { git = "https://github.com/openwallet-foundation-labs/tsp.git", optional = true, features = ["async", "resolve"] } diff --git a/src/app.rs b/src/app.rs index 174e89fb..d96a540d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use makepad_widgets::{makepad_micro_serde::*, *}; use matrix_sdk::ruma::{OwnedRoomId, RoomId}; use crate::{ avatar_cache::clear_avatar_cache, home::{ - main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{clear_timeline_states, MessageAction}, rooms_list::{clear_all_invited_rooms, enqueue_rooms_list_update, RoomsListAction, RoomsListRef, RoomsListUpdate} + home_screen::MessageSearchInputAction, main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{clear_timeline_states, MessageAction}, rooms_list::{clear_all_invited_rooms, enqueue_rooms_list_update, RoomsListAction, RoomsListRef, RoomsListUpdate} }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::callout_tooltip::{ @@ -170,6 +170,7 @@ impl LiveRegister for App { crate::settings::live_design(cx); crate::room::live_design(cx); crate::join_leave_room_modal::live_design(cx); + crate::right_drawer::live_design(cx); crate::verification_modal::live_design(cx); crate::home::live_design(cx); crate::profile::live_design(cx); @@ -287,6 +288,11 @@ impl MatchEvent for App { &Scope::default().path, StackNavigationAction::Push(live_id!(main_content_view)) ); + cx.widget_action( + self.ui.widget_uid(), + &Scope::default().path, + MessageSearchInputAction::Show + ); self.ui.redraw(cx); continue; } @@ -295,10 +301,20 @@ impl MatchEvent for App { match action.downcast_ref() { Some(AppStateAction::RoomFocused(selected_room)) => { self.app_state.selected_room = Some(selected_room.clone()); + cx.widget_action( + self.ui.widget_uid(), + &Scope::default().path, + MessageSearchInputAction::Show + ); continue; } Some(AppStateAction::FocusNone) => { self.app_state.selected_room = None; + cx.widget_action( + self.ui.widget_uid(), + &Scope::default().path, + MessageSearchInputAction::Hide + ); continue; } Some(AppStateAction::UpgradedInviteToJoinedRoom(room_id)) => { @@ -677,7 +693,7 @@ impl Eq for SelectedRoom {} /// Actions sent to the top-level App in order to update / restore its [`AppState`]. /// /// These are *NOT* widget actions. -#[derive(Debug)] +#[derive(Debug, Clone, Default)] pub enum AppStateAction { /// The given room was focused (selected). RoomFocused(SelectedRoom), @@ -698,5 +714,6 @@ pub enum AppStateAction { room_to_close: Option, destination_room: BasicRoomDetails, }, + #[default] None, } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 2b604116..7dd52775 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,6 +1,6 @@ use makepad_widgets::*; -use crate::settings::{settings_screen::SettingsScreenWidgetRefExt, SettingsAction}; +use crate::{settings::{settings_screen::SettingsScreenWidgetRefExt, SettingsAction}, shared::message_search_input_bar::{MessageSearchAction, MessageSearchInputBarRef, MessageSearchInputBarWidgetExt}}; live_design! { use link::theme::*; @@ -12,8 +12,12 @@ live_design! { use crate::home::spaces_dock::SpacesDock; use crate::shared::styles::*; use crate::shared::room_filter_input_bar::RoomFilterInputBar; + use crate::shared::message_search_input_bar::MessageSearchInputBar; + use crate::shared::icon_button::RobrixIconButton; use crate::home::main_desktop_ui::MainDesktopUI; + use crate::home::search_message::SearchResultStackView; use crate::settings::settings_screen::SettingsScreen; + use crate::right_drawer::RightDrawer; NavigationWrapper = {{NavigationWrapper}} { view_stack = {} @@ -52,10 +56,35 @@ live_design! { width: Fill, height: Fill flow: Down - { - room_filter_input_bar = {} + { + width: Fill, height: Fit + flow: Right, + + { + room_filter_input_bar = { + align: {x: 0.0 } + } + } + message_search_input_view = { + width: Fill, height: Fit, + visible: false, + align: {x: 1.0}, + + { + message_search_input_bar = { + width: 300, + } + } + } + } + + { + width: Fill, height: Fill + flow: Right + + {} + {} } - {} } settings_page = { @@ -128,12 +157,56 @@ live_design! { } } } + { + height: Fit, + width: Fill, + align: {x: 1.0 } + { + height: Fit, + width: 140, + { + message_search_input_bar = { + width: 300 + } + } + } + } } } body = { main_content = {} } } + search_result_view = { + flow: Overlay + header = { + height: 50.0, + margin: { top: 30.0 }, + content = { + flow: Right, + title_container = { + width: 0 + } + button_container = { + align: { y: 0.5 } + left_button = { + draw_icon: { + color: #666; + } + text: "Back" + } + } + { + message_search_input_bar = { + width: 300 + } + } + } + } + body = { + margin: { top: 80.0 }, + } + } } } } @@ -188,6 +261,29 @@ impl Widget for HomeScreen { } _ => {} } + match action.as_widget_action().cast() { + MessageSearchInputAction::Show => { + if !cx.has_global::() { + if self.view.message_search_input_bar(id!(message_search_input_bar)).borrow().is_some() { + Cx::set_global(cx, self.view.message_search_input_bar(id!(message_search_input_bar))); + } + } + self.view.view(id!(message_search_input_view)).set_visible(cx, true) + }, + MessageSearchInputAction::Hide => self.view.view(id!(message_search_input_view)).set_visible(cx, false), + } + + if let MessageSearchAction::Clicked = action.as_widget_action().cast() { + if !self.view + .stack_navigation(id!(view_stack)) + .stack_view_ids().contains(&live_id!(search_result_view)) { + cx.widget_action( + self.widget_uid(), + &Scope::default().path, + StackNavigationAction::Push(live_id!(search_result_view)) + ); + } + } } } @@ -242,3 +338,11 @@ impl MatchEvent for NavigationWrapper { .handle_stack_view_actions(cx, actions); } } + +/// An action that controls the visibility of the message search input bar. +#[derive(Clone, Debug, Default)] +pub enum MessageSearchInputAction { + #[default] + Show, + Hide, +} diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 9657c843..acef2ff9 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SelectedRoom}, utils::room_name_or_id}; +use crate::{app::{AppState, AppStateAction, SelectedRoom}, shared::message_search_input_bar::{MessageSearchAction, MessageSearchInputBarRef}, utils::room_name_or_id}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; live_design! { @@ -172,7 +172,13 @@ impl MainDesktopUI { } self.open_rooms.insert(room_id_as_live_id, room.clone()); - self.most_recently_selected_room = Some(room); + self.most_recently_selected_room = Some(room.clone()); + // Call AppStateAction::RoomFocused action to display the search message input box when a room is open. + cx.widget_action( + self.widget_uid(), + &HeapLiveIdPath::default(), + AppStateAction::RoomFocused(room) + ); } /// Closes a tab in the dock and focuses on the latest open room. @@ -206,6 +212,10 @@ impl MainDesktopUI { dock.close_tab(cx, tab_id); self.tab_to_close = None; self.open_rooms.remove(&tab_id); + // Clear the search input when a room is closed + cx.get_global::().set_text(""); + // Clear the search results when a room is closed + cx.widget_action(self.widget_uid(), &Scope::empty().path, MessageSearchAction::Changed(String::new())); } /// Closes all tabs @@ -384,6 +394,13 @@ impl WidgetMatchEvent for MainDesktopUI { if let Some(ref selected_room) = &app_state.selected_room { self.focus_or_create_tab(cx, selected_room.clone()); + } else { + // If there is no selected room, focus on the home tab. + cx.widget_action( + self.widget_uid(), + &HeapLiveIdPath::default(), + AppStateAction::FocusNone, + ); } self.view.redraw(cx); } diff --git a/src/home/mod.rs b/src/home/mod.rs index e4ab4bfd..071a6813 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -20,6 +20,7 @@ pub mod spaces_dock; pub mod welcome_screen; pub mod event_reaction_list; pub mod new_message_context_menu; +pub mod search_message; pub fn live_design(cx: &mut Cx) { home_screen::live_design(cx); @@ -42,4 +43,5 @@ pub fn live_design(cx: &mut Cx) { welcome_screen::live_design(cx); light_themed_dock::live_design(cx); event_reaction_list::live_design(cx); + search_message::live_design(cx); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 7506fa8d..8d3a6e47 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{ }; use crate::{ - app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, editing_pane::EditingPaneState, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::RoomsListRef, tombstone_footer::TombstoneFooterWidgetExt}, location::init_location_subscriber, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppStateAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, editing_pane::EditingPaneState, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::TombstoneFooterWidgetExt}, location::init_location_subscriber, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, shared::{ @@ -37,7 +37,7 @@ use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; use crate::room::room_input_bar::RoomInputBarWidgetExt; use crate::shared::mentionable_text_input::{MentionableTextInputWidgetRefExt, MentionableTextInputAction}; - +use std::num::NonZeroU32; use rangemap::RangeSet; use super::{editing_pane::{EditingPaneAction, EditingPaneWidgetExt}, event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, location_preview::LocationPreviewWidgetExt, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; @@ -51,10 +51,14 @@ const GEO_URI_SCHEME: &str = "geo:"; /// from getting into a long-running loop if an event cannot be found quickly. const MAX_ITEMS_TO_SEARCH_THROUGH: usize = 100; +/// The time required to scroll to the targeted message in milliseconds. +/// +/// This value cannot be zero. +const SMOOTH_SCROLL_TIME: NonZeroU32 = NonZeroU32::new(500).unwrap(); + /// The max size (width or height) of a blurhash image to decode. const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; - live_design! { use link::theme::*; use link::shaders::*; @@ -207,10 +211,10 @@ live_design! { // An empty view that takes up no space in the portal list. - Empty = { } + pub Empty = { } // The view used for each text-based message event in a room's timeline. - Message = {{Message}} { + pub Message = {{Message}} { width: Fill, height: Fit, margin: 0.0 @@ -339,6 +343,28 @@ live_design! { } text: "" } + + jump_to_this_message = { + visible: false, + width: Fit, + height: Fit, + + jump_button =