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}