@@ -7,8 +7,7 @@ use bytesize::ByteSize;
77use imbl:: Vector ;
88use makepad_widgets:: { image_cache:: ImageBuffer , * } ;
99use matrix_sdk:: {
10- room:: { reply:: { EnforceThread , Reply } , RoomMember } ,
11- ruma:: {
10+ media:: MediaFormat , room:: { reply:: { EnforceThread , Reply } , RoomMember } , ruma:: {
1211 events:: {
1312 receipt:: Receipt ,
1413 room:: {
@@ -20,19 +19,18 @@ use matrix_sdk::{
2019 sticker:: { StickerEventContent , StickerMediaSource } ,
2120 } ,
2221 matrix_uri:: MatrixId , uint, EventId , MatrixToUri , MatrixUri , OwnedEventId , OwnedMxcUri , OwnedRoomId , UserId
23- } ,
24- OwnedServerName ,
22+ } , OwnedServerName
2523} ;
2624use matrix_sdk_ui:: timeline:: {
2725 self , EmbeddedEvent , EncryptedMessage , EventTimelineItem , InReplyToDetails , MemberProfileChange , MsgLikeContent , MsgLikeKind , PollState , RoomMembershipChange , TimelineDetails , TimelineEventItemId , TimelineItem , TimelineItemContent , TimelineItemKind , VirtualTimelineItem
2826} ;
2927
3028use crate :: {
31- 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_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item} , home:: { edited_indicator:: EditedIndicatorWidgetRefExt , editing_pane:: EditingPaneState , loading_pane:: { LoadingPaneState , LoadingPaneWidgetExt } , rooms_list:: RoomsListRef } , location:: init_location_subscriber, media_cache:: MediaCacheEntry , profile:: {
29+ 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_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item} , home:: { edited_indicator:: EditedIndicatorWidgetRefExt , editing_pane:: EditingPaneState , loading_pane:: { LoadingPaneState , LoadingPaneWidgetExt } , rooms_list:: RoomsListRef } , location:: init_location_subscriber, media_cache:: { MediaCache , MediaCacheEntry } , profile:: {
3230 user_profile:: { AvatarState , ShowUserProfileAction , UserProfile , UserProfileAndRoomId , UserProfilePaneInfo , UserProfileSlidingPaneRef , UserProfileSlidingPaneWidgetExt } ,
3331 user_profile_cache,
3432 } , shared:: {
35- 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:: COLOR_FG_DANGER_RED , text_or_image:: { TextOrImageRef , TextOrImageWidgetRefExt } , timestamp:: TimestampWidgetRefExt , typing_animation:: TypingAnimationWidgetExt
33+ avatar:: AvatarWidgetRefExt , callout_tooltip:: TooltipAction , html_or_plaintext:: { HtmlOrPlaintextRef , HtmlOrPlaintextWidgetRefExt , RobrixHtmlLinkAction } , image_viewer_modal :: { get_global_image_viewer_modal , load_image_data , update_state_views , LoadState , IMAGE_LOAD_TIMEOUT } , jump_to_bottom_button:: { JumpToBottomButtonWidgetExt , UnreadMessageCount } , popup_list:: { enqueue_popup_notification, PopupItem , PopupKind } , restore_status_view:: RestoreStatusViewWidgetExt , styles:: COLOR_FG_DANGER_RED , text_or_image:: { TextOrImageAction , TextOrImageRef , TextOrImageWidgetRefExt } , timestamp:: TimestampWidgetRefExt , typing_animation:: TypingAnimationWidgetExt
3634 } , 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 }
3735} ;
3836use crate :: home:: event_reaction_list:: ReactionListWidgetRefExt ;
@@ -84,8 +82,7 @@ live_design! {
8482 use crate :: home:: room_read_receipt:: * ;
8583 use crate :: rooms_list:: * ;
8684 use crate :: shared:: restore_status_view:: * ;
87- use crate :: shared:: image_viewer_modal:: ImageViewerModal ;
88-
85+
8986 IMG_DEFAULT_AVATAR = dep( "crate://self/resources/img/default_avatar.png" )
9087
9188 ICO_LOCATION_PERSON = dep( "crate://self/resources/icons/location-person.svg" )
@@ -753,10 +750,10 @@ live_design! {
753750 // The user profile sliding pane should be displayed on top of other "static" subviews
754751 // (on top of all other views that are always visible).
755752 user_profile_sliding_pane = <UserProfileSlidingPane > { }
756-
753+
757754 // The loading pane appears while the user is waiting for something in the room screen
758755 // to finish loading, e.g., when loading an older replied-to message.
759- // loading_pane = <LoadingPane> { }
756+ loading_pane = <LoadingPane > { }
760757
761758
762759 /*
@@ -813,6 +810,8 @@ pub struct RoomScreen {
813810 #[ rust] is_loaded : bool ,
814811 /// Whether or not all rooms have been loaded (received from the homeserver).
815812 #[ rust] all_rooms_loaded : bool ,
813+ /// Timer for displaying `timeout` in the image viewer modal.
814+ #[ rust] image_viewer_timeout_timer : Timer
816815}
817816impl Drop for RoomScreen {
818817 fn drop ( & mut self ) {
@@ -980,6 +979,15 @@ impl Widget for RoomScreen {
980979 ) ;
981980 }
982981 }
982+
983+ if let TextOrImageAction :: Clicked ( room_id, mxc_uri) = action. as_widget_action ( ) . cast ( ) {
984+ if let Some ( tl) = & mut self . tl_state {
985+ // Only handle the action if it matches the current room
986+ if tl. room_id == room_id {
987+ populate_image_modal ( cx, & mut self . image_viewer_timeout_timer , Some ( mxc_uri) , tl) ;
988+ }
989+ }
990+ }
983991 }
984992
985993 /*
@@ -1322,6 +1330,7 @@ impl Widget for RoomScreen {
13221330 event_tl_item,
13231331 msg_like_content,
13241332 prev_event,
1333+ & mut tl_state. media_cache ,
13251334 & tl_state. user_power ,
13261335 item_drawn_status,
13271336 room_screen_widget_uid,
@@ -1676,6 +1685,10 @@ impl RoomScreen {
16761685 log ! ( "Timeline::handle_event(): media fetched for room {}" , tl. room_id) ;
16771686 // Here, to be most efficient, we could redraw only the media items in the timeline,
16781687 // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
1688+ if let LoadState :: Loaded = populate_image_modal ( cx, & mut self . image_viewer_timeout_timer , None , tl) {
1689+ let image_viewer_modal = get_global_image_viewer_modal ( cx) ;
1690+ image_viewer_modal. set_image_loaded ( ) ;
1691+ }
16791692 }
16801693 TimelineUpdate :: MessageEdited { timeline_event_id, result } => {
16811694 self . view . editing_pane ( id ! ( editing_pane) )
@@ -2310,6 +2323,7 @@ impl RoomScreen {
23102323 profile_drawn_since_last_update : RangeSet :: new ( ) ,
23112324 update_receiver,
23122325 request_sender,
2326+ media_cache : MediaCache :: new ( Some ( update_sender) ) ,
23132327 replying_to : None ,
23142328 saved_state : SavedState :: default ( ) ,
23152329 message_highlight_animation_state : MessageHighlightAnimationState :: default ( ) ,
@@ -2318,10 +2332,6 @@ impl RoomScreen {
23182332 scrolled_past_read_marker : false ,
23192333 latest_own_user_receipt : None ,
23202334 } ;
2321-
2322- // Add this timeline's update sender to the global MediaCache
2323- crate :: media_cache:: get_media_cache ( ) . lock ( ) . unwrap ( ) . add_timeline_update_sender ( update_sender) ;
2324-
23252335 ( tl_state, true )
23262336 } ;
23272337
@@ -2830,6 +2840,10 @@ struct TimelineUiState {
28302840 /// to the background async task that handles this room's timeline updates.
28312841 request_sender : TimelineRequestSender ,
28322842
2843+ /// The cache of media items (images, videos, etc.) that appear in this timeline.
2844+ ///
2845+ /// Currently this excludes avatars, as those are shared across multiple rooms.
2846+ media_cache : MediaCache ,
28332847
28342848 /// Info about the event currently being replied to, if any.
28352849 replying_to : Option < ( EventTimelineItem , EmbeddedEvent ) > ,
@@ -2993,6 +3007,7 @@ fn populate_message_view(
29933007 event_tl_item : & EventTimelineItem ,
29943008 msg_like_content : & MsgLikeContent ,
29953009 prev_event : Option < & Arc < TimelineItem > > ,
3010+ media_cache : & mut MediaCache ,
29963011 user_power_levels : & UserPowerLevels ,
29973012 item_drawn_status : ItemDrawnStatus ,
29983013 room_screen_widget_uid : WidgetUid ,
@@ -3191,6 +3206,7 @@ fn populate_message_view(
31913206 image_info,
31923207 image. source . clone ( ) ,
31933208 msg. body ( ) ,
3209+ media_cache,
31943210 ) ;
31953211 new_drawn_status. content_drawn = is_image_fully_drawn;
31963212 ( item, false )
@@ -3343,6 +3359,7 @@ fn populate_message_view(
33433359 Some ( Box :: new ( image_info. clone ( ) ) ) ,
33443360 MediaSource :: Plain ( owned_mxc_url. clone ( ) ) ,
33453361 body,
3362+ media_cache,
33463363 ) ;
33473364 new_drawn_status. content_drawn = is_image_fully_drawn;
33483365 ( item, false )
@@ -3510,6 +3527,7 @@ fn populate_image_message_content(
35103527 image_info_source : Option < Box < ImageInfo > > ,
35113528 original_source : MediaSource ,
35123529 body : & str ,
3530+ media_cache : & mut MediaCache ,
35133531) -> bool {
35143532 // We don't use thumbnails, as their resolution is too low to be visually useful.
35153533 // We also don't trust the provided mimetype, as it can be incorrect.
@@ -3534,19 +3552,16 @@ fn populate_image_message_content(
35343552 // A closure that fetches and shows the image from the given `mxc_uri`,
35353553 // marking it as fully drawn if the image was available.
35363554 let mut fetch_and_show_image_uri = |cx : & mut Cx2d , mxc_uri : OwnedMxcUri , image_info : Box < ImageInfo > | {
3537- match crate :: media_cache:: get_media_cache ( ) . lock ( ) . unwrap ( ) . try_get_media_or_fetch ( mxc_uri. clone ( ) , MEDIA_THUMBNAIL_FORMAT . into ( ) ) {
3555+ match media_cache. try_get_media_or_fetch ( mxc_uri. clone ( ) , MEDIA_THUMBNAIL_FORMAT . into ( ) ) {
35383556 ( MediaCacheEntry :: Loaded ( data) , _media_format) => {
3539- let show_image_result = text_or_image_ref. show_image ( cx, mxc_uri. clone ( ) , |cx, img| {
3557+ let show_image_result = text_or_image_ref. show_image ( cx, mxc_uri. clone ( ) , |cx, img| {
35403558 utils:: load_png_or_jpg ( & img, cx, & data)
35413559 . map ( |( ) | img. size_in_pixels ( cx) . unwrap_or_default ( ) )
35423560 } ) ;
35433561 if let Err ( e) = show_image_result {
35443562 let err_str = format ! ( "{body}\n \n Failed to display image: {e:?}" ) ;
35453563 error ! ( "{err_str}" ) ;
35463564 text_or_image_ref. show_text ( cx, & err_str) ;
3547- } else {
3548- // Add click handler for the image
3549- let _mxc_uri_clone = mxc_uri. clone ( ) ;
35503565 }
35513566
35523567 // We're done drawing the image, so mark it as fully drawn.
@@ -4396,4 +4411,56 @@ pub fn clear_timeline_states(_cx: &mut Cx) {
43964411 TIMELINE_STATES . with_borrow_mut ( |states| {
43974412 states. clear ( ) ;
43984413 } ) ;
4399- }
4414+ }
4415+
4416+ /// Populates the image viewer modal with the given timeline item's image.
4417+ ///
4418+ /// This function will return `LoadState::Loading` if the image is not yet
4419+ /// available, `LoadState::Loaded` if the image is successfully loaded and
4420+ /// displayed, or `LoadState::Error` if the image fails to load.
4421+ ///
4422+ /// If the image is not yet available, the timer passed in as `timer` will be
4423+ /// started with a timeout of `IMAGE_LOAD_TIMEOUT` seconds. When the timer
4424+ /// is triggered, this function will be called again with the same arguments.
4425+ ///
4426+ /// The `mxc_uri` argument should be set to `None` if the timeline item does
4427+ /// not have an associated image.
4428+ ///
4429+ /// The `tl` argument should point to the timeline state for the current room.
4430+ fn populate_image_modal ( cx : & mut Cx , timer : & mut Timer , mxc_uri : Option < OwnedMxcUri > , tl : & mut TimelineUiState ) -> LoadState {
4431+ if let Some ( mxc_uri) = mxc_uri {
4432+ * timer = cx. start_timeout ( IMAGE_LOAD_TIMEOUT ) ;
4433+ let image_viewer_modal = get_global_image_viewer_modal ( cx) ;
4434+ image_viewer_modal. initialized ( tl. room_id . clone ( ) , mxc_uri, * timer) ;
4435+ }
4436+ let image_viewer_modal = get_global_image_viewer_modal ( cx) ;
4437+ if image_viewer_modal. get_media_or_fetch ( tl. room_id . clone ( ) ) {
4438+ let Some ( view_set) = image_viewer_modal. get_view_set ( ) else { return LoadState :: Error ; } ;
4439+ let Some ( mxc_uri) = image_viewer_modal. get_mxc_uri ( ) else { return LoadState :: Error ; } ;
4440+ match tl. media_cache . try_get_media_or_fetch ( mxc_uri, MediaFormat :: File ) {
4441+ ( MediaCacheEntry :: Loaded ( data) , MediaFormat :: File ) => {
4442+ let Some ( image_ref) = image_viewer_modal. get_zoomable_image ( ) else { return LoadState :: Error ; } ;
4443+ match load_image_data ( cx, image_ref. clone ( ) , view_set. clone ( ) , & data) {
4444+ Ok ( _) => {
4445+ cx. stop_timer ( * timer) ;
4446+ return LoadState :: Loaded ;
4447+ } ,
4448+ Err ( _) => {
4449+ cx. stop_timer ( * timer) ;
4450+ update_state_views ( cx, view_set, LoadState :: Error ) ;
4451+ return LoadState :: Error ;
4452+ }
4453+ }
4454+ }
4455+ ( MediaCacheEntry :: Requested , _)
4456+ | ( MediaCacheEntry :: Loaded ( _) , MediaFormat :: Thumbnail ( _) ) => {
4457+ update_state_views ( cx, view_set, LoadState :: Loading ) ;
4458+ }
4459+ ( MediaCacheEntry :: Failed , _) => {
4460+ cx. stop_timer ( * timer) ;
4461+ update_state_views ( cx, view_set, LoadState :: Error ) ;
4462+ }
4463+ }
4464+ }
4465+ LoadState :: Loading
4466+ }
0 commit comments