Browse Source

timeline: Load previous events

This loads older events when the user scrolles to close to the upper edge
of the room-history.
merge-requests/1327/merge
Julian Sparber 5 years ago
parent
commit
090380bf44
  1. 13
      src/session/content/item_row.rs
  2. 38
      src/session/content/room_history.rs
  3. 6
      src/session/room/item.rs
  4. 17
      src/session/room/mod.rs
  5. 176
      src/session/room/timeline.rs

13
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);

38
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();
}
}
}

6
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))
}

17
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(

176
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);
}),
);
}
}

Loading…
Cancel
Save