tauri_plugin_matrix_svelte/matrix/room/
room_screen.rs

1use std::sync::Arc;
2
3use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId};
4use matrix_sdk_ui::{
5    eyeball_im::Vector,
6    timeline::{EventTimelineItem, TimelineItem},
7};
8use rangemap::RangeSet;
9use serde::Serialize;
10use tauri::{AppHandle, Runtime};
11use tauri_plugin_svelte::{ManagerExt, StoreState};
12
13use crate::matrix::{
14    requests::{submit_async_request, MatrixRequest},
15    timeline::{
16        take_timeline_endpoints, PaginationDirection, TimelineUiState, TimelineUpdate,
17        TIMELINE_STATES,
18    },
19    user_power_level::UserPowerLevels,
20    utils::room_name_or_id,
21};
22
23/// The main widget that displays a single Matrix room.
24#[derive(Debug, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct RoomScreen {
27    /// The room ID of the currently-shown room.
28    room_id: OwnedRoomId,
29    /// The display name of the currently-shown room.
30    room_name: String,
31    /// The persistent UI-relevant states for the room that this widget is currently displaying.
32    tl_state: Option<TimelineUiState>,
33}
34impl Drop for RoomScreen {
35    fn drop(&mut self) {
36        // This ensures that the `TimelineUiState` instance owned by this room is *always* returned
37        // back to to `TIMELINE_STATES`, which ensures that its UI state(s) are not lost
38        // and that other RoomScreen instances can show this room in the future.
39        // RoomScreen will be dropped whenever its widget instance is destroyed, e.g.,
40        // when a Tab is closed or the app is resized to a different AdaptiveView layout.
41        self.hide_timeline();
42    }
43}
44
45impl RoomScreen {
46    pub fn new(room_id: OwnedRoomId, room_name: String) -> Self {
47        Self {
48            room_id,
49            room_name,
50            tl_state: None,
51        }
52    }
53
54    /// Processes all pending background updates to the currently-shown timeline.
55    pub fn process_timeline_updates<R: Runtime>(&mut self, app_handle: &AppHandle<R>) {
56        // let top_space = self.view(id!(top_space));
57        // let jump_to_bottom = self.jump_to_bottom_button(id!(jump_to_bottom));
58        // let curr_first_id = portal_list.first_id();
59        let curr_first_id: usize = 0; // TODO: replace this dummy value
60
61        // let ui = self.widget_uid();
62        let Some(tl) = self.tl_state.as_mut() else {
63            return;
64        };
65
66        let mut done_loading = false;
67        let mut should_continue_backwards_pagination = false;
68        let mut num_updates = 0;
69        let mut typing_users = Vec::new();
70        while let Ok(update) = tl.update_receiver.try_recv() {
71            num_updates += 1;
72            match update {
73                TimelineUpdate::FirstUpdate { initial_items } => {
74                    tl.content_drawn_since_last_update.clear();
75                    tl.profile_drawn_since_last_update.clear();
76                    tl.fully_paginated = false;
77                    // Set the portal list to the very bottom of the timeline.
78                    // portal_list.set_first_id_and_scroll(initial_items.len().saturating_sub(1), 0.0);
79                    // portal_list.set_tail_range(true);
80                    // jump_to_bottom.update_visibility(cx, true);
81
82                    // update
83                    tl.items = initial_items;
84                    done_loading = true;
85                }
86                TimelineUpdate::NewItems {
87                    new_items,
88                    changed_indices,
89                    is_append,
90                    clear_cache,
91                } => {
92                    if new_items.is_empty() {
93                        if !tl.items.is_empty() {
94                            println!("Timeline::handle_event(): timeline (had {} items) was cleared for room {}", tl.items.len(), tl.room_id);
95                            // For now, we paginate a cleared timeline in order to be able to show something at least.
96                            // A proper solution would be what's described below, which would be to save a few event IDs
97                            // and then either focus on them (if we're not close to the end of the timeline)
98                            // or paginate backwards until we find them (only if we are close the end of the timeline).
99                            should_continue_backwards_pagination = true;
100                        }
101
102                        // If the bottom of the timeline (the last event) is visible, then we should
103                        // set the timeline to live mode.
104                        // If the bottom of the timeline is *not* visible, then we should
105                        // set the timeline to Focused mode.
106
107                        // TODO: Save the event IDs of the top 3 items before we apply this update,
108                        //       which indicates this timeline is in the process of being restored,
109                        //       such that we can jump back to that position later after applying this update.
110
111                        // TODO: here we need to re-build the timeline via TimelineBuilder
112                        //       and set the TimelineFocus to one of the above-saved event IDs.
113
114                        // TODO: the docs for `TimelineBuilder::with_focus()` claim that the timeline's focus mode
115                        //       can be changed after creation, but I do not see any methods to actually do that.
116                        //       <https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk_ui/timeline/struct.TimelineBuilder.html#method.with_focus>
117                        //
118                        //       As such, we probably need to create a new async request enum variant
119                        //       that tells the background async task to build a new timeline
120                        //       (either in live mode or focused mode around one or more events)
121                        //       and then replaces the existing timeline in ALL_ROOMS_INFO with the new one.
122                    }
123
124                    // Maybe todo?: we can often avoid the following loops that iterate over the `items` list
125                    //       by only doing that if `clear_cache` is true, or if `changed_indices` range includes
126                    //       any index that comes before (is less than) the above `curr_first_id`.
127
128                    if new_items.len() == tl.items.len() {
129                        // println!("Timeline::handle_event(): no jump necessary for updated timeline of same length: {}", items.len());
130                    } else if curr_first_id > new_items.len() {
131                        println!("Timeline::handle_event(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", curr_first_id, new_items.len());
132                        // portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0);
133                        // portal_list.set_tail_range(true);
134                        // jump_to_bottom.update_visibility(cx, true);
135                    } else if let Some((curr_item_idx, new_item_idx, new_item_scroll, _event_id)) =
136                        find_new_item_matching_current_item(
137                            0,
138                            Some(0.0), // TODO replace
139                            curr_first_id,
140                            &tl.items,
141                            &new_items,
142                        )
143                    {
144                        if curr_item_idx != new_item_idx {
145                            println!("Timeline::handle_event(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}");
146                            // portal_list.set_first_id_and_scroll(new_item_idx, new_item_scroll);
147                            tl.prev_first_index = Some(new_item_idx);
148                            // Set scrolled_past_read_marker false when we jump to a new event
149                            tl.scrolled_past_read_marker = false;
150                            // When the tooltip is up, the timeline may jump. This may take away the hover out event to required to clear the tooltip
151                            // cx.widget_action(
152                            //     ui,
153                            //     &Scope::empty().path,
154                            //     RoomScreenTooltipActions::HoverOut,
155                            // );
156                            // notify frontend ?
157                        }
158                    }
159                    //
160                    // TODO: after an (un)ignore user event, all timelines are cleared. Handle that here.
161                    //
162                    else {
163                        // eprintln!("!!! Couldn't find new event with matching ID for ANY event currently visible in the portal list");
164                    }
165
166                    // If new items were appended to the end of the timeline, show an unread messages badge on the jump to bottom button.
167                    // if is_append && !portal_list.is_at_end() {
168                    //     if let Some(room_id) = &self.room_id {
169                    //         // Immediately show the unread badge with no count while we fetch the actual count in the background.
170                    //         jump_to_bottom
171                    //             .show_unread_message_badge(cx, UnreadMessageCount::Unknown);
172                    //         submit_async_request(MatrixRequest::GetNumberUnreadMessages {
173                    //             room_id: room_id.clone(),
174                    //         });
175                    //     }
176                    // }
177
178                    if clear_cache {
179                        tl.content_drawn_since_last_update.clear();
180                        tl.profile_drawn_since_last_update.clear();
181                        tl.fully_paginated = false;
182
183                        // If this RoomScreen is showing the loading pane and has an ongoing backwards pagination request,
184                        // then we should update the status message in that loading pane
185                        // and then continue paginating backwards until we find the target event.
186                        // Note that we do this here because `clear_cache` will always be true if backwards pagination occurred.
187                        // let loading_pane = self.view.loading_pane(id!(loading_pane));
188                        // let mut loading_pane_state = loading_pane.take_state();
189                        // if let LoadingPaneState::BackwardsPaginateUntilEvent {
190                        //     ref mut events_paginated,
191                        //     target_event_id,
192                        //     ..
193                        // } = &mut loading_pane_state
194                        // {
195                        //     *events_paginated += new_items.len().saturating_sub(tl.items.len());
196                        //     println!("While finding target event {target_event_id}, loaded {events_paginated} messages...");
197                        //     // Here, we assume that we have not yet found the target event,
198                        //     // so we need to continue paginating backwards.
199                        //     // If the target event has already been found, it will be handled
200                        //     // in the `TargetEventFound` match arm below, which will set
201                        //     // `should_continue_backwards_pagination` to `false`.
202                        //     // So either way, it's okay to set this to `true` here.
203                        //     should_continue_backwards_pagination = true;
204                        // }
205                        // loading_pane.set_state(cx, loading_pane_state);
206                    } else {
207                        tl.content_drawn_since_last_update
208                            .remove(changed_indices.clone());
209                        tl.profile_drawn_since_last_update
210                            .remove(changed_indices.clone());
211                        // println!("Timeline::handle_event(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update);
212                    }
213                    tl.items = new_items;
214                    done_loading = true;
215                }
216                TimelineUpdate::NewUnreadMessagesCount(_unread_messages_count) => {
217                    // jump_to_bottom.show_unread_message_badge(unread_messages_count);
218                }
219                TimelineUpdate::TargetEventFound {
220                    target_event_id,
221                    index,
222                } => {
223                    // println!("Target event found in room {}: {target_event_id}, index: {index}", tl.room_id);
224                    tl.request_sender.send_if_modified(|requests| {
225                        requests.retain(|r| r.room_id != tl.room_id);
226                        // no need to notify/wake-up all receivers for a completed request
227                        false
228                    });
229
230                    // sanity check: ensure the target event is in the timeline at the given `index`.
231                    let item = tl.items.get(index);
232                    let is_valid = item.is_some_and(|item| {
233                        item.as_event()
234                            .is_some_and(|ev| ev.event_id() == Some(&target_event_id))
235                    });
236                    // let loading_pane = self.view.loading_pane(id!(loading_pane));
237
238                    // println!("TargetEventFound: is_valid? {is_valid}. room {}, event {target_event_id}, index {index} of {}\n  --> item: {item:?}", tl.room_id, tl.items.len());
239                    if is_valid {
240                        // We successfully found the target event, so we can close the loading pane,
241                        // reset the loading panestate to `None`, and stop issuing backwards pagination requests.
242                        // loading_pane.set_status(cx, "Successfully found replied-to message!");
243                        // loading_pane.set_state(cx, LoadingPaneState::None);
244
245                        // NOTE: this code was copied from the `MessageAction::JumpToRelated` handler;
246                        //       we should deduplicate them at some point.
247                        // let speed = 50.0;
248                        // Scroll to the message right above the replied-to message.
249                        // FIXME: `smooth_scroll_to` should accept a scroll offset parameter too,
250                        //       so that we can scroll to the replied-to message and have it
251                        //       appear beneath the top of the viewport.
252                        // portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None);
253                        // // start highlight animation.
254                        // tl.message_highlight_animation_state =
255                        //     MessageHighlightAnimationState::Pending { item_id: index };
256                    } else {
257                        // Here, the target event was not found in the current timeline,
258                        // or we found it previously but it is no longer in the timeline (or has moved),
259                        // which means we encountered an error and are unable to jump to the target event.
260                        eprintln!(
261                            "Target event index {index} of {} is out of bounds for room {}",
262                            tl.items.len(),
263                            tl.room_id
264                        );
265                        // Show this error in the loading pane, which should already be open.
266                        // loading_pane.set_state(LoadingPaneState::Error(String::from(
267                        //     "Unable to find related message; it may have been deleted.",
268                        // )));
269                    }
270
271                    should_continue_backwards_pagination = false;
272
273                    // redraw now before any other items get added to the timeline list.
274                    // self.view.redraw(cx);
275                }
276                TimelineUpdate::PaginationRunning(direction) => {
277                    if direction == PaginationDirection::Backwards {
278                        // top_space.set_visible(cx, true);
279                        done_loading = false;
280                    } else {
281                        eprintln!("Unexpected PaginationRunning update in the Forwards direction");
282                    }
283                }
284                TimelineUpdate::PaginationError { error, direction } => {
285                    eprintln!(
286                        "Pagination error ({direction}) in room {}: {error:?}",
287                        tl.room_id
288                    );
289                    done_loading = true;
290                }
291                TimelineUpdate::PaginationIdle {
292                    fully_paginated,
293                    direction,
294                } => {
295                    if direction == PaginationDirection::Backwards {
296                        // Don't set `done_loading` to `true`` here, because we want to keep the top space visible
297                        // (with the "loading" message) until the corresponding `NewItems` update is received.
298                        tl.fully_paginated = fully_paginated;
299                        if fully_paginated {
300                            done_loading = true;
301                        }
302                    } else {
303                        eprintln!("Unexpected PaginationIdle update in the Forwards direction");
304                    }
305                }
306                TimelineUpdate::EventDetailsFetched { event_id, result } => {
307                    if let Err(_e) = result {
308                        eprintln!("Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", tl.room_id);
309                    }
310                    // Here, to be most efficient, we could redraw only the updated event,
311                    // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
312                }
313                TimelineUpdate::RoomMembersSynced => {
314                    // println!("Timeline::handle_event(): room members fetched for room {}", tl.room_id);
315                    // Here, to be most efficient, we could redraw only the user avatars and names in the timeline,
316                    // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
317                }
318                TimelineUpdate::RoomMembersListFetched { members } => {
319                    // Use `pub/sub` pattern here to let multiple components share room members data
320                    // use crate::room::room_member_manager::room_members;
321                    // room_members::update(tl.room_id.clone(), members);
322                }
323                TimelineUpdate::MediaFetched => {
324                    println!(
325                        "Timeline::handle_event(): media fetched for room {}",
326                        tl.room_id
327                    );
328                    // Here, to be most efficient, we could redraw only the media items in the timeline,
329                    // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
330                }
331                TimelineUpdate::MessageEdited {
332                    timeline_event_id,
333                    result,
334                } => {
335                    // self.view
336                    //     .editing_pane(id!(editing_pane))
337                    //     .handle_edit_result(cx, timeline_event_id, result);
338                }
339                TimelineUpdate::TypingUsers { users } => {
340                    // This update loop should be kept tight & fast, so all we do here is
341                    // save the list of typing users for future use after the loop exits.
342                    // Then, we "process" it later (by turning it into a string) after the
343                    // update loop has completed, which avoids unnecessary expensive work
344                    // if the list of typing users gets updated many times in a row.
345                    typing_users = users;
346                }
347
348                TimelineUpdate::UserPowerLevels(user_power_level) => {
349                    tl.user_power = user_power_level;
350
351                    // Update the visibility of the message input bar based on the new power levels.
352                    let _can_send_message = user_power_level.can_send_message();
353                    // self.view
354                    //     .view(id!(input_bar))
355                    //     .set_visible(cx, can_send_message);
356                    // self.view
357                    //     .view(id!(can_not_send_message_notice))
358                    //     .set_visible(cx, !can_send_message);
359                }
360
361                TimelineUpdate::OwnUserReadReceipt(receipt) => {
362                    tl.latest_own_user_receipt = Some(receipt);
363                }
364            }
365        }
366
367        if should_continue_backwards_pagination {
368            submit_async_request(MatrixRequest::PaginateRoomTimeline {
369                room_id: tl.room_id.clone(),
370                num_events: 50,
371                direction: PaginationDirection::Backwards,
372            });
373        }
374
375        if done_loading {
376            // top_space.set_visible(cx, false);
377        }
378
379        if !typing_users.is_empty() {
380            let _typing_notice_text = match typing_users.as_slice() {
381                [] => String::new(),
382                [user] => format!("{user} is typing "),
383                [user1, user2] => format!("{user1} and {user2} are typing "),
384                [user1, user2, others @ ..] => {
385                    if others.len() > 1 {
386                        format!("{user1}, {user2}, and {} are typing ", &others[0])
387                    } else {
388                        format!("{user1}, {user2}, and {} others are typing ", others.len())
389                    }
390                }
391            };
392            // Set the typing notice text and make its view visible.
393            // self.view
394            //     .label(id!(typing_label))
395            //     .set_text(cx, &typing_notice_text);
396            // self.view.view(id!(typing_notice)).set_visible(cx, true);
397            // // Animate in the typing notice view (sliding it up from the bottom).
398            // self.animator_play(cx, id!(typing_notice_animator.show));
399            // // Start the typing notice text animation of bouncing dots.
400            // self.view
401            //     .typing_animation(id!(typing_animation))
402            //     .start_animation(cx);
403        } else {
404            // Animate out the typing notice view (sliding it out towards the bottom).
405            // self.animator_play(cx, id!(typing_notice_animator.hide));
406            // self.view
407            //     .typing_animation(id!(typing_animation))
408            //     .stop_animation(cx);
409        }
410
411        if num_updates > 0 {
412            // println!("Applied {} timeline updates for room {}, redrawing with {} items...", num_updates, tl.room_id, tl.items.len());
413            self.patch_frontend_store_with_current_state(&app_handle);
414        }
415    }
416
417    fn patch_frontend_store_with_current_state<R: Runtime>(&self, app_handle: &AppHandle<R>) {
418        let mut state = StoreState::new();
419        let json_room_id = serde_json::to_value(&self.room_id).expect("Couldn't serialize room_id");
420        state.set("roomId", json_room_id);
421        let json_room_name =
422            serde_json::to_value(&self.room_name).expect("Couldn't serialize room_id");
423        state.set("roomName", json_room_name);
424        let json_tl_state =
425            serde_json::to_value(&self.tl_state).expect("Couldn't serialize room_id");
426        state.set("tlState", json_tl_state);
427
428        app_handle
429            .svelte()
430            .patch(&self.room_id, state)
431            .expect("Couldn't patch the frontend state");
432    }
433
434    /// Handles any [`MessageAction`]s received by this RoomScreen.
435    /// TODO: add an action queue fed by the frontend and treated here
436    // fn handle_message_actions(&mut self, loading_pane: &LoadingPaneRef) {
437    // ...
438    // }
439
440    // /// Shows the user profile sliding pane with the given avatar info.
441    // fn show_user_profile(
442    //     ...
443    // }
444
445    /// Shows the editing pane to allow the user to edit the given event.
446    // fn show_editing_pane(
447    //     &mut self,
448    //     cx: &mut Cx,
449    //     event_tl_item: EventTimelineItem,
450    //     room_id: OwnedRoomId,
451    // ) {
452    //     ...
453    // }
454
455    /// Handles the EditingPane in this RoomScreen being fully hidden.
456    // fn on_hide_editing_pane(&mut self) {
457    //     // In `show_editing_pane()` above, we hid the input_bar while the editing pane
458    //     // is being shown, so here we need to make it visible again.
459    //     ...
460    // }
461
462    /// Shows a preview of the given event that the user is currently replying to
463    /// above the message input bar.
464    // fn show_replying_to(&mut self, cx: &mut Cx, replying_to: (EventTimelineItem, RepliedToInfo)) {
465    //    ...
466    // }
467
468    /// Clears (and makes invisible) the preview of the message
469    // /// that the user is currently replying to.
470    // fn clear_replying_to(&mut self, cx: &mut Cx) {
471    //     ...
472    // }
473
474    // fn show_location_preview(&mut self, cx: &mut Cx) {
475    //     ...
476    // }
477
478    /// Invoke this when this timeline is being shown,
479    /// e.g., when the user navigates to this timeline.
480    pub fn show_timeline<R: Runtime>(&mut self, app_handle: &AppHandle<R>) {
481        let room_id = self.room_id.clone();
482        // just an optional sanity check
483        assert!(
484            self.tl_state.is_none(),
485            "BUG: tried to show_timeline() into a timeline with existing state. \
486            Did you forget to save the timeline state back to the global map of states?",
487        );
488
489        // Obtain the current user's power levels for this room.
490        submit_async_request(MatrixRequest::GetRoomPowerLevels {
491            room_id: room_id.clone(),
492        });
493
494        let state_opt = TIMELINE_STATES.lock().unwrap().remove(&room_id);
495        let (mut tl_state, first_time_showing_room) = if let Some(existing) = state_opt {
496            (existing, false)
497        } else {
498            let (_update_sender, update_receiver, request_sender) =
499                take_timeline_endpoints(&room_id)
500                    .expect("BUG: couldn't get timeline state for first-viewed room.");
501            let new_tl_state = TimelineUiState {
502                room_id: room_id.clone(),
503                // We assume the user has all power levels by default, just to avoid
504                // unexpectedly hiding any UI elements that should be visible to the user.
505                // This doesn't mean that the user can actually perform all actions.
506                user_power: UserPowerLevels::all(),
507                // We assume timelines being viewed for the first time haven't been fully paginated.
508                fully_paginated: false,
509                items: Vector::new(),
510                content_drawn_since_last_update: RangeSet::new(),
511                profile_drawn_since_last_update: RangeSet::new(),
512                update_receiver,
513                request_sender,
514                // media_cache: MediaCache::new(Some(update_sender)),
515                // replying_to: None,
516                saved_state: SavedState::default(),
517                last_scrolled_index: usize::MAX,
518                prev_first_index: None,
519                scrolled_past_read_marker: false,
520                latest_own_user_receipt: None,
521            };
522            (new_tl_state, true)
523        };
524
525        // Subscribe to typing notices, but hide the typing notice view initially.
526        // self.view(id!(typing_notice)).set_visible(cx, false);
527        submit_async_request(MatrixRequest::SubscribeToTypingNotices {
528            room_id: room_id.clone(),
529            subscribe: true,
530        });
531
532        submit_async_request(MatrixRequest::SubscribeToOwnUserReadReceiptsChanged {
533            room_id: room_id.clone(),
534            subscribe: true,
535        });
536        // Kick off a back pagination request for this room. This is "urgent",
537        // because we want to show the user some messages as soon as possible
538        // when they first open the room, and there might not be any messages yet.
539        if first_time_showing_room && !tl_state.fully_paginated {
540            println!(
541                "Sending a first-time backwards pagination request for room {}",
542                room_id
543            );
544            submit_async_request(MatrixRequest::PaginateRoomTimeline {
545                room_id: room_id.clone(),
546                num_events: 50,
547                direction: PaginationDirection::Backwards,
548            });
549
550            // Even though we specify that room member profiles should be lazy-loaded,
551            // the matrix server still doesn't consistently send them to our client properly.
552            // So we kick off a request to fetch the room members here upon first viewing the room.
553            submit_async_request(MatrixRequest::SyncRoomMemberList { room_id });
554        }
555
556        // Now, restore the visual state of this timeline from its previously-saved state.
557        self.restore_state(&mut tl_state);
558
559        // As the final step, store the tl_state for this room into this RoomScreen widget,
560        // such that it can be accessed in future event/draw handlers.
561        self.tl_state = Some(tl_state);
562
563        // Now that we have restored the TimelineUiState into this RoomScreen widget,
564        // we can proceed to processing pending background updates, and if any were processed,
565        // the timeline will also be redrawn.
566        if first_time_showing_room {
567            // let portal_list = self.portal_list(id!(list));
568            self.process_timeline_updates(app_handle);
569        }
570
571        self.patch_frontend_store_with_current_state(app_handle);
572    }
573
574    /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown.
575    fn hide_timeline(&mut self) {
576        self.save_state();
577
578        // When closing a room view, we do the following with non-persistent states:
579        // * Unsubscribe from typing notices, since we don't care about them
580        //   when a given room isn't visible.
581        // * Clear the location preview. We don't save this to the TimelineUiState
582        //   because the location might change by the next time the user opens this same room.
583        // self.location_preview(id!(location_preview)).clear();
584        submit_async_request(MatrixRequest::SubscribeToTypingNotices {
585            room_id: self.room_id.clone(),
586            subscribe: false,
587        });
588        submit_async_request(MatrixRequest::SubscribeToOwnUserReadReceiptsChanged {
589            room_id: self.room_id.clone(),
590            subscribe: false,
591        });
592    }
593
594    /// Removes the current room's visual UI state from this widget
595    /// and saves it to the map of `TIMELINE_STATES` such that it can be restored later.
596    ///
597    /// Note: after calling this function, the widget's `tl_state` will be `None`.
598    fn save_state(&mut self) {
599        let Some(mut tl) = self.tl_state.take() else {
600            eprintln!(
601                "Timeline::save_state(): skipping due to missing state, room {:?}",
602                self.room_id
603            );
604            return;
605        };
606
607        // let portal_list = self.portal_list(id!(list));
608        // let message_input_box = self.text_input(id!(input_bar.message_input.text_input));
609        // let editing_event = self
610        //     .editing_pane(id!(editing_pane))
611        //     .get_event_being_edited();
612        let state = SavedState {
613            first_index_and_scroll: None,
614            editing_event: None,
615            // first_index_and_scroll: Some((portal_list.first_id(), portal_list.scroll_position())),
616            // message_input_state: message_input_box.save_state(),
617            // replying_to: tl.replying_to.clone(),
618            // editing_event,
619        };
620        println!(
621            "Saving TimelineUiState for room {}: {:?}",
622            tl.room_id, state
623        );
624        tl.saved_state = state;
625        // Store this Timeline's `TimelineUiState` in the global map of states.
626        TIMELINE_STATES
627            .lock()
628            .unwrap()
629            .insert(tl.room_id.clone(), tl);
630    }
631
632    /// Restores the previously-saved visual UI state of this room.
633    ///
634    /// Note: this accepts a direct reference to the timeline's UI state,
635    /// so this function must not try to re-obtain it by accessing `self.tl_state`.
636    fn restore_state(&mut self, tl_state: &mut TimelineUiState) {
637        let SavedState {
638            first_index_and_scroll,
639            // message_input_state,
640            // replying_to,
641            editing_event,
642        } = &mut tl_state.saved_state;
643        // 1. Restore the position of the timeline.
644        // if let Some((first_index, scroll_from_first_id)) = first_index_and_scroll {
645        //     self.portal_list(id!(timeline.list))
646        //         .set_first_id_and_scroll(*first_index, *scroll_from_first_id);
647        // } else {
648        //     // If the first index is not set, then the timeline has not yet been scrolled by the user,
649        //     // so we set the portal list to "tail" (track) the bottom of the list.
650        //     self.portal_list(id!(timeline.list)).set_tail_range(true);
651        // }
652
653        // 2. Restore the state of the message input box.
654        // let saved_message_input_state = std::mem::take(message_input_state);
655        // self.text_input(id!(input_bar.message_input.text_input))
656        //     .restore_state(cx, saved_message_input_state);
657
658        // 3. Restore the state of the replying-to preview.
659        // if let Some(replying_to_event) = replying_to.take() {
660        //     self.show_replying_to(cx, replying_to_event);
661        // } else {
662        //     self.clear_replying_to(cx);
663        // }
664
665        // 4. Restore the state of the editing pane.
666        // if let Some(editing_event) = editing_event.take() {
667        //     self.show_editing_pane(cx, editing_event, tl_state.room_id.clone());
668        // } else {
669        //     self.editing_pane(id!(editing_pane)).force_hide(cx);
670        //     self.on_hide_editing_pane(cx);
671        // }
672    }
673
674    /// Sets this `RoomScreen` widget to display the timeline for the given room.
675    pub fn set_displayed_room<S: Into<Option<String>>, R: Runtime>(
676        &mut self,
677        room_id: OwnedRoomId,
678        room_name: S,
679        app_handle: &AppHandle<R>,
680    ) {
681        // If the room is already being displayed, then do nothing.
682        // if self.room_id.as_ref().is_some_and(|id| id == &room_id) {
683        //     return;
684        // }
685
686        self.hide_timeline();
687        // Reset the the state of the inner loading pane.
688        // self.loading_pane(id!(loading_pane)).take_state();
689        self.room_name = room_name_or_id(room_name.into(), &room_id);
690        self.room_id = room_id.clone();
691
692        // Clear any mention input state
693        // let input_bar = self.view.room_input_bar(id!(input_bar));
694        // let message_input = input_bar.mentionable_text_input(id!(message_input));
695        // message_input.set_room_id(room_id);
696
697        self.show_timeline(app_handle);
698    }
699
700    /// Sends read receipts based on the current scroll position of the timeline.
701    fn _send_user_read_receipts_based_on_scroll_pos(
702        &mut self,
703        _scrolled: bool,
704        _first_id: usize,
705        _visible_items: usize,
706    ) {
707        // TODO: leave this to frontend
708    }
709
710    /// Sends a backwards pagination request if the user is scrolling up
711    /// and is approaching the top of the timeline.
712    fn _send_pagination_request_based_on_scroll_pos(&mut self, _scrolled: bool, _first_id: usize) {
713        // TODO: leave this to frontend
714    }
715}
716
717/// States that are necessary to save in order to maintain a consistent UI display for a timeline.
718///
719/// These are saved when navigating away from a timeline (upon `Hide`)
720/// and restored when navigating back to a timeline (upon `Show`).
721#[derive(Default, Debug)]
722pub struct SavedState {
723    /// The index of the first item in the timeline's PortalList that is currently visible,
724    /// and the scroll offset from the top of the list's viewport to the beginning of that item.
725    /// If this is `None`, then the timeline has not yet been scrolled by the user
726    /// and the portal list will be set to "tail" (track) the bottom of the list.
727    first_index_and_scroll: Option<(usize, f64)>,
728    /// The content of the message input box.
729    // message_input_state: TextInputState, // TODO: replace this by a string ?
730    /// The event that the user is currently replying to, if any.
731    // replying_to: Option<(EventTimelineItem, RepliedToInfo)>, // TODO: adapt with new type from sdk
732    /// The event that the user is currently editing, if any.
733    editing_event: Option<EventTimelineItem>,
734}
735
736/// Returns info about the item in the list of `new_items` that matches the event ID
737/// of a visible item in the given `curr_items` list.
738///
739/// This info includes a tuple of:
740/// 1. the index of the item in the current items list,
741/// 2. the index of the item in the new items list,
742/// 3. the positional "scroll" offset of the corresponding current item in the portal list,
743/// 4. the unique event ID of the item.
744fn find_new_item_matching_current_item(
745    visible_items: usize,          // DUMMY PARAM TODO CHANGE THIS
746    position_of_item: Option<f64>, // DUMMY PARAM TODO CHANGE THIS
747    starting_at_curr_idx: usize,
748    curr_items: &Vector<Arc<TimelineItem>>,
749    new_items: &Vector<Arc<TimelineItem>>,
750) -> Option<(usize, usize, f64, OwnedEventId)> {
751    let mut curr_item_focus = curr_items.focus();
752    let mut idx_curr = starting_at_curr_idx;
753    let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = Vec::with_capacity(visible_items);
754
755    // Find all items with real event IDs that are currently visible in the portal list.
756    // TODO: if this is slow, we could limit it to 3-5 events at the most.
757    if curr_items_with_ids.len() <= visible_items {
758        while let Some(curr_item) = curr_item_focus.get(idx_curr) {
759            if let Some(event_id) = curr_item.as_event().and_then(|ev| ev.event_id()) {
760                curr_items_with_ids.push((idx_curr, event_id.to_owned()));
761            }
762            if curr_items_with_ids.len() >= visible_items {
763                break;
764            }
765            idx_curr += 1;
766        }
767    }
768
769    // Find a new item that has the same real event ID as any of the current items.
770    for (idx_new, new_item) in new_items.iter().enumerate() {
771        let Some(event_id) = new_item.as_event().and_then(|ev| ev.event_id()) else {
772            continue;
773        };
774        if let Some((idx_curr, _)) = curr_items_with_ids
775            .iter()
776            .find(|(_, ev_id)| ev_id == event_id)
777        {
778            // Not all items in the portal list are guaranteed to have a position offset,
779            // some may be zeroed-out, so we need to account for that possibility by only
780            // using events that have a real non-zero area
781            if let Some(pos_offset) = position_of_item {
782                println!("Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}");
783                return Some((*idx_curr, idx_new, pos_offset, event_id.to_owned()));
784            }
785        }
786    }
787
788    None
789}