Skip to content

Comments

Attachment upload with file dialog#674

Open
alanpoon wants to merge 15 commits intoproject-robius:mainfrom
alanpoon:image_upload
Open

Attachment upload with file dialog#674
alanpoon wants to merge 15 commits intoproject-robius:mainfrom
alanpoon:image_upload

Conversation

@alanpoon
Copy link
Contributor

Screen.Recording.2026-01-26.at.4.09.11.PM.mov
  • macOS
  • Android
  • Linux
  • Windows

@alanpoon alanpoon self-assigned this Jan 26, 2026
@kevinaboos
Copy link
Member

awesome!

I know this isn't yet complete, but I wanted to quickly drop in and suggest using rfd instead of makepad's internal file API. rfd is the de facto crate for this kind of thing, and I plan to shortly create a robius crate that wraps rfd, so we'll end up with a similar API.

Plus, with rfd you'd get immediate support for all 3 desktop platforms.

@alanpoon
Copy link
Contributor Author

awesome!

I know this isn't yet complete, but I wanted to quickly drop in and suggest using rfd instead of makepad's internal file API. rfd is the de facto crate for this kind of thing, and I plan to shortly create a robius crate that wraps rfd, so we'll end up with a similar API.

Plus, with rfd you'd get immediate support for all 3 desktop platforms.

Kindly refer to this implementation of the rfd. https://github.com/Vjze/YY_DPS/blob/edb15ffc85b646c27547081b30a0e6f0d8ba688b/src/export/export_view.rs#L158 This requires the tokio runtime in a field in export screen

@kevinaboos
Copy link
Member

awesome!
I know this isn't yet complete, but I wanted to quickly drop in and suggest using rfd instead of makepad's internal file API. rfd is the de facto crate for this kind of thing, and I plan to shortly create a robius crate that wraps rfd, so we'll end up with a similar API.
Plus, with rfd you'd get immediate support for all 3 desktop platforms.

Kindly refer to this implementation of the rfd. https://github.com/Vjze/YY_DPS/blob/edb15ffc85b646c27547081b30a0e6f0d8ba688b/src/export/export_view.rs#L158 This requires the tokio runtime in a field in export screen

Ok, what's the issue with that? Is that problematic? Apologies, but I'm not quite sure what point you're trying to make.

Moly has used rfd in their app, works fine AFAIK.

@alanpoon alanpoon added the waiting-on-review This issue is waiting to be reviewed label Jan 30, 2026
@alanpoon alanpoon marked this pull request as ready for review January 30, 2026 16:18
@alanpoon
Copy link
Contributor Author

awesome!
I know this isn't yet complete, but I wanted to quickly drop in and suggest using rfd instead of makepad's internal file API. rfd is the de facto crate for this kind of thing, and I plan to shortly create a robius crate that wraps rfd, so we'll end up with a similar API.
Plus, with rfd you'd get immediate support for all 3 desktop platforms.

Kindly refer to this implementation of the rfd. https://github.com/Vjze/YY_DPS/blob/edb15ffc85b646c27547081b30a0e6f0d8ba688b/src/export/export_view.rs#L158 This requires the tokio runtime in a field in export screen

Ok, what's the issue with that? Is that problematic? Apologies, but I'm not quite sure what point you're trying to make.

Moly has used rfd in their app, works fine AFAIK.

Thanks for referring me to Moly. File Dialog does not work well in asynchronously in macOS as file dialog is only allowing in main thread.

Comment on lines 1438 to 1473
// Upload the file to the media repository and send the message
let file_size_uint = UInt::try_from(file_meta.file_size).ok();
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: file_size_uint,
blurhash: None,
is_animated: None,
}
)
} else {
matrix_sdk::attachment::AttachmentInfo::File(
matrix_sdk::attachment::BaseFileInfo {
size: file_size_uint,
}
)
});

// Note: Matrix SDK doesn't currently support progress tracking via observable
// The progress_sender is kept for future compatibility
let Some(progress_sender) = progress_sender else {
error!("Progress sender not provided for file upload to room {room_id}");
return;
};

