fractal/session/model/room_list/
mod.rs1use std::{
2 cell::Cell,
3 collections::{HashMap, HashSet},
4};
5
6use gtk::{
7 gio, glib,
8 glib::{clone, closure_local},
9 prelude::*,
10 subclass::prelude::*,
11};
12use indexmap::IndexMap;
13use matrix_sdk::sync::RoomUpdates;
14use ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomOrAliasId, UserId};
15use tracing::{error, warn};
16
17mod room_list_metainfo;
18
19use self::room_list_metainfo::RoomListMetainfo;
20pub use self::room_list_metainfo::RoomMetainfo;
21use crate::{
22 gettext_f,
23 prelude::*,
24 session::model::{Room, Session},
25 spawn_tokio,
26};
27
28mod imp {
29 use std::{cell::RefCell, sync::LazyLock};
30
31 use glib::subclass::Signal;
32
33 use super::*;
34
35 #[derive(Debug, Default, glib::Properties)]
36 #[properties(wrapper_type = super::RoomList)]
37 pub struct RoomList {
38 pub(super) list: RefCell<IndexMap<OwnedRoomId, Room>>,
40 pub(super) pending_rooms: RefCell<HashSet<OwnedRoomOrAliasId>>,
42 tombstoned_rooms: RefCell<HashSet<OwnedRoomId>>,
45 #[property(get, construct_only)]
47 session: glib::WeakRef<Session>,
48 metainfo: RoomListMetainfo,
51 }
52
53 #[glib::object_subclass]
54 impl ObjectSubclass for RoomList {
55 const NAME: &'static str = "RoomList";
56 type Type = super::RoomList;
57 type Interfaces = (gio::ListModel,);
58 }
59
60 #[glib::derived_properties]
61 impl ObjectImpl for RoomList {
62 fn signals() -> &'static [Signal] {
63 static SIGNALS: LazyLock<Vec<Signal>> =
64 LazyLock::new(|| vec![Signal::builder("pending-rooms-changed").build()]);
65 SIGNALS.as_ref()
66 }
67
68 fn constructed(&self) {
69 self.parent_constructed();
70 self.metainfo.set_room_list(&self.obj());
71 }
72 }
73
74 impl ListModelImpl for RoomList {
75 fn item_type(&self) -> glib::Type {
76 Room::static_type()
77 }
78
79 fn n_items(&self) -> u32 {
80 self.list.borrow().len() as u32
81 }
82
83 fn item(&self, position: u32) -> Option<glib::Object> {
84 self.list
85 .borrow()
86 .get_index(position as usize)
87 .map(|(_, v)| v.upcast_ref::<glib::Object>())
88 .cloned()
89 }
90 }
91
92 impl RoomList {
93 pub(super) fn get(&self, room_id: &RoomId) -> Option<Room> {
95 self.list.borrow().get(room_id).cloned()
96 }
97
98 fn contains(&self, room_id: &RoomId) -> bool {
100 self.list.borrow().contains_key(room_id)
101 }
102
103 fn remove_pending_room(&self, identifier: &RoomOrAliasId) {
105 self.pending_rooms.borrow_mut().remove(identifier);
106 self.obj().emit_by_name::<()>("pending-rooms-changed", &[]);
107 }
108
109 fn add_pending_room(&self, identifier: OwnedRoomOrAliasId) {
111 self.pending_rooms.borrow_mut().insert(identifier);
112 self.obj().emit_by_name::<()>("pending-rooms-changed", &[]);
113 }
114
115 pub(super) fn add_tombstoned_room(&self, room_id: OwnedRoomId) {
118 self.tombstoned_rooms.borrow_mut().insert(room_id);
119 }
120
121 fn remove_or_replace_pending_room(&self, identifier: &RoomOrAliasId, room_id: &RoomId) {
124 {
125 let mut pending_rooms = self.pending_rooms.borrow_mut();
126 pending_rooms.remove(identifier);
127 if !self.contains(room_id) {
128 pending_rooms.insert(room_id.to_owned().into());
129 }
130 }
131 self.obj().emit_by_name::<()>("pending-rooms-changed", &[]);
132 }
133
134 fn items_added(&self, added: usize) {
136 let position = {
137 let list = self.list.borrow();
138
139 let position = list.len().saturating_sub(added);
140
141 let mut tombstoned_rooms_to_remove = Vec::new();
142 for (_room_id, room) in list.iter().skip(position) {
143 room.connect_room_forgotten(clone!(
144 #[weak(rename_to = imp)]
145 self,
146 move |room| {
147 imp.remove(room.room_id());
148 }
149 ));
150
151 if let Some(predecessor_id) = room.predecessor_id() {
153 if self.tombstoned_rooms.borrow().contains(predecessor_id) {
154 if let Some(room) = self.get(predecessor_id) {
155 room.update_successor();
156 tombstoned_rooms_to_remove.push(predecessor_id.clone());
157 }
158 }
159 }
160 }
161
162 if !tombstoned_rooms_to_remove.is_empty() {
163 let mut tombstoned_rooms = self.tombstoned_rooms.borrow_mut();
164 for room_id in tombstoned_rooms_to_remove {
165 tombstoned_rooms.remove(&room_id);
166 }
167 }
168
169 position
170 };
171
172 self.obj().items_changed(position as u32, 0, added as u32);
173 }
174
175 fn remove(&self, room_id: &RoomId) {
177 let removed = {
178 let mut list = self.list.borrow_mut();
179
180 list.shift_remove_full(room_id)
181 };
182
183 self.tombstoned_rooms.borrow_mut().remove(room_id);
184
185 if let Some((position, ..)) = removed {
186 self.obj().items_changed(position as u32, 1, 0);
187 }
188 }
189
190 pub(super) async fn load(&self) {
192 let rooms = self.metainfo.load_rooms().await;
193 let added = rooms.len();
194 self.list.borrow_mut().extend(rooms);
195
196 self.items_added(added);
197 }
198
199 pub(super) fn handle_room_updates(&self, rooms: RoomUpdates) {
201 let Some(session) = self.session.upgrade() else {
202 return;
203 };
204 let client = session.client();
205
206 let mut new_rooms = HashMap::new();
207
208 for (room_id, left_room) in rooms.leave {
209 let room = if let Some(room) = self.get(&room_id) {
210 room
211 } else if let Some(matrix_room) = client.get_room(&room_id) {
212 new_rooms
213 .entry(room_id.clone())
214 .or_insert_with(|| Room::new(&session, matrix_room, None))
215 .clone()
216 } else {
217 warn!("Could not find left room {room_id}");
218 continue;
219 };
220
221 self.remove_pending_room((*room_id).into());
222 room.handle_ambiguity_changes(left_room.ambiguity_changes.values());
223 }
224
225 for (room_id, joined_room) in rooms.join {
226 let room = if let Some(room) = self.get(&room_id) {
227 room
228 } else if let Some(matrix_room) = client.get_room(&room_id) {
229 new_rooms
230 .entry(room_id.clone())
231 .or_insert_with(|| Room::new(&session, matrix_room, None))
232 .clone()
233 } else {
234 warn!("Could not find joined room {room_id}");
235 continue;
236 };
237
238 self.remove_pending_room((*room_id).into());
239 self.metainfo.watch_room(&room);
240 room.handle_ambiguity_changes(joined_room.ambiguity_changes.values());
241 }
242
243 for (room_id, _invited_room) in rooms.invite {
244 let room = if let Some(room) = self.get(&room_id) {
245 room
246 } else if let Some(matrix_room) = client.get_room(&room_id) {
247 new_rooms
248 .entry(room_id.clone())
249 .or_insert_with(|| Room::new(&session, matrix_room, None))
250 .clone()
251 } else {
252 warn!("Could not find invited room {room_id}");
253 continue;
254 };
255
256 self.remove_pending_room((*room_id).into());
257 self.metainfo.watch_room(&room);
258 }
259
260 if !new_rooms.is_empty() {
261 let added = new_rooms.len();
262 self.list.borrow_mut().extend(new_rooms);
263 self.items_added(added);
264 }
265 }
266
267 pub(super) async fn join_by_id_or_alias(
269 &self,
270 identifier: OwnedRoomOrAliasId,
271 via: Vec<OwnedServerName>,
272 ) -> Result<OwnedRoomId, String> {
273 let Some(session) = self.session.upgrade() else {
274 return Err("Could not upgrade Session".to_owned());
275 };
276 let client = session.client();
277 let identifier_clone = identifier.clone();
278
279 self.add_pending_room(identifier.clone());
280
281 let handle = spawn_tokio!(async move {
282 client
283 .join_room_by_id_or_alias(&identifier_clone, &via)
284 .await
285 });
286
287 match handle.await.expect("task was not aborted") {
288 Ok(matrix_room) => {
289 self.remove_or_replace_pending_room(&identifier, matrix_room.room_id());
290 Ok(matrix_room.room_id().to_owned())
291 }
292 Err(error) => {
293 self.remove_pending_room(&identifier);
294 error!("Joining room {identifier} failed: {error}");
295
296 let error = gettext_f(
297 "Could not join room {room_name}",
300 &[("room_name", identifier.as_str())],
301 );
302
303 Err(error)
304 }
305 }
306 }
307 }
308}
309
310glib::wrapper! {
311 pub struct RoomList(ObjectSubclass<imp::RoomList>)
320 @implements gio::ListModel;
321}
322
323impl RoomList {
324 pub fn new(session: &Session) -> Self {
325 glib::Object::builder().property("session", session).build()
326 }
327
328 pub(crate) async fn load(&self) {
330 self.imp().load().await;
331 }
332
333 pub(crate) fn snapshot(&self) -> Vec<Room> {
335 self.imp().list.borrow().values().cloned().collect()
336 }
337
338 pub(crate) fn is_pending_room(&self, identifier: &RoomOrAliasId) -> bool {
340 self.imp().pending_rooms.borrow().contains(identifier)
341 }
342
343 pub(crate) fn get(&self, room_id: &RoomId) -> Option<Room> {
345 self.imp().get(room_id)
346 }
347
348 pub(crate) fn get_by_identifier(&self, identifier: &RoomOrAliasId) -> Option<Room> {
350 let room_alias = match <&RoomId>::try_from(identifier) {
351 Ok(room_id) => return self.get(room_id),
352 Err(room_alias) => room_alias,
353 };
354
355 let mut matches = self
356 .imp()
357 .list
358 .borrow()
359 .iter()
360 .filter(|(_, room)| {
361 if !room.is_joined() {
364 return false;
365 }
366
367 let matrix_room = room.matrix_room();
368 matrix_room.canonical_alias().as_deref() == Some(room_alias)
369 || matrix_room.alt_aliases().iter().any(|a| a == room_alias)
370 })
371 .map(|(room_id, room)| (room_id.clone(), room.clone()))
372 .collect::<HashMap<_, _>>();
373
374 if matches.len() <= 1 {
375 return matches.into_values().next();
376 }
377
378 let predecessors = matches
381 .iter()
382 .filter_map(|(_, room)| room.predecessor_id().cloned())
383 .collect::<Vec<_>>();
384 for room_id in predecessors {
385 matches.remove(&room_id);
386 }
387
388 if matches.len() <= 1 {
389 return matches.into_values().next();
390 }
391
392 matches
394 .into_values()
395 .fold(None::<Room>, |latest_room, room| {
396 latest_room
397 .filter(|r| r.latest_activity() >= room.latest_activity())
398 .or(Some(room))
399 })
400 }
401
402 pub(crate) async fn get_wait(&self, room_id: &RoomId) -> Option<Room> {
404 if let Some(room) = self.get(room_id) {
405 return Some(room);
406 }
407
408 let (sender, receiver) = futures_channel::oneshot::channel();
409
410 let room_id = room_id.to_owned();
411 let sender = Cell::new(Some(sender));
412 let handler_id = self.connect_items_changed(move |obj, _, _, _| {
414 if let Some(room) = obj.get(&room_id) {
415 if let Some(sender) = sender.take() {
416 let _ = sender.send(Some(room));
417 }
418 }
419 });
420
421 let room = receiver.await.ok().flatten();
422
423 self.disconnect(handler_id);
424
425 room
426 }
427
428 pub(crate) fn direct_chat(&self, user_id: &UserId) -> Option<Room> {
433 self.imp()
434 .list
435 .borrow()
436 .values()
437 .filter(|r| {
438 r.is_joined() && r.direct_member().as_ref().map(|m| &**m.user_id()) == Some(user_id)
440 })
441 .max_by(|x, y| x.latest_activity().cmp(&y.latest_activity()))
443 .cloned()
444 }
445
446 pub(crate) fn add_tombstoned_room(&self, room_id: OwnedRoomId) {
449 self.imp().add_tombstoned_room(room_id);
450 }
451
452 pub(crate) fn handle_room_updates(&self, rooms: RoomUpdates) {
454 self.imp().handle_room_updates(rooms);
455 }
456
457 pub(crate) async fn join_by_id_or_alias(
459 &self,
460 identifier: OwnedRoomOrAliasId,
461 via: Vec<OwnedServerName>,
462 ) -> Result<OwnedRoomId, String> {
463 self.imp().join_by_id_or_alias(identifier, via).await
464 }
465
466 pub fn connect_pending_rooms_changed<F: Fn(&Self) + 'static>(
468 &self,
469 f: F,
470 ) -> glib::SignalHandlerId {
471 self.connect_closure(
472 "pending-rooms-changed",
473 true,
474 closure_local!(move |obj: Self| {
475 f(&obj);
476 }),
477 )
478 }
479}