Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 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
54b1fc1
improvement to image_viewer, remove avatar_placeholder, remove unnecc…
alanpoon Dec 22, 2025
afb70eb
Merge branch 'main' into image_viewer#327
alanpoon Dec 22, 2025
70e1b71
fix clippy::LoadState
alanpoon Dec 22, 2025
336ae03
image_viewer improvement
alanpoon Dec 23, 2025
58ee525
image_viewer_modal_inner
alanpoon Dec 23, 2025
f72f37b
change to image_viewer_modal_inner
alanpoon Dec 23, 2025
b131e0e
fix clippy
alanpoon Dec 23, 2025
1ef175f
fix clippy
alanpoon Dec 23, 2025
12d5b22
update show's comment
alanpoon Dec 23, 2025
62c9672
Added ImageViewerModalActions
alanpoon Dec 23, 2025
5fb5079
fix clippy
alanpoon Dec 23, 2025
b7b8099
removed ImageViewerModalAction
alanpoon Dec 23, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ reqwest = { version = "0.12", default-features = false, features = [
"http2",
"macos-system-configuration",
] }
thiserror = "2.0.16"


[features]
Expand Down
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.
30 changes: 23 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ use crate::{
persistence,
profile::user_profile_cache::clear_user_profile_cache,
room::BasicRoomDetails,
shared::callout_tooltip::{
shared::{callout_tooltip::{
CalloutTooltipWidgetRefExt,
TooltipAction,
},
sliding_sync::current_user_id,
utils::RoomNameId,
verification::VerificationAction,
verification_modal::{VerificationModalAction, VerificationModalWidgetRefExt},
}, image_viewer::{ImageViewerAction, LoadState}}, sliding_sync::current_user_id, utils::RoomNameId, verification::VerificationAction, verification_modal::{
VerificationModalAction,
VerificationModalWidgetRefExt,
}
};

live_design! {
Expand All @@ -43,6 +42,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 @@ -99,6 +99,12 @@ live_design! {
login_screen = <LoginScreen> {}
}

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

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

match action.downcast_ref() {
Some(ImageViewerAction::Show(LoadState::Loading(_, _))) => {
self.ui.modal(ids!(image_viewer_modal)).open(cx);
continue;
}
Some(ImageViewerAction::Hide) => {
self.ui.modal(ids!(image_viewer_modal)).close(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::LinkPreviewFetched);
}
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 @@ -24,6 +24,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
63 changes: 63 additions & 0 deletions src/home/room_image_viewer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use makepad_widgets::*;
use matrix_sdk_ui::timeline::EventTimelineItem;
use matrix_sdk::{
media::MediaFormat,
ruma::events::room::{message::MessageType, MediaSource},
};
use reqwest::StatusCode;

use crate::{media_cache::{MediaCache, MediaCacheEntry}, shared::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.
pub fn populate_matrix_image_modal(
cx: &mut Cx,
media_source: MediaSource,
media_cache: &mut MediaCache,
) {
let MediaSource::Plain(mxc_uri) = media_source else {
return;
};
// 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,
_ => 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));
}
_ => {}
}
}

/// Gets image name and file size in bytes from an event timeline item.
pub fn get_image_name_and_filesize(event_tl_item: &EventTimelineItem) -> (String, u64) {
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(u64::from)
.unwrap_or(0);
return (name, size);
}
}
("Unknown Image".to_string(), 0)
}
85 changes: 67 additions & 18 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,30 @@ use bytesize::ByteSize;
use imbl::Vector;
use makepad_widgets::{image_cache::ImageBuffer, *};
use matrix_sdk::{
room::RoomMember, RoomDisplayName, ruma::{
events::{
OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{
EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{
receipt::Receipt,
room::{
message::{
ImageInfo, MediaSource, message::{
AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent
},
ImageInfo, MediaSource
}
},
sticker::{StickerEventContent, StickerMediaSource},
},
matrix_uri::MatrixId, uint, EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId
}, OwnedServerName
}, matrix_uri::MatrixId, uint
}
};
use matrix_sdk_ui::timeline::{
self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem
};

use crate::{
app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::RoomsListRef, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{
app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::RoomsListRef, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{
user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
user_profile_cache,
},
room::{room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt},
shared::{
avatar::AvatarWidgetRefExt, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt
avatar::AvatarWidgetRefExt, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt
},
sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime}
};
Expand Down Expand Up @@ -206,7 +204,7 @@ live_design! {
height: Fit
flow: Down,
padding: 0.0
<View> {
username_view = <View> {
flow: Right,
width: Fill,
height: Fit,
Expand Down Expand Up @@ -600,7 +598,7 @@ impl Widget for RoomScreen {
// we want to handle those before processing any updates that might change
// the set of timeline indices (which would invalidate the index values in any actions).
if let Event::Actions(actions) = event {
for (_, wr) in portal_list.items_with_actions(actions) {
for (index, wr) in portal_list.items_with_actions(actions) {
let reaction_list = wr.reaction_list(ids!(reaction_list));
if let RoomScreenTooltipActions::HoverInReactionButton {
widget_rect,
Expand Down Expand Up @@ -664,6 +662,17 @@ impl Widget for RoomScreen {
TooltipAction::HoverOut
);
}
let content_message = wr.text_or_image(ids!(content.message));
if let TextOrImageAction::Clicked(mxc_uri) = actions.find_widget_action(content_message.widget_uid()).cast() {
let texture = content_message.get_texture(cx);
self.handle_image_click(
cx,
mxc_uri,
texture,
index,
);
continue;
}
}

self.handle_message_actions(cx, actions, &portal_list, &loading_pane);
Expand Down Expand Up @@ -1355,8 +1364,12 @@ impl RoomScreen {
// Store room members directly in TimelineUiState
tl.room_members = Some(Arc::new(members));
},
TimelineUpdate::MediaFetched => {
TimelineUpdate::MediaFetched(request) => {
log!("process_timeline_updates(): media fetched for room {}", tl.room_id);
// Set Image to image viewer modal if the media is not a thumbnail.
if let (MediaFormat::File, media_source) = (request.format, request.source) {
populate_matrix_image_modal(cx, media_source, &mut tl.media_cache);
}
// Here, to be most efficient, we could redraw only the media items in the timeline,
// but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
}
Expand Down Expand Up @@ -1423,6 +1436,7 @@ impl RoomScreen {
.update_tombstone_footer(cx, &tl.room_id, Some(&successor_room_details));
tl.tombstone_info = Some(successor_room_details);
}
TimelineUpdate::LinkPreviewFetched => {}
}
}

Expand Down Expand Up @@ -1570,6 +1584,39 @@ impl RoomScreen {
}
}

/// Handles image clicks in message content by opening the image viewer.
fn handle_image_click(
&mut self,
cx: &mut Cx,
mxc_uri: Option<MediaSource>,
texture: Option<Texture>,
item_id: usize,
) {
let Some(media_source) = mxc_uri else {
return;
};
let Some(tl_state) = self.tl_state.as_mut() else { return };
let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) else { return };

let timestamp_millis = event_tl_item.timestamp();
let (image_name, image_file_size) = get_image_name_and_filesize(event_tl_item);
cx.action(ImageViewerAction::Show(LoadState::Loading(
texture.clone(),
Some(ImageViewerMetaData {
image_name,
image_file_size,
timestamp: unix_time_millis_to_datetime(timestamp_millis),
avatar_parameter: Some((
tl_state.room_id.clone(),
event_tl_item.clone(),
)),
}),
)));

populate_matrix_image_modal(cx, media_source, &mut tl_state.media_cache);
}


/// Handles any [`MessageAction`]s received by this RoomScreen.
fn handle_message_actions(
&mut self,
Expand Down Expand Up @@ -2435,9 +2482,9 @@ pub enum TimelineUpdate {
RoomMembersListFetched {
members: Vec<RoomMember>,
},
/// A notice that one or more requested media items (images, videos, etc.)
/// A notice with an option of Media Request Parameters that one or more requested media items (images, videos, etc.)
/// that should be displayed in this timeline have now been fetched and are available.
MediaFetched,
MediaFetched(MediaRequestParameters),
/// A notice that one or more members of a this room are currently typing.
TypingUsers {
/// The list of users (their displayable name) who are currently typing in this room.
Expand All @@ -2458,6 +2505,8 @@ pub enum TimelineUpdate {
/// A notice that the given room has been tombstoned (closed)
/// and replaced by the given successor room.
Tombstoned(SuccessorRoomDetails),
/// A notice that link preview data for a URL has been fetched and is now available.
LinkPreviewFetched,
}

thread_local! {
Expand Down Expand Up @@ -3307,7 +3356,7 @@ fn populate_image_message_content(
let mut fetch_and_show_image_uri = |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box<ImageInfo>| {
match media_cache.try_get_media_or_fetch(mxc_uri.clone(), MEDIA_THUMBNAIL_FORMAT.into()) {
(MediaCacheEntry::Loaded(data), _media_format) => {
let show_image_result = text_or_image_ref.show_image(cx, |cx, img| {
let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)),|cx, img| {
utils::load_png_or_jpg(&img, cx, &data)
.map(|()| img.size_in_pixels(cx).unwrap_or_default())
});
Expand All @@ -3323,7 +3372,7 @@ fn populate_image_message_content(
(MediaCacheEntry::Requested, _media_format) => {
// If the image is being fetched, we try to show its blurhash.
if let (Some(ref blurhash), Some(width), Some(height)) = (image_info.blurhash.clone(), image_info.width, image_info.height) {
let show_image_result = text_or_image_ref.show_image(cx, |cx, img| {
let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)), |cx, img| {
let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) else {
return Err(image_cache::ImageError::EmptyData)
};
Expand Down Expand Up @@ -3367,7 +3416,7 @@ fn populate_image_message_content(
}
fully_drawn = false;
}
(MediaCacheEntry::Failed, _media_format) => {
(MediaCacheEntry::Failed(_status_code), _media_format) => {
if text_or_image_ref.view(ids!(default_image_view)).visible() {
fully_drawn = true;
return;
Expand Down
Loading