Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
587 changes: 577 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ 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"] }
mime = "0.3"
mime_guess = "2.0"
linkify = "0.10.0"
matrix-sdk-base = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested" }
matrix-sdk = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested", default-features = false, features = [
Expand Down Expand Up @@ -104,6 +107,10 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
"macos-system-configuration",
] }

# Desktop-only file dialog (doesn't work on iOS/Android)
[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies]
rfd = "0.15"


[features]
default = []
Expand Down
3 changes: 3 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.
7 changes: 7 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.
27 changes: 27 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
VerificationModalWidgetRefExt,
}
};
use crate::shared::file_upload_modal::{FileUploadModalWidgetRefExt, FilePreviewerAction};

script_mod! {
use mod.prelude.widgets.*
Expand Down Expand Up @@ -143,6 +144,16 @@ script_mod! {
}
}

// A modal to preview and confirm file uploads.
file_upload_modal := Modal {
content +: {
height: Fill,
width: Fill,
align: Align{x: 0.5, y: 0.5},
file_upload_modal_inner := FileUploadModal {}
}
Comment on lines +149 to +154
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this should follow the design of the EventSourceModal, which uses height: Fill and width: Fill for the content here, and then the actual EventSourceModal widget itself defines these layout properties:

        width: Fill { max: 1000 }
        height: Fill
        margin: 40,
        align: Align{x: 0.5, y: 0}
        padding: Inset{top: 20, right: 25, bottom: 20, left: 25}
        flow: Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

}

PopupList {}

// Tooltips must be shown in front of all other UI elements,
Expand Down Expand Up @@ -284,6 +295,21 @@ impl MatchEvent for App {
// which will open the login_status_modal to show the failure message.
}

// Handle file upload modal actions
match action.downcast_ref() {
Some(FilePreviewerAction::Show { file_data, timeline_update_sender }) => {
self.ui.file_upload_modal(cx, ids!(file_upload_modal_inner))
.set_file_data(cx, file_data.clone(), timeline_update_sender.clone());
self.ui.modal(cx, ids!(file_upload_modal)).open(cx);
continue;
}
Some(FilePreviewerAction::Hide) => {
self.ui.modal(cx, ids!(file_upload_modal)).close(cx);
continue;
}
_ => {}
}

// Handle an action requesting to open the new message context menu.
if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() {
self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx);
Expand Down Expand Up @@ -589,6 +615,7 @@ impl AppMain for App {
crate::home::location_preview::script_mod(vm);
crate::home::tombstone_footer::script_mod(vm);
crate::home::editing_pane::script_mod(vm);
crate::home::upload_progress::script_mod(vm);
crate::room::script_mod(vm);
crate::join_leave_room_modal::script_mod(vm);
crate::verification_modal::script_mod(vm);
Expand Down
2 changes: 2 additions & 0 deletions src/home/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod new_message_context_menu;
pub mod room_context_menu;
pub mod link_preview;
pub mod room_image_viewer;
pub mod upload_progress;

pub fn script_mod(vm: &mut ScriptVm) {
search_messages::script_mod(vm);
Expand Down Expand Up @@ -58,6 +59,7 @@ pub fn script_mod(vm: &mut ScriptVm) {
main_desktop_ui::script_mod(vm);
spaces_bar::script_mod(vm);
navigation_tab_bar::script_mod(vm);
upload_progress::script_mod(vm);
// Keep HomeScreen last, it references many widgets registered above.
home_screen::script_mod(vm);
}
81 changes: 80 additions & 1 deletion src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction;

use rangemap::RangeSet;

use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}};
use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}, upload_progress::UploadProgressViewAction};

/// The maximum number of timeline items to search through
/// when looking for a particular event.
Expand Down Expand Up @@ -860,6 +860,31 @@ impl Widget for RoomScreen {
continue;
}

// Handle cancel action from upload progress view.
// The upload progress view already hides itself and aborts the upload,
// so we just need to acknowledge the action here.
if let UploadProgressViewAction::Cancelled = action.as_widget_action().cast() {
// Nothing additional to do - the widget handles hiding itself
continue;
}

// Handle retry action from upload progress view.
if let UploadProgressViewAction::Retry { file_data, timeline_kind } = action.as_widget_action().cast() {
let Some(tl) = self.tl_state.as_ref() else { continue };
// Only handle if this action is for the current room/thread.
if tl.kind != timeline_kind { continue };
let room_input_bar = self.view.room_input_bar(cx, ids!(room_input_bar));
room_input_bar.show_upload_progress(cx, &file_data.name);
submit_async_request(MatrixRequest::SendAttachment {
timeline_kind,
file_data,
replied_to: None,
#[cfg(feature = "tsp")]
sign_with_tsp: room_input_bar.is_tsp_signing_enabled(cx),
});
continue;
}

// Handle the highlight animation for a message.
let Some(tl) = self.tl_state.as_mut() else { continue };
if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state {
Expand Down Expand Up @@ -984,6 +1009,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 @@ -994,6 +1020,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 @@ -1009,6 +1036,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 @@ -1646,6 +1674,33 @@ impl RoomScreen {
tl.tombstone_info = Some(successor_room_details);
}
TimelineUpdate::LinkPreviewFetched => {}
TimelineUpdate::FileUploadConfirmed(file_data) => {
let room_input_bar = self.view.room_input_bar(cx, ids!(room_input_bar));
let replied_to = room_input_bar.handle_file_upload_confirmed(cx, &file_data.name);
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 } => {
self.view.room_input_bar(cx, ids!(room_input_bar))
.set_upload_progress(cx, current, total);
}
TimelineUpdate::FileUploadAbortHandle(handle) => {
self.view.room_input_bar(cx, ids!(room_input_bar))
.set_upload_abort_handle(handle);
}
TimelineUpdate::FileUploadError { error, file_data } => {
self.view.room_input_bar(cx, ids!(room_input_bar))
.show_upload_error(cx, &error, file_data);
}
TimelineUpdate::FileUploadComplete => {
self.view.room_input_bar(cx, ids!(room_input_bar))
.hide_upload_progress(cx);
}
}
}

Expand Down Expand Up @@ -2298,6 +2353,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 @@ -2665,6 +2721,9 @@ 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 file uploads and other UI-initiated updates.
/// This is `None` when the timeline hasn't been fully loaded yet.
pub timeline_update_sender: Option<crossbeam_channel::Sender<TimelineUpdate>>,
}


Expand Down Expand Up @@ -2794,6 +2853,22 @@ pub enum TimelineUpdate {
Tombstoned(SuccessorRoomDetails),
/// A notice that link preview data for a URL has been fetched and is now available.
LinkPreviewFetched,
/// User confirmed a file upload via the file upload modal.
FileUploadConfirmed(crate::shared::file_upload_modal::FileData),
/// Progress update for an ongoing file upload.
FileUploadUpdate {
current: u64,
total: u64,
},
/// The abort handle for an in-progress file upload.
FileUploadAbortHandle(tokio::task::AbortHandle),
/// An error occurred during file upload.
FileUploadError {
error: String,
file_data: crate::shared::file_upload_modal::FileData,
},
/// File upload completed successfully.
FileUploadComplete,
}

thread_local! {
Expand Down Expand Up @@ -2855,6 +2930,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 used to send upload confirmations and other UI-initiated updates.
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
Loading