progress_sender.set(TransmissionProgress { total: file_meta.file_size as usize, current: 0 });

let result = room.send_attachment(
&file_meta.filename,
&mime_type,
file_data,
attachment_config,
).with_send_progress_observable(progress_sender.clone()).store_in_cache().await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're doing a lot of stuff manually here, some of which (I think) the Timeline's functions already handle for you.

For example, there is Timeline::send_attachment() and Timeline::send_gallery(), and of course send() and send_reply() which we already use (and I see you have used here as well for replies).

I don't mind if we call the Room sending functions directly, but is there a particular reason to do so? I would expect it to be easier/simpler to use the TImeline-provided functions.

Can you explain your rationale / thought process here? perhaps there is a reason why we can't use the Timeline::send* functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I did not check the timeline api, lost touch.

Copy link
Member

@kevinaboos kevinaboos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Alan, looks very good — impressive work here!

I left comments about a few major structural decisions, but it's mostly just about refactoring things into more modular widgets. I also left some questions about using higher-level Timeline APIs vs Room APIs for sending attachments.

Comment on lines 31 to 33
/// Maximum file size allowed for upload (100 MB).
/// Files larger than this will be rejected to prevent memory issues.
const MAX_FILE_SIZE_BYTES: u64 = 100 * 1024 * 1024;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for artificial limits. The homeserver should be responsible for informing us when a file is too large.

While the main matrix.org homeserver typically uses 100MB limits, other private homeservers likely do not have those limits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed limits


