[fractal/fractal-next] timeline: Load previous events
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] timeline: Load previous events
- Date: Thu, 9 Sep 2021 14:26:10 +0000 (UTC)
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: >k::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]