From 72020cb5810c2d5a94f0b3a37a7f081a09a5e917 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 23 Jan 2026 21:46:14 +0800 Subject: [PATCH 01/26] add image upload --- src/room/room_input_bar.rs | 41 +++++++++++++ src/shared/styles.rs | 1 + src/sliding_sync.rs | 121 +++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 982f430e..07d35596 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -38,6 +38,7 @@ live_design! { use link::tsp_link::TspSignAnycastCheckbox; ICO_LOCATION_PERSON = dep("crate://self/resources/icons/location-person.svg") + ICO_ADD_IMAGE = dep("crate://self/resources/icons/add_image.svg") pub RoomInputBar = {{RoomInputBar}} { @@ -102,6 +103,20 @@ live_design! { text: "", } + image_upload_button = { + margin: {left: 4} + spacing: 0, + draw_icon: { + svg_file: (ICO_ADD_IMAGE) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg: { + color: (COLOR_PRIMARY), + } + icon_walk: {width: Fit, height: 23, margin: {bottom: -1}} + text: "", + } + // A checkbox that enables TSP signing for the outgoing message. // If TSP is not enabled, this will be an empty invisible view. tsp_sign_checkbox = { @@ -206,7 +221,27 @@ impl Widget for RoomInputBar { } _ => {} } + if let Event::FileDialogResult { path } = event { + if let Some(path) = path { + log!("File selected for upload: {}", path.display()); + submit_async_request(MatrixRequest::Upload { + room_id: room_screen_props.room_name_id.room_id().clone(), + file_path: path.clone(), + replied_to: self.replying_to.take().and_then(|(event_tl_item, _emb)| + event_tl_item.event_id().map(|event_id| + Reply { + event_id: event_id.to_owned(), + enforce_thread: EnforceThread::MaybeThreaded, + } + ) + ), + #[cfg(feature = "tsp")] + sign_with_tsp: self.is_tsp_signing_enabled(cx), + }); + self.clear_replying_to(cx); + } + } if let Event::Actions(actions) = event { self.handle_actions(cx, actions, room_screen_props); } @@ -253,6 +288,12 @@ impl RoomInputBar { self.redraw(cx); } + // Handle the image upload button being clicked. + if self.button(ids!(image_upload_button)).clicked(actions) { + log!("Image upload button clicked; opening file dialog..."); + cx.open_system_openfile_dialog(); + } + // Handle the send location button being clicked. if self.button(ids!(location_preview.send_location_button)).clicked(actions) { let location_preview = self.location_preview(ids!(location_preview)); diff --git a/src/shared/styles.rs b/src/shared/styles.rs index 5dc7ff37..ee1c4302 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -6,6 +6,7 @@ live_design! { use link::widgets::*; pub ICON_ADD = dep("crate://self/resources/icons/add.svg") + pub ICON_ADD_IMAGE = dep("crate://self/resources/icons/add_image.svg") pub ICON_ADD_REACTION = dep("crate://self/resources/icons/add_reaction.svg") pub ICON_ADD_USER = dep("crate://self/resources/icons/add_user.svg") // TODO: FIX pub ICON_ADD_WALLET = dep("crate://self/resources/icons/add_wallet.svg") diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index a49b0929..49a33b07 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -394,6 +394,14 @@ pub enum MatrixRequest { #[cfg(feature = "tsp")] sign_with_tsp: bool, }, + /// Request to upload a file to the given room. + Upload { + room_id: OwnedRoomId, + file_path: std::path::PathBuf, + replied_to: Option, + #[cfg(feature = "tsp")] + sign_with_tsp: bool, + }, /// Sends a notice to the given room that the current user is or is not typing. /// /// This request does not return a response or notify the UI thread, and @@ -1250,6 +1258,119 @@ async fn matrix_worker_task( }); } + MatrixRequest::Upload { + room_id, + file_path, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp: _, + } => { + let Some(client) = get_client() else { continue }; + + // Spawn a new async task that will upload the file. + let _upload_task = Handle::current().spawn(async move { + log!("Uploading file to room {room_id}: {file_path:?}..."); + + // Get the room + let Some(room) = client.get_room(&room_id) else { + error!("Room not found: {room_id}"); + enqueue_popup_notification(PopupItem { + message: format!("Room not found"), + kind: PopupKind::Error, + auto_dismissal_duration: None + }); + return; + }; + + // Read the file + let file_data = match tokio::fs::read(&file_path).await { + Ok(data) => data, + Err(e) => { + error!("Failed to read file {file_path:?}: {e:?}"); + enqueue_popup_notification(PopupItem { + message: format!("Failed to read file: {e}"), + kind: PopupKind::Error, + auto_dismissal_duration: None + }); + return; + } + }; + + // Get the filename + let filename = file_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + // Detect the mime type + let mime_str = mime_guess::from_path(&file_path) + .first_or_octet_stream() + .to_string(); + + use mime_guess::mime; + let mime_type: mime::Mime = mime_str.parse().unwrap_or(mime::APPLICATION_OCTET_STREAM); + + log!("File: {filename}, size: {} bytes, mime: {mime_type}", file_data.len()); + + // Upload the file to the media repository and send the message + use ruma::UInt; + + let attachment_config = matrix_sdk::attachment::AttachmentConfig::new() + .info(if mime_type.type_() == mime::IMAGE { + matrix_sdk::attachment::AttachmentInfo::Image( + matrix_sdk::attachment::BaseImageInfo { + height: None, + width: None, + size: Some(UInt::new(file_data.len() as u64).unwrap()), + blurhash: None, + is_animated: None, + } + ) + } else { + matrix_sdk::attachment::AttachmentInfo::File( + matrix_sdk::attachment::BaseFileInfo { + size: Some(UInt::new(file_data.len() as u64).unwrap()), + } + ) + }); + + let result = if let Some(_replied_to_info) = replied_to { + // Note: Matrix SDK doesn't have a direct send_attachment_reply method + // For now, just send the attachment normally + // TODO: Implement proper reply handling for attachments + room.send_attachment( + &filename, + &mime_type, + file_data, + attachment_config, + ).await + } else { + room.send_attachment( + &filename, + &mime_type, + file_data, + attachment_config, + ).await + }; + + match result { + Ok(_) => { + log!("Successfully uploaded and sent file to room {room_id}"); + } + Err(e) => { + error!("Failed to upload file to room {room_id}: {e:?}"); + enqueue_popup_notification(PopupItem { + message: format!("Failed to upload file: {e}"), + kind: PopupKind::Error, + auto_dismissal_duration: None + }); + } + } + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::ReadReceipt { room_id, event_id } => { let timeline = { let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); From 824b6cf63a491c0e90b2fcdd03c934303fc29620 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Sat, 24 Jan 2026 12:57:40 +0800 Subject: [PATCH 02/26] added progress bar widget --- src/room/room_input_bar.rs | 68 ++++++++++++++++++++++- src/shared/mod.rs | 3 +- src/shared/progress.rs | 111 +++++++++++++++++++++++++++++++++++++ src/sliding_sync.rs | 30 +++++++--- 4 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 src/shared/progress.rs diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 07d35596..0b3eba04 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -16,10 +16,10 @@ //! use makepad_widgets::*; -use matrix_sdk::room::reply::{EnforceThread, Reply}; +use matrix_sdk::{TransmissionProgress, room::reply::{EnforceThread, Reply}}; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, RoomMessageEventContent}, OwnedRoomId}; -use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt}, location_preview::LocationPreviewWidgetExt, room_screen::{populate_preview_of_timeline_item, MessageAction, RoomScreenProps}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, styles::*}, sliding_sync::{submit_async_request, MatrixRequest, UserPowerLevels}, utils}; +use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt}, location_preview::LocationPreviewWidgetExt, room_screen::{MessageAction, RoomScreenProps, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, progress::MyProgressWidgetExt, styles::*}, sliding_sync::{MatrixRequest, UserPowerLevels, submit_async_request}, utils}; live_design! { use link::theme::*; @@ -30,6 +30,7 @@ live_design! { use crate::shared::avatar::Avatar; use crate::shared::html_or_plaintext::*; use crate::shared::mentionable_text_input::MentionableTextInput; + use crate::shared::progress::MyProgress; use crate::room::reply_preview::*; use crate::home::location_preview::*; use crate::home::tombstone_footer::TombstoneFooter; @@ -69,6 +70,30 @@ live_design! { // Below that, display a preview of the current location that a user is about to send. location_preview = { } + // Upload progress bar + upload_progress_view = { + visible: false, + width: Fill, + height: Fit, + padding: {top: 8, bottom: 8, left: 10, right: 10} + flow: Down, + spacing: 5, + + progress_label =