@@ -7,8 +7,7 @@ use bytesize::ByteSize;
7
7
use imbl:: Vector ;
8
8
use makepad_widgets:: { image_cache:: ImageBuffer , * } ;
9
9
use matrix_sdk:: {
10
- room:: { reply:: { EnforceThread , Reply } , RoomMember } ,
11
- ruma:: {
10
+ media:: MediaFormat , room:: { reply:: { EnforceThread , Reply } , RoomMember } , ruma:: {
12
11
events:: {
13
12
receipt:: Receipt ,
14
13
room:: {
@@ -20,19 +19,18 @@ use matrix_sdk::{
20
19
sticker:: { StickerEventContent , StickerMediaSource } ,
21
20
} ,
22
21
matrix_uri:: MatrixId , uint, EventId , MatrixToUri , MatrixUri , OwnedEventId , OwnedMxcUri , OwnedRoomId , UserId
23
- } ,
24
- OwnedServerName ,
22
+ } , OwnedServerName
25
23
} ;
26
24
use matrix_sdk_ui:: timeline:: {
27
25
self , EmbeddedEvent , EncryptedMessage , EventTimelineItem , InReplyToDetails , MemberProfileChange , MsgLikeContent , MsgLikeKind , PollState , RoomMembershipChange , TimelineDetails , TimelineEventItemId , TimelineItem , TimelineItemContent , TimelineItemKind , VirtualTimelineItem
28
26
} ;
29
27
30
28
use 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:: {
32
30
user_profile:: { AvatarState , ShowUserProfileAction , UserProfile , UserProfileAndRoomId , UserProfilePaneInfo , UserProfileSlidingPaneRef , UserProfileSlidingPaneWidgetExt } ,
33
31
user_profile_cache,
34
32
} , 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
36
34
} , 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 }
37
35
} ;
38
36
use crate :: home:: event_reaction_list:: ReactionListWidgetRefExt ;
@@ -84,8 +82,7 @@ live_design! {
84
82
use crate :: home:: room_read_receipt:: * ;
85
83
use crate :: rooms_list:: * ;
86
84
use crate :: shared:: restore_status_view:: * ;
87
- use crate :: shared:: image_viewer_modal:: ImageViewerModal ;
88
-
85
+
89
86
IMG_DEFAULT_AVATAR = dep( "crate://self/resources/img/default_avatar.png" )
90
87
91
88
ICO_LOCATION_PERSON = dep( "crate://self/resources/icons/location-person.svg" )
@@ -753,10 +750,10 @@ live_design! {
753
750
// The user profile sliding pane should be displayed on top of other "static" subviews
754
751
// (on top of all other views that are always visible).
755
752
user_profile_sliding_pane = <UserProfileSlidingPane > { }
756
-
753
+
757
754
// The loading pane appears while the user is waiting for something in the room screen
758
755
// to finish loading, e.g., when loading an older replied-to message.
759
- // loading_pane = <LoadingPane> { }
756
+ loading_pane = <LoadingPane > { }
760
757
761
758
762
759
/*
@@ -813,6 +810,8 @@ pub struct RoomScreen {
813
810
#[ rust] is_loaded : bool ,
814
811
/// Whether or not all rooms have been loaded (received from the homeserver).
815
812
#[ rust] all_rooms_loaded : bool ,
813
+ /// Timer for displaying `timeout` in the image viewer modal.
814
+ #[ rust] image_viewer_timeout_timer : Timer
816
815
}
817
816
impl Drop for RoomScreen {
818
817
fn drop ( & mut self ) {
@@ -980,6 +979,15 @@ impl Widget for RoomScreen {
980
979
) ;
981
980
}
982
981
}
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
+ }
983
991
}
984
992
985
993
/*
@@ -1322,6 +1330,7 @@ impl Widget for RoomScreen {
1322
1330
event_tl_item,
1323
1331
msg_like_content,
1324
1332
prev_event,
1333
+ & mut tl_state. media_cache ,
1325
1334
& tl_state. user_power ,
1326
1335
item_drawn_status,
1327
1336
room_screen_widget_uid,
@@ -1676,6 +1685,10 @@ impl RoomScreen {
1676
1685
log ! ( "Timeline::handle_event(): media fetched for room {}" , tl. room_id) ;
1677
1686
// Here, to be most efficient, we could redraw only the media items in the timeline,
1678
1687
// 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
+ }
1679
1692
}
1680
1693
TimelineUpdate :: MessageEdited { timeline_event_id, result } => {
1681
1694
self . view . editing_pane ( id ! ( editing_pane) )
@@ -2310,6 +2323,7 @@ impl RoomScreen {
2310
2323
profile_drawn_since_last_update : RangeSet :: new ( ) ,
2311
2324
update_receiver,
2312
2325
request_sender,
2326
+ media_cache : MediaCache :: new ( Some ( update_sender) ) ,
2313
2327
replying_to : None ,
2314
2328
saved_state : SavedState :: default ( ) ,
2315
2329
message_highlight_animation_state : MessageHighlightAnimationState :: default ( ) ,
@@ -2318,10 +2332,6 @@ impl RoomScreen {
2318
2332
scrolled_past_read_marker : false ,
2319
2333
latest_own_user_receipt : None ,
2320
2334
} ;
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
-
2325
2335
( tl_state, true )
2326
2336
} ;
2327
2337
@@ -2830,6 +2840,10 @@ struct TimelineUiState {
2830
2840
/// to the background async task that handles this room's timeline updates.
2831
2841
request_sender : TimelineRequestSender ,
2832
2842
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 ,
2833
2847
2834
2848
/// Info about the event currently being replied to, if any.
2835
2849
replying_to : Option < ( EventTimelineItem , EmbeddedEvent ) > ,
@@ -2993,6 +3007,7 @@ fn populate_message_view(
2993
3007
event_tl_item : & EventTimelineItem ,
2994
3008
msg_like_content : & MsgLikeContent ,
2995
3009
prev_event : Option < & Arc < TimelineItem > > ,
3010
+ media_cache : & mut MediaCache ,
2996
3011
user_power_levels : & UserPowerLevels ,
2997
3012
item_drawn_status : ItemDrawnStatus ,
2998
3013
room_screen_widget_uid : WidgetUid ,
@@ -3191,6 +3206,7 @@ fn populate_message_view(
3191
3206
image_info,
3192
3207
image. source . clone ( ) ,
3193
3208
msg. body ( ) ,
3209
+ media_cache,
3194
3210
) ;
3195
3211
new_drawn_status. content_drawn = is_image_fully_drawn;
3196
3212
( item, false )
@@ -3343,6 +3359,7 @@ fn populate_message_view(
3343
3359
Some ( Box :: new ( image_info. clone ( ) ) ) ,
3344
3360
MediaSource :: Plain ( owned_mxc_url. clone ( ) ) ,
3345
3361
body,
3362
+ media_cache,
3346
3363
) ;
3347
3364
new_drawn_status. content_drawn = is_image_fully_drawn;
3348
3365
( item, false )
@@ -3510,6 +3527,7 @@ fn populate_image_message_content(
3510
3527
image_info_source : Option < Box < ImageInfo > > ,
3511
3528
original_source : MediaSource ,
3512
3529
body : & str ,
3530
+ media_cache : & mut MediaCache ,
3513
3531
) -> bool {
3514
3532
// We don't use thumbnails, as their resolution is too low to be visually useful.
3515
3533
// We also don't trust the provided mimetype, as it can be incorrect.
@@ -3534,19 +3552,16 @@ fn populate_image_message_content(
3534
3552
// A closure that fetches and shows the image from the given `mxc_uri`,
3535
3553
// marking it as fully drawn if the image was available.
3536
3554
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 ( ) ) {
3538
3556
( 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| {
3540
3558
utils:: load_png_or_jpg ( & img, cx, & data)
3541
3559
. map ( |( ) | img. size_in_pixels ( cx) . unwrap_or_default ( ) )
3542
3560
} ) ;
3543
3561
if let Err ( e) = show_image_result {
3544
3562
let err_str = format ! ( "{body}\n \n Failed to display image: {e:?}" ) ;
3545
3563
error ! ( "{err_str}" ) ;
3546
3564
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 ( ) ;
3550
3565
}
3551
3566
3552
3567
// We're done drawing the image, so mark it as fully drawn.
@@ -4396,4 +4411,56 @@ pub fn clear_timeline_states(_cx: &mut Cx) {
4396
4411
TIMELINE_STATES . with_borrow_mut ( |states| {
4397
4412
states. clear ( ) ;
4398
4413
} ) ;
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