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