Skip to content
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e51b874
move MEDIA_CACHE to global
alanpoon Jul 10, 2025
f323303
Merge branch 'main' into image_viewer#327
alanpoon Jul 15, 2025
e1d2aaf
some testing
alanpoon Jul 16, 2025
e4fe2c8
Merge branch 'main' into image_viewer#327
alanpoon Aug 6, 2025
440bde3
changed default image
alanpoon Aug 8, 2025
d029bbc
Merge branch 'main' into image_viewer#327
alanpoon Aug 27, 2025
ddd04b9
Image Viewer with zoom and pann
alanpoon Sep 1, 2025
d641657
fix spelling
alanpoon Sep 1, 2025
9fe9ddc
simplify code for populate_image_modal
alanpoon Sep 2, 2025
27ebbd7
Deallocate image buffer
alanpoon Sep 5, 2025
92d78b3
Merge branch 'main' into image_viewer#327
alanpoon Sep 11, 2025
de263cc
remove default_image
alanpoon Sep 11, 2025
43137a1
Merge branch 'main' into image_viewer#327
alanpoon Sep 15, 2025
03e4120
Merge branch 'main' into image_viewer#327
alanpoon Oct 18, 2025
c9f2484
code improvement image_viewer
alanpoon Oct 19, 2025
81cf00a
Merge branch 'main' into image_viewer#327
alanpoon Oct 19, 2025
ec774a0
update makepad
alanpoon Oct 19, 2025
d85a312
fix
alanpoon Oct 20, 2025
09b9772
image_viewer_modal improvement
alanpoon Oct 21, 2025
8269f0a
remove global singleton for image_viewer_modal
alanpoon Oct 21, 2025
6ea6026
Merge branch 'main' into image_viewer#327
alanpoon Oct 21, 2025
4e4f34d
Friendly Error improvement
alanpoon Oct 21, 2025
ee6b8e7
change to more friendly error message
alanpoon Oct 21, 2025
0e10331
ImageViewer improvement
alanpoon Oct 22, 2025
4febe14
Minor code improvement
alanpoon Oct 22, 2025
4efdf4b
format improvement
alanpoon Oct 22, 2025
85cfb9b
update makepad version
alanpoon Oct 22, 2025
5188571
update mouse cursor
alanpoon Oct 22, 2025
c3cfc21
timeout error update
alanpoon Oct 22, 2025
932c318
ImageViewerModalAction
alanpoon Oct 23, 2025
9bd6dbb
Merge branch 'image_viewer#327' of https://github.com/alanpoon/robrix…
alanpoon Oct 23, 2025
048c3d3
Change to rotated_image
alanpoon Oct 30, 2025
816fae5
Merge branch 'main' into image_viewer#327
alanpoon Oct 30, 2025
f0cecc1
Added Image detail for Image Viewer
alanpoon Oct 31, 2025
2ca47a3
move image_buffer to background thread
alanpoon Oct 31, 2025
082de67
Fix image viewer size for thumbnail image
alanpoon Nov 3, 2025
bdf8cfc
Merge branch 'main' into image_viewer#327
alanpoon Nov 3, 2025
68179c5
added room_image_viewer_footer
alanpoon Nov 4, 2025
e55568c
fix clippy
alanpoon Nov 4, 2025
30ca041
Fix remove_cache_entry
alanpoon Nov 7, 2025
1a35f45
removed ImageViewer opening LinkPreview's image
alanpoon Nov 7, 2025
964b798
combine into room_image_viewer.rs
alanpoon Nov 7, 2025
1136030
revert LinkPreview
alanpoon Nov 7, 2025
897952c
minor code improvement
alanpoon Nov 7, 2025
571c888
Merge branch 'main' into image_viewer#327
alanpoon Nov 7, 2025
b43a96e
remove get_texture_and_size
alanpoon Nov 12, 2025
b4f81ab
Merge branch 'main' into image_viewer#327
alanpoon Nov 12, 2025
dd6555b
fix clippy
alanpoon Nov 12, 2025
9226992
fix grammar issues
alanpoon Nov 12, 2025
784a08b
Merge branch 'main' into image_viewer#327
alanpoon Nov 13, 2025
89d01b6
revert import statement
alanpoon Nov 13, 2025
348b6c5
code improvment
alanpoon Nov 13, 2025
8ae8291
doc improvement
alanpoon Nov 15, 2025
75a7ce1
improve doc in find_previous_profile_in_condensed_message
alanpoon Nov 15, 2025
932036c
avatar_ref
alanpoon Nov 17, 2025
b24a3d1
change to using avatar_placeholder
alanpoon Nov 17, 2025
1c66b16
doc fixed
alanpoon Nov 17, 2025
baf936a
media fetch param improvement
alanpoon Nov 17, 2025
69a2a7f
Update src/shared/image_viewer.rs
alanpoon Nov 17, 2025
e4f53c0
added thiserror
alanpoon Nov 17, 2025
5648d1e
added this error
alanpoon Nov 17, 2025
378760a
Merge branch 'image_viewer#327' of https://github.com/alanpoon/robrix…
alanpoon Nov 17, 2025
0c1fb66
removed rotation_animation duration
alanpoon Nov 24, 2025
555e883
empty_right_container_fixed.
alanpoon Nov 24, 2025
7e4f899
Merge branch 'main' into image_viewer#327
alanpoon Nov 24, 2025
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
6 changes: 6 additions & 0 deletions resources/icons/rotate-anti-clockwise.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions resources/icons/rotate-clockwise.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 39 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ use crate::{
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::{
}, 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::{
CalloutTooltipOptions,
CalloutTooltipWidgetRefExt,
TooltipAction,
}, sliding_sync::current_user_id, utils::{
}, image_viewer::{ImageViewerAction, ImageViewerWidgetRefExt, LoadState}}, sliding_sync::current_user_id, utils::{
room_name_or_id,
OwnedRoomIdRon,
}, verification::VerificationAction, verification_modal::{
Expand All @@ -40,6 +40,7 @@ live_design! {
use crate::shared::popup_list::*;
use crate::home::new_message_context_menu::*;
use crate::shared::callout_tooltip::CalloutTooltip;
use crate::shared::image_viewer::ImageViewer;
use link::tsp_link::TspVerificationModal;


Expand Down Expand Up @@ -96,6 +97,12 @@ live_design! {
login_screen = <LoginScreen> {}
}

image_viewer_modal = <Modal> {
content: {
width: Fill, height: Fill,
image_viewer_inner = <ImageViewer> {}
}
}
<PopupList> {}

// Context menus should be shown in front of other UI elements,
Expand Down Expand Up @@ -405,7 +412,36 @@ impl MatchEvent for App {
self.ui.modal(ids!(verification_modal)).close(cx);
continue;
}

let mut image_viewer_inner = self.ui.image_viewer(ids!(image_viewer_inner));
match action.downcast_ref() {
Some(ImageViewerAction::Show(LoadState::Loading(texture, metadata))) => {
self.ui.modal(ids!(image_viewer_modal)).open(cx);
image_viewer_inner.show_loading(cx, texture.as_ref().clone(), metadata);
continue;
}
Some(ImageViewerAction::Show(LoadState::Loaded(image_bytes))) => {
image_viewer_inner.show_loaded(cx, image_bytes);
continue;
}
Some(ImageViewerAction::Show(LoadState::FinishedBackgroundDecoding)) => {
image_viewer_inner.hide_loading(cx);
continue;
}
Some(ImageViewerAction::Show(LoadState::Error(error))) => {
// This action is emitted when syncing is offline even when the image viewer modal is not opened.
// Hence, we need to check if the modal is open before showing the error.
if self.ui.modal(ids!(image_viewer_modal)).is_open() {
image_viewer_inner.show_error(cx, error);
continue;
}
}
Some(ImageViewerAction::Hide) => {
self.ui.modal(ids!(image_viewer_modal)).close(cx);
image_viewer_inner.reset(cx);
continue;
}
_ => {}
}
// Handle actions to open/close the TSP verification modal.
#[cfg(feature = "tsp")] {
use std::ops::Deref;
Expand Down
2 changes: 1 addition & 1 deletion src/home/link_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ fn insert_into_cache(

if let Some(sender) = update_sender {
// Reuse TimelineUpdate MediaFetched to trigger redraw in the timeline.
let _ = sender.send(TimelineUpdate::MediaFetched);
let _ = sender.send(TimelineUpdate::MediaFetched(None));
}
SignalToUI::set_ui_signal();
}
Expand Down
1 change: 1 addition & 0 deletions src/home/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod welcome_screen;
pub mod event_reaction_list;
pub mod new_message_context_menu;
pub mod link_preview;
pub mod room_image_viewer;

pub fn live_design(cx: &mut Cx) {
search_messages::live_design(cx);
Expand Down
92 changes: 92 additions & 0 deletions src/home/room_image_viewer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use makepad_widgets::*;
use matrix_sdk_ui::timeline::EventTimelineItem;
use matrix_sdk::{
media::MediaFormat, ruma::{
events::room::message::MessageType,
OwnedMxcUri
}
};
use reqwest::StatusCode;

use crate::{media_cache::{MediaCache, MediaCacheEntry}, shared::{avatar::{AvatarRef, AvatarWidgetRefExt}, image_viewer::{ImageViewerAction, ImageViewerError, LoadState}}};

/// Populates the image viewer modal with the given media content.
///
/// If the media is already cached, it will be immediately displayed.
/// If the media is not cached, it will be fetched from the server.
/// If the media fetch fails, an error message will be displayed.
///
/// This function requires passing in a reference to `Cx`, which isn't used, but acts as a guarantee that this function must only be called by the main UI thread.
pub fn populate_matrix_image_modal(
cx: &mut Cx,
mxc_uri: OwnedMxcUri,
media_cache: &mut MediaCache,
) {
// Try to get media from cache or trigger fetch
let media_entry = media_cache.try_get_media_or_fetch(mxc_uri.clone(), MediaFormat::File);

// Handle the different media states
match media_entry {
(MediaCacheEntry::Loaded(data), MediaFormat::File) => {
cx.action(ImageViewerAction::Show(LoadState::Loaded(data)));
}
(MediaCacheEntry::Failed(status_code), MediaFormat::File) => {
let error = match status_code {
StatusCode::NOT_FOUND => ImageViewerError::NotFound,
StatusCode::INTERNAL_SERVER_ERROR => ImageViewerError::ConnectionFailed,
StatusCode::PARTIAL_CONTENT => ImageViewerError::BadData,
StatusCode::UNAUTHORIZED => ImageViewerError::Unauthorized,
StatusCode::REQUEST_TIMEOUT => ImageViewerError::Timeout,
_ => ImageViewerError::Unknown,
};
cx.action(ImageViewerAction::Show(LoadState::Error(error)));
// Remove failed media entry from cache for MediaFormat::File so as to start all over again from loading Thumbnail.
media_cache.remove_cache_entry(&mxc_uri, Some(MediaFormat::File));
}
_ => {}
}
}

/// Extracts image name and size from an event timeline item.
pub fn extract_image_info(event_tl_item: &EventTimelineItem) -> (String, i32) {
Copy link
Member

Choose a reason for hiding this comment

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

  1. "Extract" makes it sound like you're unarchiving/decompressing a file. I would just call it something like get_image_name_and_filesize or something similar.
  2. The image "size" sounds like a dimension, i'd recommend saying "size in bytes" or something just to be very clear.
  3. How can an image size value be a signed integer? Answer: it cannot, and in the SDK it's a UInt, so you can either use that directly or convert it to a u64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

if let Some(message) = event_tl_item.content().as_message() {
if let MessageType::Image(image_content) = message.msgtype() {
let name = message.body().to_string();
let size = image_content
.info
.as_ref()
.and_then(|info| info.size)
.map(|s| i32::try_from(s).unwrap_or_default())
.unwrap_or(0);
(name, size)
} else {
("Unknown Image".to_string(), 0)
}
} else {
("Unknown Image".to_string(), 0)
}
Copy link
Member

Choose a reason for hiding this comment

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

dedup these, e.g., by returning early from within the if conditional and then having this as a default return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

}

/// Condensed message does not have a profile, so we need to find the previous portal list item.
/// Searches backwards for a non-empty display name and avatar in previous portal list items.
/// Mutates display_name and avatar_ref.
/// Returns when first non-empty display name is found.
pub fn find_previous_profile_in_condensed_message(
Copy link
Member

Choose a reason for hiding this comment

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

  1. Reading widget values from the UI is an odd way to try to obtain content for a timeline event. I'm not sure what context you have when calling this, but why not just get this info from the event timeline item itself?
  2. Using this style of output parameters is strange and not very rusty. I'd recommend you just return the display_name and the AvatarRef directly instead of mutating them, because that would clearly convey when you were unable to obtain them. Currently, with this function signature, there's no way to know if you successfully obtained those two bits of data, which is a form of silencing errors.
  3. Nit: doc comments are rendered using markdown, so the line separation here gets lost. Generally, you should start the doc comment with a short one-line summary, then a blank line, and then the details in any format you wish.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

1-2: Removed this function, and use avatar.set_avatar_and_get_username after retrieving timeline event.
3: Doc fixed.

portal_list: &PortalListRef,
mut current_index: usize,
display_name: &mut String,
avatar_ref: &mut AvatarRef,
) {
// Start from the current index and work backwards
while current_index > 0 {
current_index -= 1;
if let Some((_id, item_ref)) = portal_list.get_item(current_index) {
let username = item_ref.label(ids!(content.username_view.username)).text();
if !username.is_empty() {
*display_name = username;
*avatar_ref = item_ref.avatar(ids!(profile.avatar));
return;
}
}
}
}
Loading