Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
72020cb
add image upload
alanpoon Jan 23, 2026
824b6cf
added progress bar widget
alanpoon Jan 24, 2026
ba038cc
file upload modal
alanpoon Jan 25, 2026
51e388c
added file_previewer
alanpoon Jan 26, 2026
172462b
code improvement
alanpoon Jan 26, 2026
13109ff
fix clippy
alanpoon Jan 26, 2026
bdb0b34
add FilePreviewer in App
alanpoon Jan 26, 2026
0ca00fb
image upload improvement
alanpoon Jan 30, 2026
2afea24
Merge branch 'main' into image_upload
alanpoon Jan 30, 2026
f922cf2
image_upload improvement
alanpoon Jan 31, 2026
59860ab
fix clippy
alanpoon Jan 31, 2026
cc335f7
Merge branch 'main' into image_upload
alanpoon Feb 5, 2026
36b2432
send attachment improvement
alanpoon Feb 7, 2026
e41250c
fix file upload modal white space issue
alanpoon Feb 7, 2026
46efd21
Merge branch 'main' into image_upload
alanpoon Feb 7, 2026
6f198d0
Merge branch 'main' into image_upload
alanpoon Feb 23, 2026
81f4718
image_upload fix after review
alanpoon Feb 24, 2026
916353c
fix hiding issue in UploadProgressView
alanpoon Feb 24, 2026
99b2df5
cleanup extra line
alanpoon Feb 24, 2026
b4fb87b
max width fix
alanpoon Feb 24, 2026
19630ea
format fix
alanpoon Feb 24, 2026
e9ca7ee
code improvement
alanpoon Feb 25, 2026
65acc69
Merge branch 'image_upload' of https://github.com/alanpoon/robrix int…
alanpoon Feb 25, 2026
3338a8b
Avoid string allocations
alanpoon Feb 26, 2026
98d230e
remove string allocation
alanpoon Feb 26, 2026
295ea3d
PopupItem Fix
alanpoon Feb 26, 2026
e1007c6
fix format
alanpoon Feb 26, 2026
c85e246
Add retry button
alanpoon Feb 26, 2026
b702d7e
Merge branch 'main' into image_upload
alanpoon Feb 26, 2026
b3bc48f
fix clippy
alanpoon Feb 26, 2026
c874b5a
fix formatting
alanpoon Feb 26, 2026
7f7f08b
Fix clippy
alanpoon Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
509 changes: 500 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ hashbrown = { version = "0.16", features = ["raw-entry"] }
htmlize = "1.0.5"
indexmap = "2.6.0"
imghdr = "0.7.0"
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
linkify = "0.10.0"
matrix-sdk-base = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main" }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main", default-features = false, features = [ "e2e-encryption", "automatic-room-key-forwarding", "markdown", "sqlite", "rustls-tls", "bundled-sqlite", "sso-login" ] }
matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main", default-features = false, features = [ "rustls-tls" ] }
mime_guess = "2.0.5"
## Use the same ruma version as what's specified in matrix-sdk's Cargo.toml.
## Enable a few extra features:
## * "compat-optional" feature to allow missing body field in m.room.tombstone event.
Expand All @@ -64,7 +66,7 @@ tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3.17"
unicode-segmentation = "1.11.0"
url = "2.5.0"

rfd = { version = "0.15.3", features = ["ashpd", "urlencoding", "xdg-portal"] }

