11//! A room screen is the UI view that displays a single Room's timeline of events/messages
22//! along with a message input bar at the bottom.
33
4- use std:: { borrow:: Cow , cell:: RefCell , collections:: BTreeMap , ops:: { DerefMut , Range } , sync:: Arc } ;
4+ use std:: { borrow:: Cow , cell:: RefCell , collections:: { BTreeMap , HashMap } , ops:: { DerefMut , Range } , sync:: { Arc , LazyLock , RwLock } } ;
55
66use bytesize:: ByteSize ;
77use imbl:: Vector ;
@@ -26,9 +26,10 @@ use matrix_sdk::{
2626use matrix_sdk_ui:: timeline:: {
2727 self , EmbeddedEvent , EncryptedMessage , EventTimelineItem , InReplyToDetails , MemberProfileChange , MsgLikeContent , MsgLikeKind , PollState , RoomMembershipChange , TimelineDetails , TimelineEventItemId , TimelineItem , TimelineItemContent , TimelineItemKind , VirtualTimelineItem
2828} ;
29+ use tokio:: sync:: Notify ;
2930
3031use crate :: {
31- app:: { AppState , AppStateAction , SelectedRoom } , 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 , RoomsListAction } } , location:: init_location_subscriber, media_cache:: { MediaCache , MediaCacheEntry } , profile:: {
32+ app:: { AppStateAction , SelectedRoom } , 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 , RoomsListAction } } , location:: init_location_subscriber, media_cache:: { MediaCache , MediaCacheEntry } , profile:: {
3233 user_profile:: { AvatarState , ShowUserProfileAction , UserProfile , UserProfileAndRoomId , UserProfilePaneInfo , UserProfileSlidingPaneRef , UserProfileSlidingPaneWidgetExt } ,
3334 user_profile_cache,
3435 } , shared:: {
@@ -48,6 +49,10 @@ const GEO_URI_SCHEME: &str = "geo:";
4849
4950const MESSAGE_NOTICE_TEXT_COLOR : Vec3 = Vec3 { x : 0.5 , y : 0.5 , z : 0.5 } ;
5051
52+ /// Global HashMap tracking timeline loaded notifications for rooms
53+ /// Maps room IDs to their respective notify instances that signal when the room timeline is loaded
54+ static ROOM_TIMELINE_LOADED_MAP : LazyLock < RwLock < HashMap < OwnedRoomId , Arc < Notify > > > > = LazyLock :: new ( || RwLock :: new ( HashMap :: new ( ) ) ) ;
55+
5156/// The maximum number of timeline items to search through
5257/// when looking for a particular event.
5358///
@@ -63,6 +68,14 @@ const SMOOTH_SCROLL_TIME: NonZeroU32 = NonZeroU32::new(500).unwrap();
6368/// The max size (width or height) of a blurhash image to decode.
6469const BLURHASH_IMAGE_MAX_SIZE : u32 = 500 ;
6570
71+ /// Gets the Arc<Notify> for signaling when a room's timeline is loaded and drawn, creating it if it doesn't exist
72+ pub fn get_timeline_loaded_notify ( room_id : & OwnedRoomId ) -> Option < Arc < Notify > > {
73+ if let Ok ( mut map) = ROOM_TIMELINE_LOADED_MAP . write ( ) {
74+ Some ( map. entry ( room_id. clone ( ) ) . or_insert_with ( || Arc :: new ( Notify :: new ( ) ) ) . clone ( ) )
75+ } else {
76+ None
77+ }
78+ }
6679
6780live_design ! {
6881 use link:: theme:: * ;
@@ -1359,7 +1372,7 @@ impl Widget for RoomScreen {
13591372 // If return DrawStep::done() inside self.view.draw_walk, turtle will misalign and panic.
13601373 return DrawStep :: done ( ) ;
13611374 }
1362-
1375+
13631376 let room_screen_widget_uid = self . widget_uid ( ) ;
13641377 while let Some ( subview) = self . view . draw_walk ( cx, scope, walk) . step ( ) {
13651378 // Here, we only need to handle drawing the portal list.
@@ -1373,6 +1386,12 @@ impl Widget for RoomScreen {
13731386 } ;
13741387 let room_id = & tl_state. room_id ;
13751388 let tl_items = & tl_state. items ;
1389+ // Notify when the room timeline is loaded and being drawn
1390+ if let Ok ( map) = ROOM_TIMELINE_LOADED_MAP . read ( ) {
1391+ if let Some ( notify) = map. get ( room_id) {
1392+ notify. notify_one ( ) ;
1393+ }
1394+ }
13761395
13771396 // Set the portal list's range based on the number of timeline items.
13781397 let last_item_id = tl_items. len ( ) ;
@@ -1531,6 +1550,7 @@ impl RoomScreen {
15311550 fn process_timeline_updates ( & mut self , cx : & mut Cx , portal_list : & PortalListRef ) {
15321551 let top_space = self . view ( id ! ( top_space) ) ;
15331552 let jump_to_bottom = self . jump_to_bottom_button ( id ! ( jump_to_bottom) ) ;
1553+ let loading_pane = self . loading_pane ( id ! ( loading_pane) ) ;
15341554 let curr_first_id = portal_list. first_id ( ) ;
15351555 let ui = self . widget_uid ( ) ;
15361556 let Some ( tl) = self . tl_state . as_mut ( ) else { return } ;
@@ -1799,6 +1819,61 @@ impl RoomScreen {
17991819 TimelineUpdate :: OwnUserReadReceipt ( receipt) => {
18001820 tl. latest_own_user_receipt = Some ( receipt) ;
18011821 }
1822+ TimelineUpdate :: ScrollToMessage { event_id } => {
1823+ // Search through the timeline to find the message with the given event_id
1824+ let mut num_items_searched = 0 ;
1825+ let target_msg_tl_index = tl. items
1826+ . focus ( )
1827+ . into_iter ( )
1828+ . position ( |item| {
1829+ num_items_searched += 1 ;
1830+ item. as_event ( )
1831+ . and_then ( |e| e. event_id ( ) )
1832+ . is_some_and ( |ev_id| ev_id == event_id)
1833+ } ) ;
1834+ if let Some ( index) = target_msg_tl_index {
1835+ let current_first_index = portal_list. first_id ( ) ;
1836+ let speed = index. saturating_sub ( 1 ) . abs_diff ( current_first_index) as f64 / ( SMOOTH_SCROLL_TIME . get ( ) as f64 * 0.001 ) ;
1837+ portal_list. smooth_scroll_to (
1838+ cx,
1839+ index. saturating_sub ( 1 ) ,
1840+ //index.saturating_sub(1).abs_diff(current_first_index) as f64 / (SMOOTH_SCROLL_TIME as f64 * 0.001),
1841+ speed,
1842+ None ,
1843+ ) ;
1844+ // start highlight animation.
1845+ tl. message_highlight_animation_state = MessageHighlightAnimationState :: Pending {
1846+ item_id : index
1847+ } ;
1848+ } else {
1849+ log ! ( "essage not found - trigger backwards pagination to find it" ) ;
1850+ // Message not found - trigger backwards pagination to find it
1851+ loading_pane. set_state (
1852+ cx,
1853+ LoadingPaneState :: BackwardsPaginateUntilEvent {
1854+ target_event_id : event_id. clone ( ) ,
1855+ events_paginated : 0 ,
1856+ request_sender : tl. request_sender . clone ( ) ,
1857+ } ,
1858+ ) ;
1859+ loading_pane. show ( cx) ;
1860+
1861+ tl. request_sender . send_if_modified ( |requests| {
1862+ if let Some ( existing) = requests. iter_mut ( ) . find ( |r| r. room_id == tl. room_id ) {
1863+ // Re-use existing request
1864+ existing. target_event_id = event_id. clone ( ) ;
1865+ } else {
1866+ requests. push ( BackwardsPaginateUntilEventRequest {
1867+ room_id : tl. room_id . clone ( ) ,
1868+ target_event_id : event_id. clone ( ) ,
1869+ starting_index : 0 , // Search from the beginning since we don't know where it is
1870+ current_tl_len : tl. items . len ( ) ,
1871+ } ) ;
1872+ }
1873+ true
1874+ } ) ;
1875+ }
1876+ }
18021877 }
18031878 }
18041879
@@ -2850,7 +2925,10 @@ pub enum TimelineUpdate {
28502925 UserPowerLevels ( UserPowerLevels ) ,
28512926 /// An update to the currently logged-in user's own read receipt for this room.
28522927 OwnUserReadReceipt ( Receipt ) ,
2853-
2928+ /// Scroll the timeline to the given event.
2929+ ScrollToMessage {
2930+ event_id : OwnedEventId ,
2931+ }
28542932}
28552933
28562934thread_local ! {
@@ -4353,8 +4431,6 @@ pub struct Message {
43534431 /// The jump option required for searched messages.
43544432 /// Contains the room ID, event ID for the message, and whether it's from an all-rooms search.
43554433 #[ rust] jump_option : Option < JumpToMessageRequest > ,
4356- /// Add a small delay to ensure new room tab is opened before jumping to the message.
4357- #[ rust] jump_delay : Timer ,
43584434}
43594435
43604436impl Widget for Message {
@@ -4368,61 +4444,31 @@ impl Widget for Message {
43684444 {
43694445 self . animator_play ( cx, id ! ( highlight. off) ) ;
43704446 }
4371- if let Event :: Timer ( te) = event {
4372- if let ( Some ( _) , Some ( jump_request) ) = ( self . jump_delay . is_timer ( te) , & self . jump_option ) {
4373- cx. widget_action (
4374- self . widget_uid ( ) ,
4375- & scope. path ,
4376- MessageAction :: ScrollToMessage {
4377- room_id : jump_request. room_id . clone ( ) ,
4378- event_id : jump_request. event_id . clone ( ) ,
4379- }
4380- ) ;
4381- }
4382- }
43834447 if let Some ( jump_request) = & self . jump_option {
43844448 if let Event :: Actions ( actions) = event {
43854449 if self . view . button ( id ! ( jump_to_this_message. jump_button) ) . clicked ( actions) {
4386- if let Some ( selected_room) = {
4387- let app_state = scope. data . get :: < AppState > ( ) . unwrap ( ) ;
4388- & app_state. selected_room
4389- } {
4390- // If room_id is not the selected room, select the room and open its dock tab
4391- if selected_room. room_id ( ) != & jump_request. room_id {
4392- let room_name: Option < String > = {
4393- let rooms_list_ref = cx. get_global :: < RoomsListRef > ( ) ;
4394- rooms_list_ref. get_room_name ( & jump_request. room_id )
4395- } ;
4396-
4397- let target_selected_room = SelectedRoom :: JoinedRoom {
4398- room_id : jump_request. room_id . clone ( ) . into ( ) ,
4399- room_name,
4400- } ;
4401-
4402- // Dispatch action to select the room and open its dock tab
4403- cx. widget_action (
4404- self . widget_uid ( ) ,
4405- & scope. path ,
4406- RoomsListAction :: Selected ( target_selected_room)
4407- ) ;
4408- }
4409- }
4410- // Add a jump delay to ensure new room tab is opened before jumping to the message.
4411- self . jump_delay = cx. start_timeout ( 0.5 ) ;
4450+ cx. widget_action (
4451+ self . widget_uid ( ) ,
4452+ & Scope :: default ( ) . path ,
4453+ StackNavigationAction :: PopToRoot
4454+ ) ;
4455+ let room_name: Option < String > = {
4456+ let rooms_list_ref = cx. get_global :: < RoomsListRef > ( ) ;
4457+ rooms_list_ref. get_room_name ( & jump_request. room_id )
4458+ } ;
4459+ let target_selected_room = SelectedRoom :: JoinedRoom {
4460+ room_id : jump_request. room_id . clone ( ) . into ( ) ,
4461+ room_name,
4462+ } ;
4463+ cx. widget_action (
4464+ self . widget_uid ( ) ,
4465+ & scope. path ,
4466+ RoomsListAction :: Selected ( target_selected_room)
4467+ ) ;
4468+ submit_async_request ( MatrixRequest :: WaitForRoomTimelineLoadedToJump { room_id : jump_request. room_id . clone ( ) , event_id : jump_request. event_id . clone ( ) } ) ;
44124469 }
44134470 }
44144471 self . view . handle_event ( cx, event, scope) ;
4415- let message_view_area = self . view . area ( ) ;
4416- match event. hits ( cx, message_view_area) {
4417- Hit :: FingerDown ( fe) => {
4418- cx. set_key_focus ( message_view_area) ;
4419- // A left click to scroll to the message in room screen.
4420- if fe. device . mouse_button ( ) . is_some_and ( |b| b. is_primary ( ) ) {
4421- self . jump_delay = cx. start_timeout ( 0.5 ) ;
4422- }
4423- }
4424- _ => { }
4425- }
44264472 return ;
44274473 }
44284474 let Some ( details) = self . details . clone ( ) else { return } ;
@@ -4554,9 +4600,6 @@ impl Message {
45544600 self . jump_option = Some ( jump_option) ;
45554601 self . view . view ( id ! ( jump_to_this_message) )
45564602 . set_visible ( cx, true ) ;
4557- self . view . apply_over ( cx, live ! {
4558- cursor: Hand
4559- } ) ;
45604603 }
45614604}
45624605
0 commit comments