// Handle the file attachment upload button being clicked.
if self.button(ids!(attachment_upload_button)).clicked(actions) {
if !cx.display_context.is_desktop() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"is_desktop()" is confusingly named here, it just checks for the window size, not the actual platform.

Instead, you should check the built-in target_os like we do for all robius crates; that way you can correctly and explicitly exclude ios and android.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to target_os.

@kevinaboos kevinaboos removed the waiting-on-review This issue is waiting to be reviewed label Feb 5, 2026
@kevinaboos kevinaboos assigned kevinaboos and alanpoon and unassigned alanpoon Feb 5, 2026
@kevinaboos kevinaboos added the waiting-on-author This issue is waiting on the original author for a response label Feb 5, 2026
@kevinaboos
Copy link
Member

  • macOS
  • Android
  • Linux
  • Windows

Also, now that you're using rfd, can you confirm whether this also works for Linux and Windows? Theoretically they are also included as supported platforms in rfd.

@alanpoon alanpoon added waiting-on-review This issue is waiting to be reviewed and removed waiting-on-author This issue is waiting on the original author for a response labels Feb 7, 2026
Copy link
Member

@kevinaboos kevinaboos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently added support for threads (and thread-focused timelines), which required adding a new type TimelineKind that identifies a timeline. You'll need to use that in your new MatrixRequest variants in order to distinguish between sending a message to a room's main timeline vs. sending it to a thread timeline within that room.

Once you merge in the latest changes from main and resolve conflicts, you'll see that new type/design being used everywhere.

Another top-level comment is that all of the new widgets you introduced in this PR unfortunately assume that they are app-wide singletons, but that is not true (or will not be true in the future). Be careful when you're using actions to update the state of something — if there are multiple instances of that widget (e.g., the ProgressBar), then they will all handle that action, which is incorrect.
Instead of using actions for these operations, you can use the TimelineUpdate infrastructure to directly send updates to only one timeline, which is more correct in general.

Comment on lines +50 to +63
cancel_upload_button = <Button> {
width: Fit,
height: Fit,
padding: {top: 4, bottom: 4, left: 8, right: 8}
draw_text: {
text_style: <REGULAR_TEXT>{font_size: 9},
color: #fff
}
draw_bg: {
color: #c44
}
text: "Cancel"
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use the existing RobrixIconButton for the sake of stylistic consistency. You can copy the style, icon, and coloring of other cancel buttons across robrix, which are red and have the "forbidden" icon.

Comment on lines +82 to +88
// Poll for the upload task's abort handle (for cancellation support)
if let Some(receiver) = &self.upload_abort_receiver {
if let Ok(handle) = receiver.try_recv() {
self.upload_abort_handle = Some(handle);
self.upload_abort_receiver = None;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we definitely shouldn't be doing this on every event. Either use a UI Signal, or use a dedicated action type, or something similar.

Comment on lines +111 to +115
enqueue_popup_notification(PopupItem {
message: String::from("Upload cancelled"),
kind: PopupKind::Info,
auto_dismissal_duration: Some(3.0)
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we now have a new function API for this, please use it after resolving conflicts

}

for action in actions {
if let Some(ProgressBarAction::Update { current, total }) = action.downcast_ref() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can have multiple progress bars showing simultaneously, so how does this action identify which progress bar should be updated?

Seems like you'll need a unique progress bar ID (or better yet, just use a direct channel instead of an action that is broadcasted to all widgets) in order to avoid progress updates getting mixed up across multiple progress bar widgets.

Comment on lines +204 to +205
#[rust] background_task_id: u32,
#[rust] receiver: Option<(u32, FileLoadReceiver)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are both vaguely named. Can you give them more specific names and also add doc comments to clarify what they represent?

use mime_guess::mime;

/// Maximum dimensions for image thumbnails
const THUMBNAIL_MAX_WIDTH: u32 = 600;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my previous comment said that the standard thumbnail size for matrix is 800x600.

Suggested change
const THUMBNAIL_MAX_WIDTH: u32 = 600;
const THUMBNAIL_MAX_WIDTH: u32 = 800;


final_img.write_to(&mut std::io::Cursor::new(&mut bytes), ImageEncodingFormat::Jpeg)?;
let bytes_size = bytes.len() as u32;
Ok(Some(Thumbnail { data: bytes, content_type: mime::IMAGE_JPEG, height: UInt::from(thumb_height), width: UInt::from(thumb_width), size: UInt::from(bytes_size) }))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting nit

Suggested change
Ok(Some(Thumbnail { data: bytes, content_type: mime::IMAGE_JPEG, height: UInt::from(thumb_height), width: UInt::from(thumb_width), size: UInt::from(bytes_size) }))
Ok(Some(Thumbnail {
data: bytes,
content_type: mime::IMAGE_JPEG,
height: UInt::from(thumb_height),
width: UInt::from(thumb_width),
size: UInt::from(bytes_size),
}))

}

MatrixRequest::SendAttachment {
room_id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. use the new TimelineKind type, along with the new function get_timeline_and_sender() for convenience.
  2. Use the returned sender to send a TimelineUpdate (or multiple TimelineUpdates) back to the proper timeline, which avoids the issues I mentioned previously about widgets getting improperly updated.
  • I think it makes the most sense to use TimelineUpdates for all of this: the progress bar updates, the abort handle, etc. Then, in the RoomScreen where you handle those new TimelineUpdate variants, you can just directly call various RoomInputBar functions to update the widgets that are actually in the progress bar.

Comment on lines +502 to +507
// Set up progress tracking for the upload
// Create a channel to receive the upload task's AbortHandle for cancellation support
let (abort_sender, abort_receiver) = crossbeam_channel::bounded(1);
let upload_progress_view = self.upload_progress_view(ids!(upload_progress_view));
upload_progress_view.set_abort_receiver(abort_receiver);
upload_progress_view.set_visible(cx, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use the existing TimelineUpdate infrastructure for all of this. It's more correct to do it that way, since these things exist on a per-timeline basis and are specific to just one timeline.

Comment on lines +193 to +194
/// Type alias for file data message sent through the channel.
pub type FileData = Arc<(FilePreviewerMetaData, Option<Thumbnail>)>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in accordance with my other comment, you can include a timeline_update_sender here in order to directly send updates to the one timeline that this upload is related to.

@kevinaboos kevinaboos added waiting-on-author This issue is waiting on the original author for a response and removed waiting-on-review This issue is waiting to be reviewed labels Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on-author This issue is waiting on the original author for a response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants