From f40692f975a78f17e7da60a3a95467035ab5363d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 22 Oct 2023 11:47:44 +0200 Subject: [PATCH] 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 --- po/POTFILES.in | 2 +- src/session/model/room/mod.rs | 23 +- .../model/{room_list.rs => room_list/mod.rs} | 69 +++-- .../model/room_list/room_list_metainfo.rs | 237 ++++++++++++++++++ src/session/model/session.rs | 2 +- 5 files changed, 303 insertions(+), 30 deletions(-) rename src/session/model/{room_list.rs => room_list/mod.rs} (89%) create mode 100644 src/session/model/room_list/room_list_metainfo.rs diff --git a/po/POTFILES.in b/po/POTFILES.in index 2d126bd8..651faad5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index d71087e2..b0371e5c 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -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::() .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. diff --git a/src/session/model/room_list.rs b/src/session/model/room_list/mod.rs similarity index 89% rename from src/session/model/room_list.rs rename to src/session/model/room_list/mod.rs index a6d97fa0..8b457c67 100644 --- a/src/session/model/room_list.rs +++ b/src/session/model/room_list/mod.rs @@ -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>, + /// The list of rooms we are currently joining. pub pending_rooms: RefCell>, + /// The list of rooms that were upgraded and for which we haven't joined + /// the successor yet. pub tombstoned_rooms: RefCell>, pub session: WeakRef, + /// 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 { 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); } } diff --git a/src/session/model/room_list/room_list_metainfo.rs b/src/session/model/room_list/room_list_metainfo.rs new file mode 100644 index 00000000..580bb0b2 --- /dev/null +++ b/src/session/model/room_list/room_list_metainfo.rs @@ -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); + +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 { + 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::>(); + + // 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::::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; + +#[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, + /// 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>, + /// The parent RoomList|. + room_list: glib::WeakRef, +} + +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 + } +} diff --git a/src/session/model/session.rs b/src/session/model/session.rs index fc83c1c3..d2c8eca0 100644 --- a/src/session/model/session.rs +++ b/src/session/model/session.rs @@ -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();