room-list: Persist Room metainfo between restarts

Allows to restore the RoomList in the exact same state, even without
waiting for the rooms timelines to be loaded
This commit is contained in:
Kévin Commaille 2023-10-22 11:47:44 +02:00
parent 350f5164ae
commit f40692f975
No known key found for this signature in database
GPG key ID: 29A48C1F03620416
5 changed files with 303 additions and 30 deletions

View file

@ -29,7 +29,7 @@ src/session/model/session.rs
src/session/model/room/member.rs
src/session/model/room/member_role.rs
src/session/model/room/mod.rs
src/session/model/room_list.rs
src/session/model/room_list/mod.rs
src/session/model/sidebar/category/category_type.rs
src/session/model/sidebar/entry/entry_type.rs
src/session/view/account_settings/devices_page/device_list.rs

View file

@ -46,8 +46,8 @@ pub use self::{
typing_list::TypingList,
};
use super::{
AvatarData, AvatarImage, AvatarUriSource, IdentityVerification, Session, SidebarItem,
SidebarItemImpl, User,
room_list::RoomMetainfo, AvatarData, AvatarImage, AvatarUriSource, IdentityVerification,
Session, SidebarItem, SidebarItemImpl, User,
};
use crate::{components::Pill, gettext_f, prelude::*, spawn, spawn_tokio};
@ -284,11 +284,24 @@ glib::wrapper! {
}
impl Room {
pub fn new(session: &Session, room_id: &RoomId) -> Self {
glib::Object::builder()
pub fn new(session: &Session, room_id: &RoomId, metainfo: Option<&RoomMetainfo>) -> Self {
let this = glib::Object::builder::<Self>()
.property("session", session)
.property("room-id", &room_id.to_string())
.build()
.build();
if let Some(&RoomMetainfo {
latest_activity,
is_read,
}) = metainfo
{
this.set_latest_activity(latest_activity);
this.set_is_read(is_read);
this.update_highlight();
}
this
}
/// The current session.

View file

@ -11,6 +11,10 @@ use matrix_sdk::{
};
use tracing::error;
mod room_list_metainfo;
use self::room_list_metainfo::RoomListMetainfo;
pub use self::room_list_metainfo::RoomMetainfo;
use crate::{
gettext_f,
session::model::{Room, Session},
@ -27,10 +31,20 @@ mod imp {
#[derive(Debug, Default)]
pub struct RoomList {
/// The list of rooms.
pub list: RefCell<IndexMap<OwnedRoomId, Room>>,
/// The list of rooms we are currently joining.
pub pending_rooms: RefCell<HashSet<OwnedRoomOrAliasId>>,
/// The list of rooms that were upgraded and for which we haven't joined
/// the successor yet.
pub tombstoned_rooms: RefCell<HashSet<OwnedRoomId>>,
pub session: WeakRef<Session>,
/// The rooms metainfo that allow to restore the RoomList in its
/// previous state.
///
/// This is in a Mutex because updating the data in the store is async
/// and we don't want to overwrite newer data with older data.
pub metainfo: RoomListMetainfo,
}
#[glib::object_subclass]
@ -53,7 +67,7 @@ mod imp {
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"session" => self.session.set(value.get().ok().as_ref()),
"session" => self.obj().set_session(value.get().ok().as_ref()),
_ => unimplemented!(),
}
}
@ -70,15 +84,22 @@ mod imp {
Lazy::new(|| vec![Signal::builder("pending-rooms-changed").build()]);
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
self.metainfo.set_room_list(&self.obj());
}
}
impl ListModelImpl for RoomList {
fn item_type(&self) -> glib::Type {
Room::static_type()
}
fn n_items(&self) -> u32 {
self.list.borrow().len() as u32
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.list
.borrow()
@ -107,11 +128,21 @@ impl RoomList {
glib::Object::builder().property("session", session).build()
}
/// The current session.
/// The ancestor session.
pub fn session(&self) -> Session {
self.imp().session.upgrade().unwrap()
}
/// Set the ancestor session.
fn set_session(&self, session: Option<&Session>) {
let Some(session) = session else {
return;
};
let imp = self.imp();
imp.session.set(Some(session));
}
pub fn is_pending_room(&self, identifier: &RoomOrAliasId) -> bool {
self.imp().pending_rooms.borrow().contains(identifier)
}
@ -241,28 +272,18 @@ impl RoomList {
///
/// Note that the `Store` currently doesn't store all events, therefore, we
/// aren't really loading much via this function.
pub fn load(&self) {
let session = self.session();
let client = session.client();
let matrix_rooms = client.rooms();
let added = matrix_rooms.len();
pub async fn load(&self) {
let imp = self.imp();
if added > 0 {
{
let mut added = Vec::with_capacity(matrix_rooms.len());
for matrix_room in matrix_rooms {
let room_id = matrix_room.room_id().to_owned();
let room = Room::new(&session, &room_id);
added.push((room_id, room));
}
self.imp().list.borrow_mut().extend(added);
}
let rooms = imp.metainfo.load_rooms().await;
let added = rooms.len();
imp.list.borrow_mut().extend(rooms);
self.items_added(added);
}
self.items_added(added);
}
pub fn handle_response_rooms(&self, rooms: ResponseRooms) {
let imp = self.imp();
let session = self.session();
let mut new_rooms = HashMap::new();
@ -272,7 +293,7 @@ impl RoomList {
Some(room) => room,
None => new_rooms
.entry(room_id.clone())
.or_insert_with_key(|room_id| Room::new(&session, room_id))
.or_insert_with_key(|room_id| Room::new(&session, room_id, None))
.clone(),
};
@ -286,11 +307,12 @@ impl RoomList {
Some(room) => room,
None => new_rooms
.entry(room_id.clone())
.or_insert_with_key(|room_id| Room::new(&session, room_id))
.or_insert_with_key(|room_id| Room::new(&session, room_id, None))
.clone(),
};
self.pending_rooms_remove((*room_id).into());
imp.metainfo.watch_room(&room);
room.update_room();
room.handle_joined_response(joined_room);
}
@ -300,17 +322,18 @@ impl RoomList {
Some(room) => room,
None => new_rooms
.entry(room_id.clone())
.or_insert_with_key(|room_id| Room::new(&session, room_id))
.or_insert_with_key(|room_id| Room::new(&session, room_id, None))
.clone(),
};
self.pending_rooms_remove((*room_id).into());
imp.metainfo.watch_room(&room);
room.update_room();
}
if !new_rooms.is_empty() {
let added = new_rooms.len();
self.imp().list.borrow_mut().extend(new_rooms);
imp.list.borrow_mut().extend(new_rooms);
self.items_added(added);
}
}

View file

@ -0,0 +1,237 @@
use std::{
cell::RefCell,
collections::{BTreeMap, HashSet},
ops::Deref,
rc::Rc,
};
use futures_util::lock::Mutex;
use gtk::{glib, prelude::*};
use indexmap::IndexMap;
use ruma::OwnedRoomId;
use serde::{Deserialize, Serialize};
use tracing::error;
use super::RoomList;
use crate::{session::model::Room, spawn, spawn_tokio};
const ROOMS_METAINFO_KEY: &str = "rooms_metainfo";
/// The rooms metainfo that allow to restore the RoomList in its previous state.
#[derive(Debug, Default, Clone)]
pub struct RoomListMetainfo(Rc<RoomListMetainfoInner>);
impl RoomListMetainfo {
/// Set the parent `RoomList`.
pub fn set_room_list(&self, room_list: &RoomList) {
self.room_list.set(Some(room_list));
}
/// Load the rooms and their metainfo from the store.
pub async fn load_rooms(&self) -> IndexMap<OwnedRoomId, Room> {
let session = self.room_list().session();
let client = session.client();
let room_ids = client
.rooms()
.into_iter()
.map(|room| room.room_id().to_owned())
.collect::<HashSet<_>>();
// Load the serialized map from the store.
let handle = spawn_tokio!(async move {
client
.store()
.get_custom_value(ROOMS_METAINFO_KEY.as_bytes())
.await
});
let mut rooms_metainfo: RoomsMetainfoMap = match handle.await.unwrap() {
Ok(Some(value)) => match serde_json::from_slice(&value) {
Ok(metainfo) => metainfo,
Err(error) => {
error!("Failed to deserialize rooms metainfo: {error}");
Default::default()
}
},
Ok(None) => Default::default(),
Err(error) => {
error!("Failed to load rooms metainfo: {error}");
Default::default()
}
};
// Remove unknown rooms.
rooms_metainfo.retain(|room_id, _| room_ids.contains(room_id));
// We need to acquire the lock now to make sure we have the full map before any
// change happens and the map tries to be persisted.
let mut rooms_metainfo_guard = self.rooms_metainfo.lock().await;
// Restore rooms and listen to changes.
let mut rooms = IndexMap::with_capacity(room_ids.len());
for room_id in room_ids {
let metainfo = rooms_metainfo.get(&room_id);
let room = Room::new(&session, &room_id, metainfo);
self.watch_room(&room);
rooms.insert(room_id, room);
}
*rooms_metainfo_guard = rooms_metainfo;
rooms
}
/// Watch the given room for metainfo changes.
pub fn watch_room(&self, room: &Room) {
let inner_weak = std::rc::Rc::<RoomListMetainfoInner>::downgrade(&self.0);
room.connect_notify_local(None, move |room, param_spec| {
if !matches!(param_spec.name(), "latest-activity" | "is-read") {
return;
}
let inner_weak = inner_weak.clone();
let room_id = room.room_id().to_owned();
spawn!(async move {
let Some(inner) = inner_weak.upgrade() else {
return;
};
inner.update_rooms_metainfo_for_room(room_id).await;
});
});
}
}
impl Deref for RoomListMetainfo {
type Target = RoomListMetainfoInner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type RoomsMetainfoMap = BTreeMap<OwnedRoomId, RoomMetainfo>;
#[derive(Debug, Default)]
pub struct RoomListMetainfoInner {
/// The rooms metainfos.
///
/// This is in a Mutex because persisting the data in the store is async and
/// we only want one operation at a time.
rooms_metainfo: Mutex<RoomsMetainfoMap>,
/// Set of room IDs for which the metainfo should be updated.
///
/// This list is kept to avoid queuing the same room several times in a row
/// while we wait for the async operation to finish.
pending_rooms_metainfo_updates: RefCell<HashSet<OwnedRoomId>>,
/// The parent RoomList|.
room_list: glib::WeakRef<RoomList>,
}
impl RoomListMetainfoInner {
/// The parent `RoomList`.
fn room_list(&self) -> RoomList {
self.room_list.upgrade().unwrap()
}
/// Persist the metainfo in the store.
async fn persist(&self, rooms_metainfo: &RoomsMetainfoMap) {
let value = match serde_json::to_vec(rooms_metainfo) {
Ok(value) => value,
Err(error) => {
error!("Failed to serialize rooms metainfo: {error}");
return;
}
};
let client = self.room_list().session().client();
let handle = spawn_tokio!(async move {
client
.store()
.set_custom_value(ROOMS_METAINFO_KEY.as_bytes(), value)
.await
});
if let Err(error) = handle.await.unwrap() {
error!("Failed to store rooms metainfo: {error}");
}
}
/// Update the room metainfo for the room with the given ID.
async fn update_rooms_metainfo_for_room(&self, room_id: OwnedRoomId) {
self.pending_rooms_metainfo_updates
.borrow_mut()
.insert(room_id);
while !self.pending_rooms_metainfo_updates.borrow().is_empty() {
if !self.try_update_rooms_metainfo().await {
return;
}
}
}
/// Update the rooms metainfo if a lock can be acquired.
///
/// Returns `true` if the lock could be acquired.
async fn try_update_rooms_metainfo(&self) -> bool {
let Some(mut rooms_metainfo_guard) = self.rooms_metainfo.try_lock() else {
return false;
};
let room_ids = self.pending_rooms_metainfo_updates.take();
if room_ids.is_empty() {
return true;
}
let room_list = self.room_list();
let mut has_changed = false;
for (room, room_id) in room_ids
.into_iter()
.filter_map(|room_id| room_list.get(&room_id).map(|room| (room, room_id)))
{
let metainfo = rooms_metainfo_guard.entry(room_id).or_default();
has_changed |= metainfo.update(&room);
}
if has_changed {
self.persist(&rooms_metainfo_guard).await;
}
true
}
}
/// The room metainfo that needs to be persisted in the state store .
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub struct RoomMetainfo {
pub latest_activity: u64,
pub is_read: bool,
}
impl RoomMetainfo {
/// Update this `RoomMetainfo` for the given `Room`.
///
/// Returns `true` if the data was updated.
fn update(&mut self, room: &Room) -> bool {
let mut has_changed = false;
let latest_activity = room.latest_activity();
if self.latest_activity != latest_activity {
self.latest_activity = latest_activity;
has_changed = true;
}
let is_read = room.is_read();
if self.is_read != is_read {
self.is_read = is_read;
has_changed = true;
}
has_changed
}
}

View file

@ -236,7 +236,7 @@ impl Session {
self.update_user_profile();
self.update_offline().await;
self.room_list().load();
self.room_list().load().await;
self.setup_direct_room_handler();
self.setup_room_encrypted_changes();