fractal/session/model/room_list/
room_list_metainfo.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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 Some(session) = self.room_list().session() else {
            return IndexMap::new();
        };
        let client = session.client();

        // Load the serialized map from the store.
        let client_clone = client.clone();
        let handle = spawn_tokio!(async move {
            client_clone
                .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!("Could not deserialize rooms metainfo: {error}");
                    Default::default()
                }
            },
            Ok(None) => Default::default(),
            Err(error) => {
                error!("Could not load rooms metainfo: {error}");
                Default::default()
            }
        };

        // 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 matrix_rooms = client.rooms();
        let mut rooms = IndexMap::with_capacity(matrix_rooms.len());

        for matrix_room in matrix_rooms {
            let room_id = matrix_room.room_id().to_owned();
            let metainfo = rooms_metainfo.remove(&room_id);

            let room = Room::new(&session, matrix_room, metainfo);

            self.watch_room(&room);

            if let Some(metainfo) = metainfo {
                rooms_metainfo_guard.insert(room_id.clone(), metainfo);
            }

            rooms.insert(room_id, room);
        }

        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 Some(session) = self.room_list().session() else {
            return;
        };
        let value = match serde_json::to_vec(rooms_metainfo) {
            Ok(value) => value,
            Err(error) => {
                error!("Could not serialize rooms metainfo: {error}");
                return;
            }
        };

        let client = 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!("Could not 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
    }
}