Skip to content
86 changes: 83 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ use crate::{
}, join_leave_room_modal::{
JoinLeaveRoomModalAction,
JoinLeaveRoomModalWidgetRefExt,
}, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, shared::callout_tooltip::{
}, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, shared::{callout_tooltip::{
CalloutTooltipOptions,
CalloutTooltipWidgetRefExt,
TooltipAction,
}, sliding_sync::current_user_id, utils::{
}, image_viewer_modal::ImageViewerModalWidgetRefExt}, sliding_sync::current_user_id, utils::{
room_name_or_id,
OwnedRoomIdRon,
}, verification::VerificationAction, verification_modal::{
Expand All @@ -44,7 +44,84 @@ live_design! {
use crate::shared::popup_list::*;
use crate::home::new_message_context_menu::*;
use crate::shared::callout_tooltip::CalloutTooltip;
use link::tsp_link::TspVerificationModal;
use crate::shared::image_viewer_modal::ImageViewerModal;

APP_TAB_COLOR = #344054
APP_TAB_COLOR_HOVER = #636e82
APP_TAB_COLOR_ACTIVE = #091

AppTab = <RadioButton> {
width: Fit,
height: Fill,
flow: Down,
align: {x: 0.5, y: 0.5},

icon_walk: {width: 20, height: 20, margin: 0.0}
label_walk: {margin: 0.0}

draw_bg: {
radio_type: Tab,

// Draws a horizontal line under the tab when selected or hovered.
fn pixel(self) -> vec4 {
let sdf = Sdf2d::viewport(self.pos * self.rect_size);
sdf.box(
20.0,
self.rect_size.y - 2.5,
self.rect_size.x - 40,
self.rect_size.y - 4,
0.5
);
sdf.fill(
mix(
mix(
#0000,
(APP_TAB_COLOR_HOVER),
self.hover
),
(APP_TAB_COLOR_ACTIVE),
self.active
)
);
return sdf.result;
}
}

draw_text: {
color: (APP_TAB_COLOR)
color_hover: (APP_TAB_COLOR_HOVER)
color_active: (APP_TAB_COLOR_ACTIVE)

fn get_color(self) -> vec4 {
return mix(
mix(
self.color,
self.color_hover,
self.hover
),
self.color_active,
self.active
)
}
}

draw_icon: {
instance color: (APP_TAB_COLOR)
instance color_hover: (APP_TAB_COLOR_HOVER)
instance color_active: (APP_TAB_COLOR_ACTIVE)
fn get_color(self) -> vec4 {
return mix(
mix(
self.color,
self.color_hover,
self.hover
),
self.color_active,
self.selected
)
}
}
} use link::tsp_link::TspVerificationModal;


App = {{App}} {
Expand Down Expand Up @@ -128,6 +205,8 @@ live_design! {
// Tooltips must be shown in front of all other UI elements,
// since they can be shown as a hover atop any other widget.
app_tooltip = <CalloutTooltip> {}

image_viewer_modal = <ImageViewerModal> {}
}
} // end of body
}
Expand Down Expand Up @@ -190,6 +269,7 @@ impl LiveHook for App {
// Here we set the global singleton for the PopupList widget,
// which is used to access PopupList Widget from anywhere in the app.
crate::shared::popup_list::set_global_popup_list(cx, &self.ui);
crate::shared::image_viewer_modal::set_global_image_viewer_modal(cx, self.ui.image_viewer_modal(id!(image_viewer_modal)));
}
}

