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:
parent
350f5164ae
commit
f40692f975
5 changed files with 303 additions and 30 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
237
src/session/model/room_list/room_list_metainfo.rs
Normal file
237
src/session/model/room_list/room_list_metainfo.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue