Skip to content

Commit 8ae6afb

Browse files
committed
Added image detail overlay for image viewer
1 parent 816fae5 commit 8ae6afb

File tree

4 files changed

+272
-14
lines changed

4 files changed

+272
-14
lines changed

src/app.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use makepad_widgets::{image_cache::ImageError, makepad_micro_serde::*, *};
1010
use matrix_sdk::ruma::{OwnedRoomId, RoomId};
1111
use crate::{
1212
avatar_cache::clear_avatar_cache, home::{
13-
main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}
13+
main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_image_message_detail::RoomImageMessageDetailWidgetRefExt, room_screen::{MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}
1414
}, join_leave_room_modal::{
1515
JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt
1616
}, 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::{
@@ -41,6 +41,7 @@ live_design! {
4141
use crate::shared::callout_tooltip::CalloutTooltip;
4242
use crate::shared::image_viewer::ImageViewer;
4343
use crate::shared::icon_button::RobrixIconButton;
44+
use crate::home::room_image_message_detail::RoomImageMessageDetail;
4445
use link::tsp_link::TspVerificationModal;
4546

4647

@@ -114,17 +115,8 @@ live_design! {
114115
debug: true
115116
padding: {bottom: 0}
116117
}
117-
image_detail = <View> {
118-
width: 500, height: 200,
119-
debug: false
120-
image_viewer_status_label = <Label> {
121-
width: Fit, height: 30,
122-
text: "Loading----- image...",
123-
draw_text: {
124-
text_style: <REGULAR_TEXT>{font_size: 14},
125-
color: (COLOR_PRIMARY)
126-
}
127-
}
118+
image_detail = <RoomImageMessageDetail> {
119+
width: Fill, height: Fill,
128120
}
129121
}
130122

@@ -465,6 +457,7 @@ impl MatchEvent for App {
465457
&LoadState::Loading(thumbnail_data) => {
466458
self.ui.view(id!(image_viewer_loading_spinner_view)).set_visible(cx, true);
467459
self.ui.label(id!(image_viewer_status_label)).set_text(cx, "Loading...");
460+
self.ui.view(id!(image_viewer_forbidden_view)).set_visible(cx, false);
468461
let _ = self.ui.image_viewer(id!(image_viewer_inner)).display_rotated_image(cx, &thumbnail_data);
469462
}
470463
&LoadState::Loaded(image_bytes) => {
@@ -497,6 +490,7 @@ impl MatchEvent for App {
497490
}
498491
Some(ImageViewerAction::Hide) => {
499492
self.ui.modal(id!(image_viewer)).close(cx);
493+
self.ui.room_image_message_detail(id!(image_detail)).reset_state(cx);
500494
continue;
501495
}
502496
_ => {}

src/home/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod welcome_screen;
2121
pub mod event_reaction_list;
2222
pub mod new_message_context_menu;
2323
pub mod link_preview;
24+
pub mod room_image_message_detail;
2425

2526
pub fn live_design(cx: &mut Cx) {
2627
home_screen::live_design(cx);
@@ -44,4 +45,5 @@ pub fn live_design(cx: &mut Cx) {
4445
light_themed_dock::live_design(cx);
4546
event_reaction_list::live_design(cx);
4647
link_preview::live_design(cx);
48+
room_image_message_detail::live_design(cx);
4749
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! A room image message detail widget that displays a user's avatar, username, and message date.
2+
3+
use makepad_widgets::*;
4+
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId};
5+
use matrix_sdk_ui::timeline::{Profile, TimelineDetails};
6+
7+
use crate::{
8+
shared::{
9+
avatar::AvatarWidgetExt,
10+
timestamp::TimestampWidgetExt,
11+
},
12+
utils::unix_time_millis_to_datetime,
13+
};
14+
use matrix_sdk::ruma::OwnedEventId;
15+
16+
live_design! {
17+
use link::theme::*;
18+
use link::shaders::*;
19+
use link::widgets::*;
20+
21+
use crate::shared::styles::*;
22+
use crate::shared::avatar::Avatar;
23+
use crate::shared::timestamp::Timestamp;
24+
25+
pub RoomImageMessageDetail = {{RoomImageMessageDetail}} {
26+
width: Fill, height: Fill
27+
flow: Right
28+
29+
top_left_container = <View> {
30+
width: 150, height: Fit,
31+
flow: Right,
32+
spacing: 10,
33+
margin: {left: 20, top: 20}
34+
align: { y: 0.5}
35+
36+
avatar = <Avatar> {
37+
width: 40,
38+
height: 40,
39+
}
40+
41+
content = <View> {
42+
width: Fill, height: Fit,
43+
flow: Down,
44+
spacing: 4,
45+
align: { x: 0.0 }
46+
47+
username = <Label> {
48+
width: Fill, height: Fit,
49+
draw_text: {
50+
text_style: <REGULAR_TEXT>{font_size: 14},
51+
color: (COLOR_TEXT)
52+
}
53+
text: ""
54+
}
55+
timestamp_view = <View> {
56+
width: Fill, height: Fit
57+
timestamp = <Timestamp> {
58+
width: Fill, height: Fit,
59+
margin: { left: 5}
60+
}
61+
}
62+
63+
}
64+
}
65+
image_name_and_size = <Label> {
66+
width: Fill, height: Fit,
67+
margin: {top: 40}
68+
align: { x: 0.5, }
69+
draw_text: {
70+
text_style: <REGULAR_TEXT>{font_size: 14},
71+
color: (COLOR_TEXT),
72+
wrap: Word
73+
}
74+
}
75+
empty_right_container = <View> {
76+
// equal width as the top-left container to keep the image name centered.
77+
width: 150, height: Fit,
78+
}
79+
}
80+
}
81+
82+
#[derive(Live, LiveHook, Widget)]
83+
pub struct RoomImageMessageDetail {
84+
#[deref] view: View,
85+
#[rust] sender: Option<OwnedUserId>,
86+
#[rust] sender_profile: Option<TimelineDetails<Profile>>,
87+
#[rust] room_id: Option<OwnedRoomId>,
88+
#[rust] event_id: Option<OwnedEventId>,
89+
#[rust] avatar_drawn: bool,
90+
}
91+
92+
/// Convert bytes to human-readable file size format
93+
fn format_file_size(bytes: i32) -> String {
94+
if bytes < 0 {
95+
return "Unknown size".to_string();
96+
}
97+
98+
let bytes = bytes as u64;
99+
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
100+
101+
if bytes == 0 {
102+
return "0 B".to_string();
103+
}
104+
105+
let mut size = bytes as f64;
106+
let mut unit_index = 0;
107+
108+
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
109+
size /= 1024.0;
110+
unit_index += 1;
111+
}
112+
113+
if unit_index == 0 {
114+
format!("{} {}", bytes, UNITS[unit_index])
115+
} else {
116+
format!("{:.1} {}", size, UNITS[unit_index])
117+
}
118+
}
119+
120+
impl Widget for RoomImageMessageDetail {
121+
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
122+
self.view.handle_event(cx, event, scope);
123+
self.match_event(cx, event);
124+
}
125+
126+
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
127+
if !self.avatar_drawn {
128+
let avatar_ref = self.avatar(id!(top_left_container.avatar));
129+
let Some(room_id) = &self.room_id else { return DrawStep::done() };
130+
let Some(sender) = &self.sender else { return DrawStep::done() };
131+
let (username, avatar_drawn) = avatar_ref.set_avatar_and_get_username(cx, room_id, sender, self.sender_profile.as_ref(), self.event_id.as_deref());
132+
self.label(id!(top_left_container.username)).set_text(cx, &username);
133+
self.avatar_drawn = avatar_drawn;
134+
}
135+
self.view.draw_walk(cx, scope, walk)
136+
}
137+
}
138+
139+
impl MatchEvent for RoomImageMessageDetail {
140+
fn handle_action(&mut self, cx: &mut Cx, action:&Action) {
141+
match action.as_widget_action().cast() {
142+
RoomImageMessageDetailAction::SetImageDetail {
143+
room_id,
144+
sender,
145+
sender_profile,
146+
event_id,
147+
timestamp_millis,
148+
image_name,
149+
image_size
150+
} => {
151+
self.room_id = room_id.clone();
152+
self.sender = sender.clone();
153+
self.sender_profile = sender_profile.clone();
154+
self.event_id = event_id.clone();
155+
self.avatar_drawn = false;
156+
// Format and display image name and size
157+
let human_readable_size = format_file_size(image_size);
158+
let display_text = format!("{} ({})", image_name, human_readable_size);
159+
self.label(id!(image_name_and_size)).set_text(cx, &display_text);
160+
if let Some(dt) = unix_time_millis_to_datetime(timestamp_millis) {
161+
self.view(id!(timestamp_view)).set_visible(cx, true);
162+
self.timestamp(id!(timestamp)).set_date_time(cx, dt);
163+
}
164+
}
165+
_ => {}
166+
}
167+
}
168+
}
169+
170+
impl RoomImageMessageDetail {
171+
/// Reset the widget state to its default values
172+
pub fn reset_state(&mut self, cx: &mut Cx) {
173+
self.sender = None;
174+
self.sender_profile = None;
175+
self.room_id = None;
176+
self.event_id = None;
177+
self.avatar_drawn = false;
178+
179+
// Clear the UI elements
180+
self.label(id!(top_left_container.username)).set_text(cx, "");
181+
self.label(id!(image_name_and_size)).set_text(cx, "");
182+
self.view(id!(timestamp_view)).set_visible(cx, false);
183+
}
184+
}
185+
186+
impl RoomImageMessageDetailRef {
187+
/// See [`RoomImageMessageDetail::reset_state()`]
188+
pub fn reset_state(&self, cx: &mut Cx) {
189+
if let Some(mut inner) = self.borrow_mut() {
190+
inner.reset_state(cx);
191+
}
192+
}
193+
}
194+
195+
/// Actions handled by the `RoomImageMessageDetail`
196+
#[derive(Debug, Clone, DefaultNone)]
197+
pub enum RoomImageMessageDetailAction {
198+
/// Set the image detail onto image viewer modal.
199+
SetImageDetail {
200+
/// Room ID
201+
room_id: Option<OwnedRoomId>,
202+
/// User ID for the sender of the image
203+
sender: Option<OwnedUserId>,
204+
/// Profile details for the sender
205+
sender_profile: Option<TimelineDetails<Profile>>,
206+
/// Event ID
207+
event_id: Option<OwnedEventId>,
208+
/// Timestamp of the message
209+
timestamp_millis: MilliSecondsSinceUnixEpoch,
210+
/// Image name
211+
image_name: String,
212+
/// Image size in bytes.
213+
image_size: i32
214+
},
215+
None,
216+
}

src/home/room_screen.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction;
4444

4545
use rangemap::RangeSet;
4646

47-
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}};
47+
use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_image_message_detail::RoomImageMessageDetailAction, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}};
4848