Expand Down
68 changes: 60 additions & 8 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use bytesize::ByteSize;
use imbl::Vector;
use makepad_widgets::{image_cache::ImageBuffer, *};
use matrix_sdk::{
room::{reply::{EnforceThread, Reply}, RoomMember},
ruma::{
media::MediaFormat, room::{reply::{EnforceThread, Reply}, RoomMember}, ruma::{
events::{
receipt::Receipt,
room::{
Expand All @@ -20,8 +19,7 @@ use matrix_sdk::{
sticker::{StickerEventContent, StickerMediaSource},
},
matrix_uri::MatrixId, uint, EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId
},
OwnedServerName,
}, OwnedServerName
};
use matrix_sdk_ui::timeline::{
self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MsgLikeContent, MsgLikeKind, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem
Expand All @@ -32,7 +30,7 @@ use crate::{
user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
user_profile_cache,
}, shared::{
avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt, typing_animation::TypingAnimationWidgetExt
avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer_modal::{get_global_image_viewer_modal, handle_media_cache_entry, initialize_image_modal_with_uri, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt, typing_animation::TypingAnimationWidgetExt
}, sliding_sync::{get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineRequestSender, UserPowerLevels}, utils::{self, room_name_or_id, unix_time_millis_to_datetime, ImageFormat, MEDIA_THUMBNAIL_FORMAT}
};
use crate::home::event_reaction_list::ReactionListWidgetRefExt;
Expand Down Expand Up @@ -813,6 +811,8 @@ pub struct RoomScreen {
#[rust] is_loaded: bool,
/// Whether or not all rooms have been loaded (received from the homeserver).
#[rust] all_rooms_loaded: bool,
/// Timer for displaying `timeout` in the image viewer modal.
#[rust] image_viewer_timeout_timer: Timer
}
impl Drop for RoomScreen {
fn drop(&mut self) {
Expand Down Expand Up @@ -980,6 +980,15 @@ impl Widget for RoomScreen {
);
}
}

if let TextOrImageAction::Clicked(room_id, mxc_uri) = action.as_widget_action().cast() {
if let Some(tl) = &mut self.tl_state {
// Only handle the action if it matches the current room
if tl.room_id == room_id {
populate_image_modal(cx, &mut self.image_viewer_timeout_timer, Some(mxc_uri), tl);
}
}
}
}

/*
Expand Down Expand Up @@ -1694,6 +1703,10 @@ impl RoomScreen {
log!("Timeline::handle_event(): media fetched for room {}", tl.room_id);
// 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.
if let LoadState::Loaded = populate_image_modal(cx, &mut self.image_viewer_timeout_timer, None, tl) {
let image_viewer_modal = get_global_image_viewer_modal(cx);
image_viewer_modal.set_image_loaded();
}
}
TimelineUpdate::MessageEdited { timeline_event_id, result } => {
self.view.editing_pane(id!(editing_pane))
Expand Down Expand Up @@ -3592,7 +3605,7 @@ fn populate_image_message_content(
let mut fetch_and_show_image_uri = |cx: &mut Cx2d, 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, mxc_uri.clone(), |cx, img| {
utils::load_png_or_jpg(&img, cx, &data)
.map(|()| img.size_in_pixels(cx).unwrap_or_default())
});
Expand All @@ -3608,7 +3621,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, mxc_uri.clone(), |cx, img| {
let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) else {
return Err(image_cache::ImageError::EmptyData)
};
Expand Down Expand Up @@ -4449,4 +4462,43 @@ pub fn clear_timeline_states(_cx: &mut Cx) {
TIMELINE_STATES.with_borrow_mut(|states| {
states.clear();
});
}
}

/// Populates the image modal with media content and handles various loading states
///
/// This function manages the complete lifecycle of loading and displaying an image in the modal:
/// 1. Optionally initializes the modal with a new MXC URI
/// 2. Attempts to fetch or retrieve cached media
/// 3. Updates the UI based on the current media state (loading, loaded, failed)
fn populate_image_modal(
cx: &mut Cx,
timer: &mut Timer,
mxc_uri: Option<OwnedMxcUri>,
tl: &mut TimelineUiState
) -> LoadState {
// Initialize modal with new URI if provided
if let Some(mxc_uri) = mxc_uri {
initialize_image_modal_with_uri(cx, timer, mxc_uri, tl.room_id.clone());
}

let image_viewer_modal = get_global_image_viewer_modal(cx);

// Only proceed if media fetching is active
if !image_viewer_modal.get_media_or_fetch(tl.room_id.clone()) {
return LoadState::Loading;
}

// Get required modal components
let Some(view_set) = image_viewer_modal.get_view_set() else {
return LoadState::Error;
};
let Some(mxc_uri) = image_viewer_modal.get_mxc_uri() else {
return LoadState::Error;
};

// Try to get media from cache or trigger fetch
let media_entry = tl.media_cache.try_get_media_or_fetch(mxc_uri, MediaFormat::File);

// Handle the different media states
handle_media_cache_entry(cx, timer, media_entry, view_set)
}
Loading