## Dependencies for TSP support.
## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219.
Expand Down
17 changes: 17 additions & 0 deletions resources/icons/add_attachment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions resources/icons/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
}, 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::{
CalloutTooltipWidgetRefExt,
TooltipAction,
}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{
}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::FilePreviewerAction, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{
VerificationModalAction,
VerificationModalWidgetRefExt,
}
Expand All @@ -39,6 +39,7 @@ live_design! {
use crate::home::event_source_modal::EventSourceModal;
use crate::shared::callout_tooltip::CalloutTooltip;
use crate::shared::image_viewer::ImageViewer;
use crate::shared::file_upload_modal::FileUploadModal;
use link::tsp_link::TspVerificationModal;


Expand Down Expand Up @@ -101,6 +102,13 @@ live_design! {
image_viewer_modal_inner = <ImageViewer> {}
}
}
file_upload_modal = <Modal> {
content: {
height: Fill, width: Fill,
align: {x: 0.5, y: 0.5},
file_upload_modal_inner = <FileUploadModal> {}
}
}

// Context menus should be shown in front of other UI elements,
// but behind verification modals.
Expand Down Expand Up @@ -508,6 +516,17 @@ impl MatchEvent for App {
}
_ => {}
}
match action.downcast_ref() {
Some(FilePreviewerAction::Show(_)) => {
self.ui.modal(ids!(file_upload_modal)).open(cx);
continue;
}
Some(FilePreviewerAction::Hide) => {
self.ui.modal(ids!(file_upload_modal)).close(cx);
continue;
}
_ => {}
}
// Handle actions to open/close the TSP verification modal.
#[cfg(feature = "tsp")] {
use std::ops::Deref;
Expand Down
4 changes: 4 additions & 0 deletions src/home/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub mod new_message_context_menu;
pub mod room_context_menu;
pub mod link_preview;
pub mod room_image_viewer;
pub mod thumbnail_loading;
pub mod upload_progress;

pub fn live_design(cx: &mut Cx) {
search_messages::live_design(cx);
Expand Down Expand Up @@ -59,4 +61,6 @@ pub fn live_design(cx: &mut Cx) {
light_themed_dock::live_design(cx);
event_reaction_list::live_design(cx);
link_preview::live_design(cx);
thumbnail_loading::live_design(cx);
upload_progress::live_design(cx);
}
57 changes: 57 additions & 0 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ impl Widget for RoomScreen {
timeline_kind: tl.kind.clone(),
room_members,
room_avatar_url,
timeline_update_sender: Some(tl.update_sender.clone()),
}
} else if let Some(room_name) = &self.room_name_id {
// Fallback case: we have a room_name but no tl_state yet
Expand All @@ -967,6 +968,7 @@ impl Widget for RoomScreen {
.expect("BUG: room_name_id was set but timeline_kind was missing"),
room_members: None,
room_avatar_url: None,
timeline_update_sender: None,
}
} else {
// No room selected yet, skip event handling that requires room context
Expand All @@ -982,6 +984,7 @@ impl Widget for RoomScreen {
timeline_kind: TimelineKind::MainRoom { room_id },
room_members: None,
room_avatar_url: None,
timeline_update_sender: None,
}
};
let mut room_scope = Scope::with_props(&room_props);
Expand Down Expand Up @@ -1617,6 +1620,32 @@ impl RoomScreen {
tl.tombstone_info = Some(successor_room_details);
}
TimelineUpdate::LinkPreviewFetched => {}
TimelineUpdate::FileUploadConfirmed(file_data) => {
// Handle file upload confirmation from the file upload modal.
// This ensures the upload is associated with the correct timeline.
let room_input_bar = self.view.room_input_bar(ids!(room_input_bar));
if let Some(replied_to) = room_input_bar.handle_file_upload_confirmed(cx) {
submit_async_request(MatrixRequest::SendAttachment {
timeline_kind: tl.kind.clone(),
file_data,
replied_to,
#[cfg(feature = "tsp")]
sign_with_tsp: room_input_bar.is_tsp_signing_enabled(cx),
});
}
}
TimelineUpdate::FileUploadUpdate { current, total } => {
let room_input_bar = self.view.room_input_bar(ids!(room_input_bar));
room_input_bar.set_progress_value(cx, current, total);
}
TimelineUpdate::FileUploadAbortHandle(handle) => {
let room_input_bar = self.view.room_input_bar(ids!(room_input_bar));
room_input_bar.set_abort_handle(handle);
}
TimelineUpdate::FileUploadError { error, file_data } => {
let room_input_bar = self.view.room_input_bar(ids!(room_input_bar));
room_input_bar.handle_upload_error(cx, error, file_data);
}
}
}

