[fractal/fractal-next] timeline: Load previous events



commit 090380bf44a1a4f674ef36d87417e1f5b89e7416
Author: Julian Sparber <julian sparber net>
Date:   Mon Sep 6 18:42:03 2021 +0200

    timeline: Load previous events
    
    This loads older events when the user scrolles to close to the upper edge
    of the room-history.

 src/session/content/item_row.rs     |  13 +++
 src/session/content/room_history.rs |  38 ++++++--
 src/session/room/item.rs            |   6 ++
 src/session/room/mod.rs             |  17 ----
 src/session/room/timeline.rs        | 176 ++++++++++++++++++++++++++++++++++--
 5 files changed, 220 insertions(+), 30 deletions(-)
---
diff --git a/src/session/content/item_row.rs b/src/session/content/item_row.rs
index 9b1b5c8b..a7fface0 100644
--- a/src/session/content/item_row.rs
+++ b/src/session/content/item_row.rs
@@ -196,6 +196,19 @@ impl ItemRow {
                         self.set_child(Some(&child));
                     };
                 }
+                ItemType::LoadingSpinner => {
+                    if !self
+                        .child()
+                        .map_or(false, |widget| widget.is::<gtk::Spinner>())
+                    {
+                        let spinner = gtk::SpinnerBuilder::new()
+                            .spinning(true)
+                            .margin_top(12)
+                            .margin_bottom(12)
+                            .build();
+                        self.set_child(Some(&spinner));
+                    }
+                }
             }
         }
         priv_.item.replace(item);
diff --git a/src/session/content/room_history.rs b/src/session/content/room_history.rs
index af972145..1ef7785a 100644
--- a/src/session/content/room_history.rs
+++ b/src/session/content/room_history.rs
@@ -22,6 +22,7 @@ mod imp {
         pub room: RefCell<Option<Room>>,
         pub category_handler: RefCell<Option<SignalHandlerId>>,
         pub empty_timeline_handler: RefCell<Option<SignalHandlerId>>,
+        pub loading_timeline_handler: RefCell<Option<SignalHandlerId>>,
         pub md_enabled: Cell<bool>,
         pub is_auto_scrolling: Cell<bool>,
         pub sticky: Cell<bool>,
@@ -189,13 +190,14 @@ mod imp {
                     }
                 } else {
                     obj.set_sticky(adj.value() + adj.page_size() == adj.upper());
-                    obj.load_more_messages(adj);
                 }
+                obj.load_more_messages(adj);
             }));
