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