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        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
41/// The number of events to request when loading more history.
42const 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        /// The room containing this timeline.
60        #[property(get, set = Self::set_room, construct_only)]
61        room: OnceCell<Room>,
62        /// The underlying SDK timeline.
63        matrix_timeline: OnceCell<Arc<SdkTimeline>>,
64        /// Items added at the start of the timeline.
65        ///
66        /// Currently this can only contain one item at a time.
67        start_items: gio::ListStore,
68        /// Items provided by the SDK timeline.
69        pub(super) sdk_items: gio::ListStore,
70        /// Items added at the end of the timeline.
71        end_items: gio::ListStore,
72        /// The `GListModel` containing all the timeline items.
73        #[property(get)]
74        items: gtk::FlattenListModel,
75        /// A Hashmap linking a `TimelineEventItemId` to the corresponding
76        /// `Event`.
77        pub(super) event_map: RefCell<HashMap<TimelineEventItemId, Event>>,
78        /// The loading state of the timeline.
79        #[property(get, builder(LoadingState::default()))]
80        state: Cell<LoadingState>,
81        /// Whether we are loading events at the start of the timeline.
82        #[property(get)]
83        is_loading_start: Cell<bool>,
84        /// Whether the timeline is empty.
85        #[property(get = Self::is_empty)]
86        is_empty: PhantomData<bool>,
87        /// Whether the timeline should be pre-loaded when it is ready.
88        #[property(get, set = Self::set_preload, explicit_notify)]
89        preload: Cell<bool>,
90        /// Whether we have reached the start of the timeline.
91        #[property(get)]
92        has_reached_start: Cell<bool>,
93        /// Whether the timeline has the `m.room.create` event of the room.
94        #[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        /// Set the room containing this timeline.
162        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        /// The room containing this timeline.
177        fn room(&self) -> &Room {
178            self.room.get().expect("room should be initialized")
179        }
180
181        /// Initialize the underlying SDK timeline.
182        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        /// The underlying SDK timeline.
259        pub(super) fn matrix_timeline(&self) -> &Arc<SdkTimeline> {
260            self.matrix_timeline
261                .get()
262                .expect("matrix timeline is initialized")
263        }
264
265        /// Whether the timeline is empty.
266        fn is_empty(&self) -> bool {
267            self.sdk_items.n_items() == 0
268        }
269
270        /// Set the loading state of the timeline.
271        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        /// Update the loading state of the timeline.
282        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        /// Set whether we are loading events at the start of the timeline.
293        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        /// Set whether we have reached the start of the timeline.
306        fn set_has_reached_start(&self, has_reached_start: bool) {
307            if self.has_reached_start.get() == has_reached_start {
308                // Nothing to do.
309                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        /// Set whether the timeline has the `m.room.create` event of the room.
319        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        /// Update the virtual items at the start of the timeline.
331        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        /// Clear the state of the timeline.
368        ///
369        /// This doesn't handle removing items in `sdk_items` because it can be
370        /// optimized by the caller of the function.
371        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        /// Set whether the timeline should be pre-loaded when it is ready.
378        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        /// Preload the timeline, if there are not enough items.
401        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        /// Update this timeline with the given diff list.
408        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                // The diff could not be minimized, handle it manually.
417                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        /// Attempt to minimize the given list of diffs.
435        ///
436        /// This is necessary because the SDK diffs are not always optimized,
437        /// e.g. an item is removed then re-added, which creates jumps in the
438        /// room history.
439        ///
440        /// Returns the list of diffs if it could not be minimized.
441        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        /// Update this timeline with the given diff.
455        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                        // This is the same item, update it.
495                        self.update_item(&item, &value);
496                        // The header visibility might have changed.
497                        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                    // Reset the state.
513                    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        /// Get the item at the given position.
527        fn item_at(&self, pos: u32) -> Option<TimelineItem> {
528            self.sdk_items.item(pos).and_downcast()
529        }
530
531        /// Update the items at the given position by removing the given number
532        /// of items and adding the given items.
533        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                    // This should not happen.
537                    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            // Update the header visibility of all the new additions, and the first item
547            // after this batch.
548            self.update_items_headers(pos, additions.len() as u32);
549
550            // Try to update the latest unread message.
551            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        /// Update the headers of the item at the given position and the given
559        /// number of items after it.
560        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            // Update the headers of changed events plus the first event after them.
574            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        /// Remove the given item from this `Timeline`.
594        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                // We need to remove both the transaction ID and the event ID.
600                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                    // We check if we are removing the right event, in case we receive a diff that
608                    // adds an existing event to another place, making us create a new event, before
609                    // another diff that removes it from its old place, making
610                    // us remove the old event.
611                    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        /// Whether we can load more events at the start of the timeline with
626        /// the current state.
627        pub(super) fn can_paginate_backwards(&self) -> bool {
628            // We do not want to load twice at the same time, and it's useless to try to
629            // load more history before the timeline is ready or if we have
630            // reached the start of the timeline.
631            self.state.get() != LoadingState::Initial
632                && !self.is_loading_start.get()
633                && !self.has_reached_start.get()
634        }
635
636        /// Load more events at the start of the timeline until the given
637        /// function tells us to stop.
638        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        /// Load more events at the start of the timeline.
658        ///
659        /// Returns `true` if more events can be loaded.
660        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        /// Whether the timeline has a typing row.
684        fn has_typing_row(&self) -> bool {
685            self.end_items.n_items() > 0
686        }
687
688        /// Add the typing row to the timeline, if it isn't present already.
689        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        /// Remove the typing row from the timeline.
698        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        /// Listen to read receipts changes.
707        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                // Keep track of the activity of the sender.
767                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                // Update the identifier in the event map, in case we switched from a
787                // transaction ID to an event ID.
788                self.event_map
789                    .borrow_mut()
790                    .insert(event.identifier(), event.clone());
791
792                // Try to update the latest unread message.
793                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    /// The default log filter initialized with the `RUST_LOG` environment
812    /// variable.
813    ///
814    /// Used to know if we are likely to need to log the diff.
815    static IS_AT_TRACE_LEVEL: LazyLock<bool> = LazyLock::new(|| {
816        tracing_subscriber::EnvFilter::try_from_default_env()
817            // If the env variable is not set, we know that we are not at trace level.
818            .ok()
819            .and_then(|filter| filter.max_level_hint())
820            .is_some_and(|max| max == tracing::level_filters::LevelFilter::TRACE)
821    });
822
823    /// Temporary methods to debug items in the timeline.
824    impl Timeline {
825        /// Log the given diff list.
826        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        /// Log the items in this timeline.
866        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    // Helper methods for logging items.
881    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    /// All loaded items in a room.
911    ///
912    /// There is no strict message ordering enforced by the Timeline; items
913    /// will be appended/prepended to existing items in the order they are
914    /// received by the server.
915    pub struct Timeline(ObjectSubclass<imp::Timeline>);
916}
917
918impl Timeline {
919    /// Construct a new `Timeline` for the given room.
920    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    /// The underlying SDK timeline.
938    pub(crate) fn matrix_timeline(&self) -> Arc<SdkTimeline> {
939        self.imp().matrix_timeline().clone()
940    }
941
942    /// Load more events at the start of the timeline until the given function
943    /// tells us to stop.
944    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    /// Get the event with the given identifier from this `Timeline`.
958    ///
959    /// Use this method if you are sure the event has already been received.
960    /// Otherwise use `fetch_event_by_id`.
961    pub(crate) fn event_by_identifier(&self, identifier: &TimelineEventItemId) -> Option<Event> {
962        self.imp().event_map.borrow().get(identifier).cloned()
963    }
964
965    /// Get the position of the event with the given identifier in this
966    /// `Timeline`.
967    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    /// Remove the typing row from the timeline.
989    pub(crate) fn remove_empty_typing_row(&self) {
990        self.imp().remove_empty_typing_row();
991    }
992
993    /// Whether this timeline has unread messages.
994    ///
995    /// Returns `None` if it is not possible to know, for example if there are
996    /// no events in the Timeline.
997    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                // The event is the oldest one, we have read it all.
1020                return Some(false);
1021            }
1022            if event.counts_as_unread() {
1023                // There is at least one unread event.
1024                return Some(true);
1025            }
1026        }
1027
1028        // This should only happen if we do not have a read receipt item in the
1029        // timeline, and there are not enough events in the timeline to know if there
1030        // are unread messages.
1031        None
1032    }
1033
1034    /// The IDs of redactable events sent by the given user in this timeline.
1035    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                // The iterator is broken.
1041                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    /// Emit the trigger that a read change might have occurred.
1062    fn emit_read_change_trigger(&self) {
1063        self.emit_by_name::<()>("read-change-trigger", &[]);
1064    }
1065
1066    /// Connect to the trigger emitted when a read change might have occurred.
1067    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
1081/// Whether the given event should be shown in the timeline.
1082fn show_in_timeline(any: &AnySyncTimelineEvent, room_version: &RoomVersionId) -> bool {
1083    // Make sure we do not show events that cannot be shown.
1084    if !default_event_filter(any, room_version) {
1085        return false;
1086    }
1087
1088    // Only show events we want.
1089    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            // Do not show member events if the content that we support has not
1113            // changed. This avoids duplicate "user has joined" events in the
1114            // timeline which are confusing and wrong.
1115            !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}