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