-            adj.connect_upper_notify(clone!(@weak obj => move |_| {
+            adj.connect_upper_notify(clone!(@weak obj => move |adj| {
                 if obj.sticky() {
                     obj.scroll_down();
                 }
+                obj.load_more_messages(adj);
             }));
 
             let key_events = gtk::EventControllerKey::new();
@@ -274,6 +276,12 @@ impl RoomHistory {
             }
         }
 
+        if let Some(loading_timeline_handler) = priv_.loading_timeline_handler.take() {
+            if let Some(room) = self.room() {
+                room.timeline().disconnect(loading_timeline_handler);
+            }
+        }
+
         if let Some(ref room) = room {
             let handler_id = room.connect_notify_local(
                 Some("category"),
@@ -292,6 +300,21 @@ impl RoomHistory {
             );
 
             priv_.empty_timeline_handler.replace(Some(handler_id));
+
+            let handler_id = room.timeline().connect_notify_local(
+                Some("loading"),
+                clone!(@weak self as obj => move |timeline, _| {
+                    // We need to make sure that we loaded enough events to fill the `ScrolledWindow`
+                    let priv_ = imp::RoomHistory::from_instance(&obj);
+                    if !timeline.loading() {
+                        let adj = priv_.listview.vadjustment().unwrap();
+                        obj.load_more_messages(&adj);
+                    }
+                }),
+            );
+
+            priv_.loading_timeline_handler.replace(Some(handler_id));
+
             room.load_members();
         }
 
@@ -361,7 +384,7 @@ impl RoomHistory {
         let priv_ = imp::RoomHistory::from_instance(self);
 
         if let Some(room) = &*priv_.room.borrow() {
-            if room.timeline().empty() {
+            if room.timeline().is_empty() {
                 priv_.stack.set_visible_child(&*priv_.loading);
             } else {
                 priv_.stack.set_visible_child(&*priv_.content);
@@ -372,9 +395,12 @@ impl RoomHistory {
     fn load_more_messages(&self, adj: &gtk::Adjustment) {
         // Load more messages when the user gets close to the end of the known room history
         // Use the page size twice to detect if the user gets close to the end
-        if adj.value() < adj.page_size() * 2.0 || adj.upper() <= adj.page_size() * 2.0 {
-            if let Some(room) = self.room() {
-                room.load_previous_events();
+        if let Some(room) = self.room() {
+            if adj.value() < adj.page_size() * 2.0
+                || adj.upper() <= adj.page_size() / 2.0
+                || room.timeline().is_empty()
+            {
+                room.timeline().load_previous_events();
             }
         }
     }
diff --git a/src/session/room/item.rs b/src/session/room/item.rs
index 69c125ef..cea294a9 100644
--- a/src/session/room/item.rs
+++ b/src/session/room/item.rs
@@ -13,6 +13,7 @@ pub enum ItemType {
     // TODO: Add item type for grouped events
     DayDivider(DateTime),
     NewMessageDivider,
+    LoadingSpinner,
 }
 
 #[derive(Clone, Debug, glib::GBoxed)]
@@ -133,6 +134,11 @@ impl Item {
         glib::Object::new(&[("type", &type_)]).expect("Failed to create Item")
     }
 
+    pub fn for_loading_spinner() -> Self {
+        let type_ = BoxedItemType(ItemType::LoadingSpinner);
+        glib::Object::new(&[("type", &type_)]).expect("Failed to create Item")
+    }
+
     pub fn selectable(&self) -> bool {
         matches!(self.type_(), ItemType::Event(_event))
     }
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index 7bf15cad..2e300119 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -761,23 +761,6 @@ impl Room {
         );
     }
 
-    pub fn load_previous_events(&self) {
-        warn!("Loading previous events is not yet implemented");
-        /*
-        let matrix_room = priv_.matrix_room.get().unwrap().clone();
-        do_async(
-            async move { matrix_room.messages().await },
-            clone!(@weak self as obj => move |events| async move {
-                // FIXME: We should retry to load the room members if the request failed
-                match events {
-                        Ok(events) => obj.prepend(events),
-                        Err(error) => error!("Couldn’t load room members: {}", error),
-                };
-            }),
-        );
-        */
-    }
-
     fn load_power_levels(&self) {
         let matrix_room = self.matrix_room();
         do_async(
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline.rs
index 83a3568a..f9d16f2c 100644
--- a/src/session/room/timeline.rs
+++ b/src/session/room/timeline.rs
@@ -1,12 +1,18 @@
-use gtk::{gio, glib, prelude::*, subclass::prelude::*};
-use matrix_sdk::ruma::identifiers::EventId;
+use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use log::error;
+use matrix_sdk::ruma::{
+    api::client::r0::message::get_message_events::Direction,
+    events::{AnySyncRoomEvent, AnySyncStateEvent},
+    identifiers::EventId,
+};
 
-use crate::session::room::{Event, Item, Room};
+use crate::session::room::{Event, Item, ItemType, Room};
+use crate::utils::do_async;
 
 mod imp {
     use super::*;
     use once_cell::sync::{Lazy, OnceCell};
-    use std::cell::RefCell;
+    use std::cell::{Cell, RefCell};
     use std::collections::{HashMap, VecDeque};
 
     #[derive(Debug, Default)]
@@ -20,6 +26,9 @@ mod imp {
         pub event_map: RefCell<HashMap<EventId, Event>>,
         /// Maps the temporary `EventId` of the pending Event to the real `EventId`
         pub pending_events: RefCell<HashMap<EventId, EventId>>,
+        pub loading: Cell<bool>,
+        pub complete: Cell<bool>,
+        pub oldest_event: RefCell<Option<EventId>>,
     }
 
     #[glib::object_subclass]
@@ -41,6 +50,13 @@ mod imp {
                         Room::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "loading",
+                        "Loading",
+                        "Whether a response is loaded or not",
+                        false,
+                        glib::ParamFlags::READABLE,
+                    ),
                     glib::ParamSpec::new_boolean(
                         "empty",
                         "Empty",
@@ -48,6 +64,13 @@ mod imp {
                         false,
                         glib::ParamFlags::READABLE,
                     ),
+                    glib::ParamSpec::new_boolean(
+                        "complete",
+                        "Complete",
+                        "Whether the full timeline is loaded",
+                        false,
+                        glib::ParamFlags::READABLE,
+                    ),
                 ]
             });
 
@@ -73,7 +96,9 @@ mod imp {
         fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
             match pspec.name() {
                 "room" => self.room.get().unwrap().to_value(),
-                "empty" => obj.empty().to_value(),
+                "loading" => obj.loading().to_value(),
+                "empty" => obj.is_empty().to_value(),
+                "complete" => obj.is_complete().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -119,6 +144,8 @@ impl Timeline {
     fn items_changed(&self, position: u32, removed: u32, added: u32) {
         let priv_ = imp::Timeline::from_instance(self);
 
+        let last_new_message_date;
+
         // Insert date divider, this needs to happen before updating the position and headers
         let added = {
             let position = position as usize;
@@ -145,6 +172,10 @@ impl Timeline {
             }
 
             let divider_len = divider.len();
+            last_new_message_date = divider.last().and_then(|item| match item.1.type_() {
+                ItemType::DayDivider(date) => Some(date.clone()),
+                _ => None,
+            });
             for (position, date) in divider {
                 list.insert(position, date);
             }
@@ -152,6 +183,24 @@ impl Timeline {
             (added + divider_len) as u32
         };
 
+        // Remove first day divider if a new one is added earlier with the same day
+        let removed = {
+            let mut list = priv_.list.borrow_mut();
+            if let Some(ItemType::DayDivider(date)) = list
+                .get(position as usize + added as usize)
+                .map(|item| item.type_())
+            {
+                if Some(date.ymd()) == last_new_message_date.as_ref().map(|date| date.ymd()) {
+                    list.remove(position as usize + added as usize);
+                    removed + 1
+                } else {
+                    removed
+                }
+            } else {
+                removed
+            }
+        };
+
         // Update the header for events that are allowed to hide the header
         {
             let position = position as usize;
@@ -283,6 +332,13 @@ impl Timeline {
                 let mut list = priv_.list.borrow_mut();
                 // Extend the size of the list so that rust doesn't need to reallocate memory multiple times
                 list.reserve(batch.len());
+
+                if list.is_empty() {
+                    priv_
+                        .oldest_event
+                        .replace(batch.first().as_ref().map(|event| event.matrix_event_id()));
+                }
+
                 list.len()
             };
 
@@ -368,6 +424,10 @@ impl Timeline {
         let priv_ = imp::Timeline::from_instance(self);
         let mut added = batch.len();
 
+        priv_
+            .oldest_event
+            .replace(batch.last().as_ref().map(|event| event.matrix_event_id()));
+
         {
             // Extend the size of the list so that rust doesn't need to reallocate memory multiple times
             priv_.list.borrow_mut().reserve(added);
@@ -400,8 +460,110 @@ impl Timeline {
         priv_.room.get().unwrap()
     }
 
-    pub fn empty(&self) -> bool {
+    fn set_loading(&self, loading: bool) {
+        let priv_ = imp::Timeline::from_instance(self);
+
+        if loading == priv_.loading.get() {
+            return;
+        }
+
+        priv_.loading.set(loading);
+
+        self.notify("loading");
+    }
+
+    fn set_complete(&self, complete: bool) {
+        let priv_ = imp::Timeline::from_instance(self);
+
+        if complete == priv_.complete.get() {
+            return;
+        }
+
+        priv_.complete.set(complete);
+        self.notify("complete");
+    }
+
+    // Wether the timeline is full loaded
+    pub fn is_complete(&self) -> bool {
+        let priv_ = imp::Timeline::from_instance(self);
+        priv_.complete.get()
+    }
+
+    pub fn loading(&self) -> bool {
+        let priv_ = imp::Timeline::from_instance(self);
+        priv_.loading.get()
+    }
+
+    pub fn is_empty(&self) -> bool {
         let priv_ = imp::Timeline::from_instance(self);
-        priv_.list.borrow().is_empty()
+        priv_.list.borrow().is_empty() || (priv_.list.borrow().len() == 1 && self.loading())
+    }
+
+    fn oldest_event(&self) -> Option<EventId> {
+        let priv_ = imp::Timeline::from_instance(self);
+        priv_.oldest_event.borrow().clone()
+    }
+    fn add_loading_spinner(&self) {
+        let priv_ = imp::Timeline::from_instance(self);
+        priv_
+            .list
+            .borrow_mut()
+            .push_front(Item::for_loading_spinner());
+        self.upcast_ref::<gio::ListModel>().items_changed(0, 0, 1);
+    }
+
+    fn remove_loading_spinner(&self) {
+        let priv_ = imp::Timeline::from_instance(self);
+        priv_.list.borrow_mut().pop_front();
+        self.upcast_ref::<gio::ListModel>().items_changed(0, 1, 0);
+    }
+
+    pub fn load_previous_events(&self) {
+        if self.loading() || self.is_complete() {
+            return;
+        }
+
+        self.set_loading(true);
+        self.add_loading_spinner();
+
+        let matrix_room = self.room().matrix_room();
+        let last_event = self.oldest_event();
+        let contains_last_event = last_event.is_some();
+
+        do_async(
+            glib::PRIORITY_LOW,
+            async move {
+                matrix_room
+                    .messages(last_event.as_ref(), None, 20, Direction::Backward)
+                    .await
+            },
+            clone!(@weak self as obj => move |events| async move {
+                obj.remove_loading_spinner();
+
+                // FIXME: If the request fails it's automatically restarted because the added events (none), 
didn't fill the screen.
+                // We should block the loading for some time before retrying
+                match events {
+                       Ok(Some(events)) => {
+                            let events: Vec<Event> = if contains_last_event {
+                                            events
+                                           .into_iter()
+                                           .skip(1)
+                                           .map(|event| Event::new(event, obj.room())).collect()
+                            } else {
+                                            events
+                                           .into_iter()
+                                           .map(|event| Event::new(event, obj.room())).collect()
+                            };
+                            obj.set_complete(events.iter().any(|event| matches!(event.matrix_event(), 
Some(AnySyncRoomEvent::State(AnySyncStateEvent::RoomCreate(_))))));
+                            obj.prepend(events)
+                       },
+                       Ok(None) => {
+                           error!("The start event wasn't found in the timeline for room {}.", 
obj.room().room_id());
+                       },
+                       Err(error) => error!("Couldn't load previous events for room {}: {}", error, 
obj.room().room_id()),
+               }
+               obj.set_loading(false);
+            }),
+        );
     }
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]