4949
/// The maximum number of timeline items to search through
5050
/// when looking for a particular event.
@@ -603,7 +603,7 @@ impl Widget for RoomScreen {
603603
// we want to handle those before processing any updates that might change
604604
// the set of timeline indices (which would invalidate the index values in any actions).
605605
if let Event::Actions(actions) = event {
606-
for (_, wr) in portal_list.items_with_actions(actions) {
606+
for (index, wr) in portal_list.items_with_actions(actions) {
607607
let reaction_list = wr.reaction_list(id!(reaction_list));
608608
if let RoomScreenTooltipActions::HoverInReactionButton {
609609
widget_rect,
@@ -662,6 +662,52 @@ impl Widget for RoomScreen {
662662
TooltipAction::HoverOut
663663
);
664664
}
665+
let image_widget_uid = wr.image(id!(content.message)).widget_uid();
666+
match actions.find_widget_action(image_widget_uid).cast() {
667+
TextOrImageAction::Clicked(_mxc_uri) => {
668+
if let Some(tl_state) = &self.tl_state {
669+
if let Some(item) = tl_state.items.get(index) {
670+
if let Some(event_tl_item) = item.as_event() {
671+
let sender_profile = event_tl_item.sender_profile();
672+
let sender = event_tl_item.sender();
673+
let event_id = event_tl_item.event_id().map(|id| id.to_owned());
674+
let timestamp = event_tl_item.timestamp();
675+
676+
// Extract image name and size from the message content
677+
let (image_name, image_size) = if let Some(message) = event_tl_item.content().as_message() {
678+
if let MessageType::Image(image_content) = message.msgtype() {
679+
let name = message.body().to_string();
680+
let size = image_content.info.as_ref()
681+
.and_then(|info| info.size)
682+
.map(|s| i32::try_from(s).unwrap_or_default())
683+
.unwrap_or(0);
684+
(name, size)
685+
} else {
686+
("Unknown Image".to_string(), 0)
687+
}
688+
} else {
689+
("Unknown Image".to_string(), 0)
690+
};
691+
692+
cx.widget_action(
693+
room_screen_widget_uid,
694+
&scope.path,
695+
RoomImageMessageDetailAction::SetImageDetail {
696+
room_id: self.room_id.clone(),
697+
sender: Some(sender.to_owned()),
698+
sender_profile: Some(sender_profile.clone()),
699+
event_id,
700+
timestamp_millis: timestamp,
701+
image_name,
702+
image_size
703+
}
704+
);
705+
}
706+
}
707+
}
708+
}
709+
_ => {}
710+
}
665711
}
666712

667713
self.handle_message_actions(cx, actions, &portal_list, &loading_pane);

0 commit comments

Comments
 (0)