Expand Down Expand Up @@ -2274,6 +2303,7 @@ impl RoomScreen {
content_drawn_since_last_update: RangeSet::new(),
profile_drawn_since_last_update: RangeSet::new(),
update_receiver,
update_sender: update_sender.clone(),
request_sender,
media_cache: MediaCache::new(Some(update_sender.clone())),
link_preview_cache: LinkPreviewCache::new(Some(update_sender)),
Expand Down Expand Up @@ -2631,6 +2661,10 @@ pub struct RoomScreenProps {
pub timeline_kind: TimelineKind,
pub room_members: Option<Arc<Vec<RoomMember>>>,
pub room_avatar_url: Option<OwnedMxcUri>,
/// The sender for timeline updates, used for operations like file uploads
/// that need to notify this specific timeline.
/// This is `None` in fallback cases where the timeline state isn't fully loaded yet.
pub timeline_update_sender: Option<crossbeam_channel::Sender<TimelineUpdate>>,
}


Expand Down Expand Up @@ -2759,6 +2793,25 @@ pub enum TimelineUpdate {
Tombstoned(SuccessorRoomDetails),
/// A notice that link preview data for a URL has been fetched and is now available.
LinkPreviewFetched,
/// A file upload was confirmed from the file upload modal.
FileUploadConfirmed(crate::shared::file_upload_modal::FileData),
/// An update on the progress of a file upload that is currently in-flight.
FileUploadUpdate {
/// Current progress value (e.g., bytes uploaded).
current: u64,
/// Total value to reach (e.g., total file size in bytes).
total: u64,
},
/// A file upload failed with an error.
/// Includes the file data so the user can retry the upload.
FileUploadError {
/// The error message describing why the upload failed.
error: String,
/// The file data that can be used to retry the upload.
file_data: crate::shared::file_upload_modal::FileData,
},
/// A notice that a file upload in this timeline was aborted.
FileUploadAbortHandle(tokio::task::AbortHandle),
}

thread_local! {
Expand Down Expand Up @@ -2820,6 +2873,10 @@ struct TimelineUiState {
/// which is okay because a sender on an unbounded channel never needs to block.
update_receiver: crossbeam_channel::Receiver<TimelineUpdate>,

/// The channel sender for timeline updates for this room.
/// This is passed to child widgets that need to send updates to this timeline.
update_sender: crossbeam_channel::Sender<TimelineUpdate>,

/// The sender for timeline requests from a RoomScreen showing this room
/// to the background async task that handles this room's timeline updates.
request_sender: TimelineRequestSender,
Expand Down
40 changes: 40 additions & 0 deletions src/home/thumbnail_loading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! A simple loading view displayed while generating thumbnails for file uploads.

use makepad_widgets::*;

live_design! {
use link::theme::*;
use link::shaders::*;
use link::widgets::*;
use crate::shared::styles::*;

// A view that displays a loading spinner and text while generating thumbnails.
pub ThumbnailLoadingView = <View> {
visible: false,
width: Fill,
height: Fit,
padding: {top: 8, bottom: 8, left: 10, right: 10}
flow: Right,
spacing: 10,
align: {y: 0.5}

loading_spinner = <LoadingSpinner> {
width: 25,
height: 25,
draw_bg: {
color: (COLOR_ACTIVE_PRIMARY)
border_size: 3.0,
}
}

loading_text = <Label> {
width: Fit,
height: Fit,
draw_text: {
text_style: <REGULAR_TEXT>{font_size: 11},
color: #666
}
text: "Generating thumbnail..."
}
}
}
Loading