1use std::{collections::HashMap, ops::ControlFlow, sync::Arc};
2
3use futures_util::StreamExt;
4use gtk::{
5 gio, glib,
6 glib::{clone, closure_local},
7 prelude::*,
8 subclass::prelude::*,
9};
10use matrix_sdk_ui::{
11 eyeball_im::VectorDiff,
12 timeline::{
13 RoomExt, Timeline as SdkTimeline, TimelineEventItemId, TimelineItem as SdkTimelineItem,
14 default_event_filter,
15 },
16};
17use ruma::{
18 OwnedEventId, RoomVersionId, UserId,
19 events::{
20 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, SyncMessageLikeEvent,
21 SyncStateEvent, room::message::MessageType,
22 },
23};
24use tokio::task::AbortHandle;
25use tracing::error;
26
27mod event;
28mod timeline_diff_minimizer;
29mod timeline_item;
30mod virtual_item;
31
32use self::timeline_diff_minimizer::{TimelineDiff, TimelineDiffItemStore};
33pub(crate) use self::{
34 event::*,
35 timeline_item::{TimelineItem, TimelineItemExt, TimelineItemImpl},
36 virtual_item::{VirtualItem, VirtualItemKind},
37};
38use super::Room;
39use crate::{
40 prelude::*,
41 spawn, spawn_tokio,
42 utils::{LoadingState, SingleItemListModel},
43};
44
45const MAX_BATCH_SIZE: u16 = 20;
47const MAX_TIME_BETWEEN_HEADERS: u64 = 20 * 60 * 1000;
52
53mod imp {
54 use std::{
55 cell::{Cell, OnceCell, RefCell},
56 iter,
57 marker::PhantomData,
58 sync::LazyLock,
59 };
60
61 use glib::subclass::Signal;
62
63 use super::*;
64
65 #[derive(Debug, Default, glib::Properties)]
66 #[properties(wrapper_type = super::Timeline)]
67 pub struct Timeline {
68 #[property(get, set = Self::set_room, construct_only)]
70 room: OnceCell<Room>,
71 matrix_timeline: OnceCell<Arc<SdkTimeline>>,
73 start_items: OnceCell<SingleItemListModel>,
77 sdk_items: OnceCell<gio::ListStore>,
79 filter: gtk::CustomFilter,
81 filtered_sdk_items: gtk::FilterListModel,
83 end_items: OnceCell<SingleItemListModel>,
87 #[property(get = Self::items)]
89 items: OnceCell<gtk::FlattenListModel>,
90 pub(super) event_map: RefCell<HashMap<TimelineEventItemId, Event>>,
93 #[property(get, builder(LoadingState::default()))]
95 state: Cell<LoadingState>,
96 #[property(get)]
98 is_loading_start: Cell<bool>,
99 #[property(get = Self::is_empty)]
101 is_empty: PhantomData<bool>,
102 #[property(get, set = Self::set_preload, explicit_notify)]
104 preload: Cell<bool>,
105 #[property(get)]
107 has_reached_start: Cell<bool>,
108 #[property(get)]
110 has_room_create: Cell<bool>,
111 diff_handle: OnceCell<AbortHandle>,
112 back_pagination_status_handle: OnceCell<AbortHandle>,
113 read_receipts_changed_handle: OnceCell<AbortHandle>,
114 }
115
116 #[glib::object_subclass]
117 impl ObjectSubclass for Timeline {
118 const NAME: &'static str = "Timeline";
119 type Type = super::Timeline;
120 }
121
122 #[glib::derived_properties]
123 impl ObjectImpl for Timeline {
124 fn signals() -> &'static [Signal] {
125 static SIGNALS: LazyLock<Vec<Signal>> =
126 LazyLock::new(|| vec![Signal::builder("read-change-trigger").build()]);
127 SIGNALS.as_ref()
128 }
129
130 fn constructed(&self) {
131 self.parent_constructed();
132
133 self.filter.set_filter_func(clone!(
134 #[weak(rename_to = imp)]
135 self,
136 #[upgrade_or]
137 true,
138 move |obj| {
139 obj.downcast_ref::<VirtualItem>().is_none_or(|item| {
141 !(imp.has_room_create.get()
142 && item.kind() == VirtualItemKind::TimelineStart)
143 })
144 }
145 ));
146 self.filtered_sdk_items.set_filter(Some(&self.filter));
147 }
148
149 fn dispose(&self) {
150 if let Some(handle) = self.diff_handle.get() {
151 handle.abort();
152 }
153 if let Some(handle) = self.back_pagination_status_handle.get() {
154 handle.abort();
155 }
156 if let Some(handle) = self.read_receipts_changed_handle.get() {
157 handle.abort();
158 }
159 }
160 }
161
162 impl Timeline {
163 fn set_room(&self, room: Room) {
165 let room = self.room.get_or_init(|| room);
166
167 room.typing_list().connect_is_empty_notify(clone!(
168 #[weak(rename_to = imp)]
169 self,
170 move |list| {
171 if !list.is_empty() {
172 imp.add_typing_row();
173 }
174 }
175 ));
176 }
177
178 fn room(&self) -> &Room {
180 self.room.get().expect("room should be initialized")
181 }
182
183 pub(super) async fn init_matrix_timeline(&self) {
185 let room = self.room();
186 let room_id = room.room_id().to_owned();
187 let matrix_room = room.matrix_room().clone();
188
189 let handle = spawn_tokio!(async move {
190 matrix_room
191 .timeline_builder()
192 .event_filter(show_in_timeline)
193 .add_failed_to_parse(false)
194 .build()
195 .await
196 });
197
198 let matrix_timeline = match handle.await.expect("task was not aborted") {
199 Ok(timeline) => timeline,
200 Err(error) => {
201 error!("Could not create timeline: {error}");
202 return;
203 }
204 };
205
206 let matrix_timeline = Arc::new(matrix_timeline);
207 self.matrix_timeline
208 .set(matrix_timeline.clone())
209 .expect("matrix timeline is uninitialized");
210
211 let (values, timeline_stream) = matrix_timeline.subscribe().await;
212
213 if *IS_AT_TRACE_LEVEL {
214 tracing::trace!(
215 room = self.room().human_readable_id(),
216 items = ?sdk_items_to_log(&values),
217 "Initial timeline items",
218 );
219 }
220
221 if !values.is_empty() {
222 self.update_with_single_diff(VectorDiff::Append { values });
223 }
224
225 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
226 let fut = timeline_stream.for_each(move |diff_list| {
227 let obj_weak = obj_weak.clone();
228 let room_id = room_id.clone();
229 async move {
230 let ctx = glib::MainContext::default();
231 ctx.spawn(async move {
232 spawn!(async move {
233 if let Some(obj) = obj_weak.upgrade() {
234 obj.imp().update_with_diff_list(diff_list);
235 } else {
236 error!(
237 "Could not send timeline diff for room {room_id}: \
238 could not upgrade weak reference"
239 );
240 }
241 });
242 });
243 }
244 });
245
246 let diff_handle = spawn_tokio!(fut);
247 self.diff_handle
248 .set(diff_handle.abort_handle())
249 .expect("handle should be uninitialized");
250
251 self.watch_read_receipts().await;
252
253 if self.preload.get() {
254 self.preload().await;
255 }
256
257 self.set_state(LoadingState::Ready);
258 }
259
260 pub(super) fn matrix_timeline(&self) -> &Arc<SdkTimeline> {
262 self.matrix_timeline
263 .get()
264 .expect("matrix timeline should be initialized")
265 }
266
267 fn start_items(&self) -> &SingleItemListModel {
269 self.start_items.get_or_init(|| {
270 let model = SingleItemListModel::new(&VirtualItem::spinner(&self.obj()));
271 model.set_is_hidden(true);
272 model
273 })
274 }
275
276 pub(super) fn sdk_items(&self) -> &gio::ListStore {
278 self.sdk_items.get_or_init(|| {
279 let sdk_items = gio::ListStore::new::<TimelineItem>();
280 self.filtered_sdk_items.set_model(Some(&sdk_items));
281 sdk_items
282 })
283 }
284
285 fn end_items(&self) -> &SingleItemListModel {
287 self.end_items.get_or_init(|| {
288 let model = SingleItemListModel::new(&VirtualItem::typing(&self.obj()));
289 model.set_is_hidden(true);
290 model
291 })
292 }
293
294 fn items(&self) -> gtk::FlattenListModel {
296 self.items
297 .get_or_init(|| {
298 let model_list = gio::ListStore::new::<gio::ListModel>();
299 model_list.append(self.start_items());
300 model_list.append(&self.filtered_sdk_items);
301 model_list.append(self.end_items());
302 gtk::FlattenListModel::new(Some(model_list))
303 })
304 .clone()
305 }
306
307 fn is_empty(&self) -> bool {
309 self.filtered_sdk_items.n_items() == 0
310 }
311
312 fn set_state(&self, state: LoadingState) {
314 if self.state.get() == state {
315 return;
316 }
317
318 self.state.set(state);
319
320 self.obj().notify_state();
321 }
322
323 fn update_loading_state(&self) {
325 let is_loading = self.is_loading_start.get();
326
327 if is_loading {
328 self.set_state(LoadingState::Loading);
329 } else if self.state.get() != LoadingState::Error {
330 self.set_state(LoadingState::Ready);
331 }
332 }
333
334 fn set_loading_start(&self, is_loading_start: bool) {
336 if self.is_loading_start.get() == is_loading_start {
337 return;
338 }
339
340 self.is_loading_start.set(is_loading_start);
341
342 self.update_loading_state();
343 self.start_items().set_is_hidden(!is_loading_start);
344 self.obj().notify_is_loading_start();
345 }
346
347 fn set_has_reached_start(&self, has_reached_start: bool) {
349 if self.has_reached_start.get() == has_reached_start {
350 return;
352 }
353
354 self.has_reached_start.set(has_reached_start);
355
356 self.obj().notify_has_reached_start();
357 }
358
359 fn set_has_room_create(&self, has_room_create: bool) {
361 if self.has_room_create.get() == has_room_create {
362 return;
363 }
364
365 self.has_room_create.set(has_room_create);
366
367 let change = if has_room_create {
368 gtk::FilterChange::MoreStrict
369 } else {
370 gtk::FilterChange::LessStrict
371 };
372 self.filter.changed(change);
373
374 self.obj().notify_has_room_create();
375 }
376
377 fn clear(&self) {
382 self.event_map.borrow_mut().clear();
383 self.set_has_reached_start(false);
384 self.set_has_room_create(false);
385 }
386
387 fn set_preload(&self, preload: bool) {
389 if self.preload.get() == preload {
390 return;
391 }
392
393 self.preload.set(preload);
394 self.obj().notify_preload();
395
396 if preload && self.can_paginate_backwards() {
397 spawn!(
398 glib::Priority::DEFAULT_IDLE,
399 clone!(
400 #[weak(rename_to = imp)]
401 self,
402 async move {
403 imp.preload().await;
404 }
405 )
406 );
407 }
408 }
409
410 async fn preload(&self) {
412 if self.filtered_sdk_items.n_items() < u32::from(MAX_BATCH_SIZE) {
413 self.paginate_backwards(|| ControlFlow::Break(())).await;
414 }
415 }
416
417 fn update_with_diff_list(&self, diff_list: Vec<VectorDiff<Arc<SdkTimelineItem>>>) {
419 if *IS_AT_TRACE_LEVEL {
420 self.log_diff_list(&diff_list);
421 }
422
423 let was_empty = self.is_empty();
424
425 if let Some(diff_list) = self.try_minimize_diff_list(diff_list) {
426 for diff in diff_list {
428 self.update_with_single_diff(diff);
429 }
430 }
431
432 if *IS_AT_TRACE_LEVEL {
433 self.log_items();
434 }
435
436 let obj = self.obj();
437 if self.is_empty() != was_empty {
438 obj.notify_is_empty();
439 }
440
441 obj.emit_read_change_trigger();
442 }
443
444 fn try_minimize_diff_list(
452 &self,
453 diff_list: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
454 ) -> Option<Vec<VectorDiff<Arc<SdkTimelineItem>>>> {
455 if !self.can_minimize_diff_list(&diff_list) {
456 return Some(diff_list);
457 }
458
459 self.minimize_diff_list(diff_list);
460
461 None
462 }
463
464 fn update_with_single_diff(&self, diff: VectorDiff<Arc<SdkTimelineItem>>) {
466 match diff {
467 VectorDiff::Append { values } => {
468 let new_list = values
469 .into_iter()
470 .map(|item| self.create_item(&item))
471 .collect::<Vec<_>>();
472
473 self.update_items(self.sdk_items().n_items(), 0, &new_list);
474 }
475 VectorDiff::Clear => {
476 self.sdk_items().remove_all();
477 self.clear();
478 }
479 VectorDiff::PushFront { value } => {
480 let item = self.create_item(&value);
481 self.update_items(0, 0, &[item]);
482 }
483 VectorDiff::PushBack { value } => {
484 let item = self.create_item(&value);
485 self.update_items(self.sdk_items().n_items(), 0, &[item]);
486 }
487 VectorDiff::PopFront => {
488 self.update_items(0, 1, &[]);
489 }
490 VectorDiff::PopBack => {
491 self.update_items(self.sdk_items().n_items().saturating_sub(1), 1, &[]);
492 }
493 VectorDiff::Insert { index, value } => {
494 let item = self.create_item(&value);
495 self.update_items(index as u32, 0, &[item]);
496 }
497 VectorDiff::Set { index, value } => {
498 let pos = index as u32;
499 let item = self
500 .item_at(pos)
501 .expect("there should be an item at the given position");
502
503 if item.timeline_id() == value.unique_id().0 {
504 self.update_item(&item, &value);
506 self.update_items_headers(pos, 1);
508 } else {
509 let item = self.create_item(&value);
510 self.update_items(pos, 1, &[item]);
511 }
512 }
513 VectorDiff::Remove { index } => {
514 self.update_items(index as u32, 1, &[]);
515 }
516 VectorDiff::Truncate { length } => {
517 let length = length as u32;
518 let old_len = self.sdk_items().n_items();
519 self.update_items(length, old_len.saturating_sub(length), &[]);
520 }
521 VectorDiff::Reset { values } => {
522 self.clear();
524
525 let removed = self.sdk_items().n_items();
526 let new_list = values
527 .into_iter()
528 .map(|item| self.create_item(&item))
529 .collect::<Vec<_>>();
530
531 self.update_items(0, removed, &new_list);
532 }
533 }
534 }
535
536 fn item_at(&self, pos: u32) -> Option<TimelineItem> {
538 self.sdk_items().item(pos).and_downcast()
539 }
540
541 fn update_items(&self, pos: u32, n_removals: u32, additions: &[TimelineItem]) {
544 for i in pos..pos + n_removals {
545 let Some(item) = self.item_at(i) else {
546 error!("Timeline item at position {i} not found");
548 break;
549 };
550
551 self.remove_item(&item);
552 }
553
554 self.sdk_items().splice(pos, n_removals, additions);
555
556 self.update_items_headers(pos, additions.len() as u32);
559
560 if !additions.is_empty() {
562 self.room().update_latest_activity(
563 additions.iter().filter_map(|i| i.downcast_ref::<Event>()),
564 );
565 }
566 }
567
568 fn update_items_headers(&self, pos: u32, nb: u32) {
571 let sdk_items = self.sdk_items();
572
573 let (mut previous_sender, mut previous_timestamp) = if pos > 0 {
574 sdk_items
575 .item(pos - 1)
576 .and_downcast::<Event>()
577 .filter(Event::can_show_header)
578 .map(|event| (event.sender_id(), event.origin_server_ts()))
579 } else {
580 None
581 }
582 .unzip();
583
584 for i in pos..=pos + nb {
586 let Some(current) = self.item_at(i) else {
587 break;
588 };
589 let Ok(current) = current.downcast::<Event>() else {
590 previous_sender = None;
591 continue;
592 };
593
594 let current_sender = current.sender_id();
595
596 if !current.can_show_header() {
597 current.set_header_state(EventHeaderState::Hidden);
598 previous_sender = None;
599 previous_timestamp = None;
600 continue;
601 }
602
603 let header_state = if previous_sender
604 .as_ref()
605 .is_none_or(|previous_sender| current_sender != *previous_sender)
606 {
607 EventHeaderState::Full
609 } else if previous_timestamp
610 .and_then(|ts| current.origin_server_ts().0.checked_sub(ts.0))
611 .is_some_and(|elapsed| u64::from(elapsed) >= MAX_TIME_BETWEEN_HEADERS)
612 {
613 EventHeaderState::TimestampOnly
615 } else {
616 EventHeaderState::Hidden
618 };
619
620 current.set_header_state(header_state);
621 previous_sender = Some(current_sender);
622 previous_timestamp = Some(current.origin_server_ts());
623 }
624 }
625
626 fn remove_item(&self, item: &TimelineItem) {
628 if let Some(event) = item.downcast_ref::<Event>() {
629 let mut removed_from_map = false;
630 let mut event_map = self.event_map.borrow_mut();
631
632 let identifiers = event
634 .transaction_id()
635 .map(TimelineEventItemId::TransactionId)
636 .into_iter()
637 .chain(event.event_id().map(TimelineEventItemId::EventId));
638
639 for id in identifiers {
640 let found = event_map.get(&id).is_some_and(|e| e == event);
645
646 if found {
647 event_map.remove(&id);
648 removed_from_map = true;
649 }
650 }
651
652 if removed_from_map && event.is_room_create() {
653 self.set_has_room_create(false);
654 }
655 }
656 }
657
658 pub(super) fn can_paginate_backwards(&self) -> bool {
661 self.state.get() != LoadingState::Initial
665 && !self.is_loading_start.get()
666 && !self.has_reached_start.get()
667 }
668
669 pub(super) async fn paginate_backwards<F>(&self, continue_fn: F)
672 where
673 F: Fn() -> ControlFlow<()>,
674 {
675 self.set_loading_start(true);
676
677 loop {
678 if !self.paginate_backwards_inner().await {
679 break;
680 }
681
682 if continue_fn().is_break() {
683 break;
684 }
685 }
686
687 self.set_loading_start(false);
688 }
689
690 async fn paginate_backwards_inner(&self) -> bool {
694 let matrix_timeline = self.matrix_timeline().clone();
695 let handle =
696 spawn_tokio!(
697 async move { matrix_timeline.paginate_backwards(MAX_BATCH_SIZE).await }
698 );
699
700 match handle.await.expect("task was not aborted") {
701 Ok(reached_start) => {
702 if reached_start {
703 self.set_has_reached_start(true);
704 }
705
706 !reached_start
707 }
708 Err(error) => {
709 error!("Could not load timeline: {error}");
710 self.set_state(LoadingState::Error);
711 false
712 }
713 }
714 }
715
716 fn add_typing_row(&self) {
718 self.end_items().set_is_hidden(false);
719 }
720
721 pub(super) fn remove_empty_typing_row(&self) {
723 if !self.room().typing_list().is_empty() {
724 return;
725 }
726
727 self.end_items().set_is_hidden(true);
728 }
729
730 async fn watch_read_receipts(&self) {
732 let room_id = self.room().room_id().to_owned();
733 let matrix_timeline = self.matrix_timeline();
734
735 let stream = matrix_timeline
736 .subscribe_own_user_read_receipts_changed()
737 .await;
738
739 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
740 let fut = stream.for_each(move |()| {
741 let obj_weak = obj_weak.clone();
742 let room_id = room_id.clone();
743 async move {
744 let ctx = glib::MainContext::default();
745 ctx.spawn(async move {
746 spawn!(async move {
747 if let Some(obj) = obj_weak.upgrade() {
748 obj.emit_read_change_trigger();
749 } else {
750 error!(
751 "Could not emit read change trigger for room {room_id}: \
752 could not upgrade weak reference"
753 );
754 }
755 });
756 });
757 }
758 });
759
760 let handle = spawn_tokio!(fut);
761 self.read_receipts_changed_handle
762 .set(handle.abort_handle())
763 .expect("handle is uninitialized");
764 }
765 }
766
767 impl TimelineDiffItemStore for Timeline {
768 type Item = TimelineItem;
769 type Data = Arc<SdkTimelineItem>;
770
771 fn items(&self) -> Vec<TimelineItem> {
772 self.sdk_items()
773 .snapshot()
774 .into_iter()
775 .map(|obj| {
776 obj.downcast::<TimelineItem>()
777 .expect("SDK items are TimelineItems")
778 })
779 .collect()
780 }
781
782 fn create_item(&self, data: &Arc<SdkTimelineItem>) -> TimelineItem {
783 let item = TimelineItem::new(data, &self.obj());
784
785 if let Some(event) = item.downcast_ref::<Event>() {
786 self.event_map
787 .borrow_mut()
788 .insert(event.identifier(), event.clone());
789
790 if event.counts_as_unread() {
792 if let Some(members) = self.room().members() {
793 let member = members.get_or_create(event.sender_id());
794 member.set_latest_activity(u64::from(event.origin_server_ts().get()));
795 }
796 }
797
798 if event.is_room_create() {
799 self.set_has_room_create(true);
800 }
801 }
802
803 item
804 }
805
806 fn update_item(&self, item: &TimelineItem, data: &Arc<SdkTimelineItem>) {
807 item.update_with(data);
808
809 if let Some(event) = item.downcast_ref::<Event>() {
810 self.event_map
813 .borrow_mut()
814 .insert(event.identifier(), event.clone());
815
816 self.room().update_latest_activity(iter::once(event));
818 }
819 }
820
821 fn apply_item_diff_list(&self, item_diff_list: Vec<TimelineDiff<TimelineItem>>) {
822 for item_diff in item_diff_list {
823 match item_diff {
824 TimelineDiff::Splice(splice) => {
825 self.update_items(splice.pos, splice.n_removals, &splice.additions);
826 }
827 TimelineDiff::Update(update) => {
828 self.update_items_headers(update.pos, update.n_items);
829 }
830 }
831 }
832 }
833 }
834
835 static IS_AT_TRACE_LEVEL: LazyLock<bool> = LazyLock::new(|| {
840 tracing_subscriber::EnvFilter::try_from_default_env()
841 .ok()
843 .and_then(|filter| filter.max_level_hint())
844 .is_some_and(|max| max == tracing::level_filters::LevelFilter::TRACE)
845 });
846
847 impl Timeline {
849 fn log_diff_list(&self, diff_list: &[VectorDiff<Arc<SdkTimelineItem>>]) {
851 let mut log_list = Vec::with_capacity(diff_list.len());
852
853 for diff in diff_list {
854 let log = match diff {
855 VectorDiff::Append { values } => {
856 format!("append: {:?}", sdk_items_to_log(values))
857 }
858 VectorDiff::Clear => "clear".to_owned(),
859 VectorDiff::PushFront { value } => {
860 format!("push_front: {}", sdk_item_to_log(value))
861 }
862 VectorDiff::PushBack { value } => {
863 format!("push_back: {}", sdk_item_to_log(value))
864 }
865 VectorDiff::PopFront => "pop_front".to_owned(),
866 VectorDiff::PopBack => "pop_back".to_owned(),
867 VectorDiff::Insert { index, value } => {
868 format!("insert at {index}: {}", sdk_item_to_log(value))
869 }
870 VectorDiff::Set { index, value } => {
871 format!("set at {index}: {}", sdk_item_to_log(value))
872 }
873 VectorDiff::Remove { index } => format!("remove at {index}"),
874 VectorDiff::Truncate { length } => format!("truncate at {length}"),
875 VectorDiff::Reset { values } => {
876 format!("reset: {:?}", sdk_items_to_log(values))
877 }
878 };
879
880 log_list.push(log);
881 }
882
883 tracing::trace!(
884 room = self.room().human_readable_id(),
885 "Diff list: {log_list:#?}"
886 );
887 }
888
889 fn log_items(&self) {
891 let items = self
892 .sdk_items()
893 .iter::<TimelineItem>()
894 .filter_map(|item| item.as_ref().map(item_to_log).ok())
895 .collect::<Vec<_>>();
896
897 tracing::trace!(
898 room = self.room().human_readable_id(),
899 "Timeline: {items:#?}"
900 );
901 }
902 }
903
904 fn sdk_items_to_log(
906 items: &matrix_sdk_ui::eyeball_im::Vector<Arc<SdkTimelineItem>>,
907 ) -> Vec<String> {
908 items.iter().map(|item| sdk_item_to_log(item)).collect()
909 }
910
911 fn sdk_item_to_log(item: &SdkTimelineItem) -> String {
912 match item.kind() {
913 matrix_sdk_ui::timeline::TimelineItemKind::Event(event) => {
914 format!("event::{:?}", event.identifier())
915 }
916 matrix_sdk_ui::timeline::TimelineItemKind::Virtual(virtual_item) => {
917 format!("virtual::{virtual_item:?}")
918 }
919 }
920 }
921
922 fn item_to_log(item: &TimelineItem) -> String {
923 if let Some(virtual_item) = item.downcast_ref::<VirtualItem>() {
924 format!("virtual::{:?}", virtual_item.kind())
925 } else if let Some(event) = item.downcast_ref::<Event>() {
926 format!("event::{:?}", event.identifier())
927 } else {
928 "Unknown item".to_owned()
929 }
930 }
931}
932
933glib::wrapper! {
934 pub struct Timeline(ObjectSubclass<imp::Timeline>);
940}
941
942impl Timeline {
943 pub(crate) fn new(room: &Room) -> Self {
945 let obj = glib::Object::builder::<Self>()
946 .property("room", room)
947 .build();
948
949 let imp = obj.imp();
950 spawn!(clone!(
951 #[weak]
952 imp,
953 async move {
954 imp.init_matrix_timeline().await;
955 }
956 ));
957
958 obj
959 }
960
961 pub(crate) fn matrix_timeline(&self) -> Arc<SdkTimeline> {
963 self.imp().matrix_timeline().clone()
964 }
965
966 pub(crate) async fn paginate_backwards<F>(&self, continue_fn: F)
969 where
970 F: Fn() -> ControlFlow<()>,
971 {
972 let imp = self.imp();
973
974 if !imp.can_paginate_backwards() {
975 return;
976 }
977
978 imp.paginate_backwards(continue_fn).await;
979 }
980
981 pub(crate) fn event_by_identifier(&self, identifier: &TimelineEventItemId) -> Option<Event> {
986 self.imp().event_map.borrow().get(identifier).cloned()
987 }
988
989 pub(crate) fn find_event_position(&self, identifier: &TimelineEventItemId) -> Option<usize> {
992 self.items()
993 .iter::<glib::Object>()
994 .enumerate()
995 .find_map(|(index, item)| {
996 item.ok()
997 .and_downcast::<Event>()
998 .is_some_and(|event| event.matches_identifier(identifier))
999 .then_some(index)
1000 })
1001 }
1002
1003 pub(crate) fn remove_empty_typing_row(&self) {
1005 self.imp().remove_empty_typing_row();
1006 }
1007
1008 pub(crate) async fn has_unread_messages(&self) -> Option<bool> {
1013 let session = self.room().session()?;
1014 let own_user_id = session.user_id().clone();
1015 let matrix_timeline = self.matrix_timeline();
1016
1017 let user_receipt_item = spawn_tokio!(async move {
1018 matrix_timeline
1019 .latest_user_read_receipt_timeline_event_id(&own_user_id)
1020 .await
1021 })
1022 .await
1023 .expect("task was not aborted");
1024
1025 let sdk_items = self.imp().sdk_items();
1026 let count = sdk_items.n_items();
1027
1028 for pos in (0..count).rev() {
1029 let Some(event) = sdk_items.item(pos).and_downcast::<Event>() else {
1030 continue;
1031 };
1032
1033 if user_receipt_item.is_some() && event.event_id() == user_receipt_item {
1034 return Some(false);
1036 }
1037 if event.counts_as_unread() {
1038 return Some(true);
1040 }
1041 }
1042
1043 None
1047 }
1048
1049 pub(crate) fn redactable_events_for(&self, user_id: &UserId) -> Vec<OwnedEventId> {
1051 let mut events = vec![];
1052
1053 for item in self.imp().sdk_items().iter::<glib::Object>() {
1054 let Ok(item) = item else {
1055 break;
1057 };
1058 let Ok(event) = item.downcast::<Event>() else {
1059 continue;
1060 };
1061
1062 if event.sender_id() != user_id {
1063 continue;
1064 }
1065
1066 if event.can_be_redacted() {
1067 if let Some(event_id) = event.event_id() {
1068 events.push(event_id);
1069 }
1070 }
1071 }
1072
1073 events
1074 }
1075
1076 fn emit_read_change_trigger(&self) {
1078 self.emit_by_name::<()>("read-change-trigger", &[]);
1079 }
1080
1081 pub(crate) fn connect_read_change_trigger<F: Fn(&Self) + 'static>(
1083 &self,
1084 f: F,
1085 ) -> glib::SignalHandlerId {
1086 self.connect_closure(
1087 "read-change-trigger",
1088 true,
1089 closure_local!(move |obj: Self| {
1090 f(&obj);
1091 }),
1092 )
1093 }
1094}
1095
1096fn show_in_timeline(any: &AnySyncTimelineEvent, room_version: &RoomVersionId) -> bool {
1098 if !default_event_filter(any, room_version) {
1100 return false;
1101 }
1102
1103 match any {
1105 AnySyncTimelineEvent::MessageLike(msg) => match msg {
1106 AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(ev)) => {
1107 matches!(
1108 ev.content.msgtype,
1109 MessageType::Audio(_)
1110 | MessageType::Emote(_)
1111 | MessageType::File(_)
1112 | MessageType::Image(_)
1113 | MessageType::Location(_)
1114 | MessageType::Notice(_)
1115 | MessageType::ServerNotice(_)
1116 | MessageType::Text(_)
1117 | MessageType::Video(_)
1118 )
1119 }
1120 AnySyncMessageLikeEvent::Sticker(SyncMessageLikeEvent::Original(_))
1121 | AnySyncMessageLikeEvent::RoomEncrypted(SyncMessageLikeEvent::Original(_)) => true,
1122 _ => false,
1123 },
1124 AnySyncTimelineEvent::State(AnySyncStateEvent::RoomMember(SyncStateEvent::Original(
1125 member_event,
1126 ))) => {
1127 !member_event
1131 .unsigned
1132 .prev_content
1133 .as_ref()
1134 .is_some_and(|prev_content| {
1135 prev_content.membership == member_event.content.membership
1136 && prev_content.displayname == member_event.content.displayname
1137 && prev_content.avatar_url == member_event.content.avatar_url
1138 })
1139 }
1140 AnySyncTimelineEvent::State(state) => matches!(
1141 state,
1142 AnySyncStateEvent::RoomMember(_)
1143 | AnySyncStateEvent::RoomCreate(_)
1144 | AnySyncStateEvent::RoomEncryption(_)
1145 | AnySyncStateEvent::RoomThirdPartyInvite(_)
1146 ),
1147 }
1148}