Skip to main content

fractal/session/room/timeline/
mod.rs

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
46/// The number of events to request when loading more history.
47const MAX_BATCH_SIZE: u16 = 20;
48/// The maximum time between contiguous events before we show their header, in
49/// milliseconds.
50///
51/// This matches 20 minutes.
52const 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        /// The room containing this timeline.
70        #[property(get, set = Self::set_room, construct_only)]
71        room: OnceCell<Room>,
72        /// The underlying SDK timeline.
73        matrix_timeline: OnceCell<Arc<SdkTimeline>>,
74        /// Items added at the start of the timeline.
75        ///
76        /// Currently this can only contain one item at a time.
77        start_items: OnceCell<SingleItemListModel>,
78        /// Items provided by the SDK timeline.
79        sdk_items: OnceCell<gio::ListStore>,
80        /// Filter for the list of items provided by the SDK timeline.
81        filter: gtk::CustomFilter,
82        /// Filtered list of items provided by the SDK timeline.
83        filtered_sdk_items: gtk::FilterListModel,
84        /// Items added at the end of the timeline.
85        ///
86        /// Currently this can only contain one item at a time.
87        end_items: OnceCell<SingleItemListModel>,
88        /// The `GListModel` containing all the timeline items.
89        #[property(get = Self::items)]
90        items: OnceCell<gtk::FlattenListModel>,
91        /// A Hashmap linking a `TimelineEventItemId` to the corresponding
92        /// `Event`.
93        pub(super) event_map: RefCell<HashMap<TimelineEventItemId, Event>>,
94        /// The loading state of the timeline.
95        #[property(get, builder(LoadingState::default()))]
96        state: Cell<LoadingState>,
97        /// Whether we are loading events at the start of the timeline.
98        #[property(get)]
99        is_loading_start: Cell<bool>,
100        /// Whether the timeline is empty.
101        #[property(get = Self::is_empty)]
102        is_empty: PhantomData<bool>,
103        /// Whether the timeline should be pre-loaded when it is ready.
104        #[property(get, set = Self::set_preload, explicit_notify)]
105        preload: Cell<bool>,
106        /// Whether we have reached the start of the timeline.
107        #[property(get)]
108        has_reached_start: Cell<bool>,
109        /// Whether we have the `m.room.create` event in the timeline.
110        #[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                    // Hide the timeline start item if we have the `m.room.create` event too.
141                    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        /// Set the room containing this timeline.
165        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        /// The room containing this timeline.
180        fn room(&self) -> &Room {
181            self.room.get().expect("room should be initialized")
182        }
183
184        /// Initialize the underlying SDK timeline.
185        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        /// The underlying SDK timeline.
262        pub(super) fn matrix_timeline(&self) -> &Arc<SdkTimeline> {
263            self.matrix_timeline
264                .get()
265                .expect("matrix timeline should be initialized")
266        }
267
268        /// Items added at the start of the timeline.
269        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        /// Items provided by the SDK timeline.
278        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        /// Items added at the end of the timeline.
287        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        /// The `GListModel` containing all the timeline items.
296        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        /// Whether the timeline is empty.
309        fn is_empty(&self) -> bool {
310            self.filtered_sdk_items.n_items() == 0
311        }
312
313        /// Set the loading state of the timeline.
314        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        /// Update the loading state of the timeline.
325        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        /// Set whether we are loading events at the start of the timeline.
336        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        /// Set whether we have reached the start of the timeline.
349        fn set_has_reached_start(&self, has_reached_start: bool) {
350            if self.has_reached_start.get() == has_reached_start {
351                // Nothing to do.
352                return;
353            }
354
355            self.has_reached_start.set(has_reached_start);
356
357            self.obj().notify_has_reached_start();
358        }
359
360        /// Set whether the timeline has the `m.room.create` event of the room.
361        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        /// Clear the state of the timeline.
379        ///
380        /// This doesn't handle removing items in `sdk_items` because it can be
381        /// optimized by the caller of the function.
382        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        /// Set whether the timeline should be pre-loaded when it is ready.
389        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        /// Preload the timeline, if there are not enough items.
412        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        /// Update this timeline with the given diff list.
419        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                // The diff could not be minimized, handle it manually.
428                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        /// Attempt to minimize the given list of diffs.
446        ///
447        /// This is necessary because the SDK diffs are not always optimized,
448        /// e.g. an item is removed then re-added, which creates jumps in the
449        /// room history.
450        ///
451        /// Returns the list of diffs if it could not be minimized.
452        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        /// Update this timeline with the given diff.
466        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                        // This is the same item, update it.
506                        self.update_item(&item, &value);
507                        // The header visibility might have changed.
508                        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                    // Reset the state.
524                    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        /// Get the item at the given position.
538        fn item_at(&self, pos: u32) -> Option<TimelineItem> {
539            self.sdk_items().item(pos).and_downcast()
540        }
541
542        /// Update the items at the given position by removing the given number
543        /// of items and adding the given items.
544        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                    // This should not happen.
548                    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            // Update the header visibility of all the new additions, and the first item
558            // after this batch.
559            self.update_items_headers(pos, additions.len() as u32);
560
561            // Try to update the latest unread message.
562            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        /// Update the headers of the item at the given position and the given
570        /// number of items after it.
571        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            // Update the headers of changed events plus the first event after them.
586            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                    // The sender is different, show the full header.
609                    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                    // Too much time has passed, show the timestamp.
615                    EventHeaderState::TimestampOnly
616                } else {
617                    // Do not show header.
618                    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        /// Remove the given item from this `Timeline`.
628        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                // We need to remove both the transaction ID and the event ID.
634                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                    // We check if we are removing the right event, in case we receive a diff that
642                    // adds an existing event to another place, making us create a new event, before
643                    // another diff that removes it from its old place, making us remove the old
644                    // event.
645                    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        /// Whether we can load more events at the start of the timeline with
660        /// the current state.
661        pub(super) fn can_paginate_backwards(&self) -> bool {
662            // We do not want to load twice at the same time, and it's useless to try to
663            // load more history before the timeline is ready or if we have
664            // reached the start of the timeline.
665            self.state.get() != LoadingState::Initial
666                && !self.is_loading_start.get()
667                && !self.has_reached_start.get()
668        }
669
670        /// Load more events at the start of the timeline until the given
671        /// function tells us to stop.
672        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        /// Load more events at the start of the timeline.
692        ///
693        /// Returns `true` if more events can be loaded.
694        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        /// Add the typing row to the timeline, if it isn't present already.
718        fn add_typing_row(&self) {
719            self.end_items().set_is_hidden(false);
720        }
721
722        /// Remove the typing row from the timeline.
723        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        /// Listen to read receipts changes.
732        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                // Keep track of the activity of the sender.
792                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                // Update the identifier in the event map, in case we switched from a
812                // transaction ID to an event ID.
813                self.event_map
814                    .borrow_mut()
815                    .insert(event.identifier(), event.clone());
816
817                // Try to update the latest unread message.
818                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    /// The default log filter initialized with the `RUST_LOG` environment
837    /// variable.
838    ///
839    /// Used to know if we are likely to need to log the diff.
840    static IS_AT_TRACE_LEVEL: LazyLock<bool> = LazyLock::new(|| {
841        tracing_subscriber::EnvFilter::try_from_default_env()
842            // If the env variable is not set, we know that we are not at trace level.
843            .ok()
844            .and_then(|filter| filter.max_level_hint())
845            .is_some_and(|max| max == tracing::level_filters::LevelFilter::TRACE)
846    });
847
848    /// Temporary methods to debug items in the timeline.
849    impl Timeline {
850        /// Log the given diff list.
851        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        /// Log the items in this timeline.
891        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    // Helper methods for logging items.
906    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    /// All loaded items in a room.
936    ///
937    /// There is no strict message ordering enforced by the Timeline; items
938    /// will be appended/prepended to existing items in the order they are
939    /// received by the server.
940    pub struct Timeline(ObjectSubclass<imp::Timeline>);
941}
942
943impl Timeline {
944    /// Construct a new `Timeline` for the given room.
945    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    /// The underlying SDK timeline.
963    pub(crate) fn matrix_timeline(&self) -> Arc<SdkTimeline> {
964        self.imp().matrix_timeline().clone()
965    }
966
967    /// Load more events at the start of the timeline until the given function
968    /// tells us to stop.
969    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    /// Get the event with the given identifier from this `Timeline`.
983    ///
984    /// Use this method if you are sure the event has already been received.
985    /// Otherwise use `fetch_event_by_id`.
986    pub(crate) fn event_by_identifier(&self, identifier: &TimelineEventItemId) -> Option<Event> {
987        self.imp().event_map.borrow().get(identifier).cloned()
988    }
989
990    /// Get the position of the event with the given identifier in this
991    /// `Timeline`.
992    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    /// Remove the typing row from the timeline.
1005    pub(crate) fn remove_empty_typing_row(&self) {
1006        self.imp().remove_empty_typing_row();
1007    }
1008
1009    /// Whether this timeline has unread messages.
1010    ///
1011    /// Returns `None` if it is not possible to know, for example if there are
1012    /// no events in the Timeline.
1013    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                // The event is the oldest one, we have read it all.
1036                return Some(false);
1037            }
1038            if event.counts_as_unread() {
1039                // There is at least one unread event.
1040                return Some(true);
1041            }
1042        }
1043
1044        // This should only happen if we do not have a read receipt item in the
1045        // timeline, and there are not enough events in the timeline to know if there
1046        // are unread messages.
1047        None
1048    }
1049
1050    /// The IDs of redactable events sent by the given user in this timeline.
1051    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                // The iterator is broken.
1057                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    /// Emit the trigger that a read change might have occurred.
1078    fn emit_read_change_trigger(&self) {
1079        self.emit_by_name::<()>("read-change-trigger", &[]);
1080    }
1081
1082    /// Connect to the trigger emitted when a read change might have occurred.
1083    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
1097/// Whether the given event should be shown in the timeline.
1098fn show_in_timeline(any: &AnySyncTimelineEvent, rules: &RoomVersionRules) -> bool {
1099    // Make sure we do not show events that cannot be shown.
1100    if !default_event_filter(any, rules) {
1101        return false;
1102    }
1103
1104    // Only show events we want.
1105    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            // Do not show member events if the content that we support has not
1129            // changed. This avoids duplicate "user has joined" events in the
1130            // timeline which are confusing and wrong.
1131            !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}