1use std::{
2 borrow::{Borrow, BorrowMut},
3 collections::HashMap,
4 sync::Arc,
5};
6
7use anyhow::{Ok, anyhow, bail};
8use crossbeam_queue::SegQueue;
9use eyeball::Subscriber;
10use matrix_sdk::{
11 RoomDisplayName, RoomState,
12 ruma::{
13 MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, events::tag::Tags,
14 },
15};
16use matrix_sdk_ui::room_list_service::RoomListLoadingState;
17use serde::Serialize;
18use tokio::{
19 runtime::Handle,
20 sync::oneshot::{self, Sender},
21};
22
23use crate::{
24 init::singletons::{UIUpdateMessage, broadcast_event},
25 models::state_updater::StateUpdater,
26 room::{
27 invited_room::InvitedRoomInfo,
28 joined_room::UnreadMessageCount,
29 room_filter::{FilterableRoom, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn},
30 tags::FrontendRoomTags,
31 },
32 stores::room_store::send_room_creation_request_and_await_response,
33};
34
35use super::{room_filter::RoomDisplayFilter, room_screen::RoomScreen};
36
37#[derive(Debug)]
43pub enum RoomsListUpdate {
44 NotLoaded,
46 LoadedRooms { max_rooms: Option<u32> },
49 AddInvitedRoom(InvitedRoomInfo),
52 AddJoinedRoom(JoinedRoomInfo),
54 ClearRooms,
56 UpdateLatestEvent {
58 room_id: OwnedRoomId,
59 timestamp: MilliSecondsSinceUnixEpoch,
60 latest_message_text: String,
62 },
63 UpdateNumUnreadMessages {
65 room_id: OwnedRoomId,
66 count: UnreadMessageCount,
67 unread_mentions: u64,
68 },
69 UpdateRoomName {
71 room_id: OwnedRoomId,
72 new_room_name: RoomDisplayName,
73 },
74 UpdateRoomAvatar {
76 room_id: OwnedRoomId,
77 avatar: OwnedMxcUri,
78 },
79 RemoveRoom {
81 room_id: OwnedRoomId,
82 _new_state: RoomState,
84 },
85 Tags {
87 room_id: OwnedRoomId,
88 new_tags: Tags,
89 },
90 Status { status: RoomsCollectionStatus },
92}
93
94static PENDING_ROOM_UPDATES: SegQueue<RoomsListUpdate> = SegQueue::new();
95
96pub fn enqueue_rooms_list_update(update: RoomsListUpdate) {
99 PENDING_ROOM_UPDATES.push(update);
100 broadcast_event(UIUpdateMessage::RefreshUI).expect("Couldn't broadcast event to UI");
101}
102
103#[derive(Debug, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct JoinedRoomInfo {
110 pub(crate) room_id: OwnedRoomId,
112 pub(crate) room_name: Option<String>,
114 pub(crate) num_unread_messages: u64,
116 pub(crate) num_unread_mentions: u64,
118 pub(crate) canonical_alias: Option<OwnedRoomAliasId>,
120 pub(crate) alt_aliases: Vec<OwnedRoomAliasId>,
122 pub(crate) tags: FrontendRoomTags,
126 pub(crate) latest: Option<(MilliSecondsSinceUnixEpoch, String)>,
128 pub(crate) avatar: Option<OwnedMxcUri>,
130 pub(crate) has_been_paginated: bool,
135 pub(crate) is_selected: bool,
137 pub(crate) is_direct: bool,
139}
140
141pub fn handle_rooms_loading_state(mut loading_state: Subscriber<RoomListLoadingState>) {
142 println!(
143 "Initial room list loading state is {:?}",
144 loading_state.get()
145 );
146 Handle::current().spawn(async move {
147 while let Some(state) = loading_state.next().await {
148 println!("Received a room list loading state update: {state:?}");
149 match state {
150 RoomListLoadingState::NotLoaded => {
151 enqueue_rooms_list_update(RoomsListUpdate::NotLoaded);
152 }
153 RoomListLoadingState::Loaded {
154 maximum_number_of_rooms,
155 } => {
156 enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms {
157 max_rooms: maximum_number_of_rooms,
158 });
159 }
160 }
161 }
162 });
163}
164
165#[derive(Debug, Serialize)]
168#[serde(rename_all = "camelCase")]
169pub struct RoomsList {
170 invited_rooms: HashMap<OwnedRoomId, InvitedRoomInfo>,
172
173 all_joined_rooms: HashMap<OwnedRoomId, JoinedRoomInfo>,
176
177 #[serde(skip)]
184 display_filter: RoomDisplayFilter,
185
186 displayed_invited_rooms: Vec<OwnedRoomId>,
190
191 displayed_joined_rooms: Vec<OwnedRoomId>,
196
197 status: RoomsCollectionStatus,
200 current_active_room: Option<OwnedRoomId>,
203 #[serde(skip)]
206 current_active_room_killer: Option<Sender<()>>,
207 max_known_rooms: Option<u32>,
210 #[serde(skip)]
212 state_updaters: Arc<Box<dyn StateUpdater>>,
213}
214
215#[derive(Debug, Clone, Serialize)]
216#[serde(
217 rename_all = "camelCase",
218 rename_all_fields = "camelCase",
219 tag = "status",
220 content = "message"
221)]
222pub enum RoomsCollectionStatus {
223 NotLoaded(String),
224 Loading(String),
225 Loaded(String),
226 Error(String),
227}
228
229impl RoomsList {
230 pub(crate) fn new(updaters: Arc<Box<dyn StateUpdater>>) -> Self {
231 Self {
232 invited_rooms: HashMap::default(),
233 all_joined_rooms: HashMap::default(),
234 display_filter: RoomDisplayFilter::default(),
235 displayed_joined_rooms: Vec::new(),
236 displayed_invited_rooms: Vec::new(),
237 status: RoomsCollectionStatus::NotLoaded("Initiating".to_string()),
238 current_active_room: None,
239 current_active_room_killer: None,
240 max_known_rooms: None,
241 state_updaters: updaters,
242 }
243 }
244
245 fn update_frontend_state(&self) {
246 self.state_updaters
247 .update_rooms_list(self)
248 .expect("Couldn't update the frontend RoomsList state")
249 }
250
251 pub(crate) async fn handle_rooms_list_updates(&mut self) {
253 let mut num_updates: usize = 0;
254 while let Some(update) = PENDING_ROOM_UPDATES.pop() {
255 num_updates += 1;
256
257 #[cfg(debug_assertions)]
258 println!("Processing update type: {update:?}");
259
260 match update {
261 RoomsListUpdate::AddInvitedRoom(invited_room) => {
262 let room_id = invited_room.room_id.clone();
263 let should_display = (self.display_filter)(&invited_room);
264 let _replaced = self
265 .invited_rooms
266 .borrow_mut()
267 .insert(room_id.clone(), invited_room);
268 if let Some(_old_room) = _replaced {
269 eprintln!("BUG: Added invited room {room_id} that already existed");
270 } else {
271 if should_display {
272 self.displayed_invited_rooms.push(room_id);
273 }
274 }
275 self.update_status_rooms_count();
276 }
277 RoomsListUpdate::AddJoinedRoom(joined_room) => {
278 let room_id = joined_room.room_id.clone();
279 let should_display = (self.display_filter)(&joined_room);
280 let _replaced = self.all_joined_rooms.insert(room_id.clone(), joined_room);
281 if let Some(_old_room) = _replaced {
282 eprintln!("BUG: Added joined room {room_id} that already existed");
283 } else {
284 if should_display {
285 send_room_creation_request_and_await_response(room_id.as_str())
287 .await
288 .expect("Couldn't create svelte store");
289
290 self.displayed_joined_rooms.push(room_id.clone());
291 }
292 }
293 if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id)
297 {
298 println!("Removed room {room_id} from the list of invited rooms");
299 self.displayed_invited_rooms
300 .iter()
301 .position(|r| r == &room_id)
302 .map(|index| self.displayed_invited_rooms.remove(index));
303 }
304 self.update_status_rooms_count();
305 }
306 RoomsListUpdate::UpdateRoomAvatar { room_id, avatar } => {
307 if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
308 room.avatar = Some(avatar);
309 } else {
310 eprintln!("Error: couldn't find room {room_id} to update avatar");
311 }
312 }
313 RoomsListUpdate::UpdateLatestEvent {
314 room_id,
315 timestamp,
316 latest_message_text,
317 } => {
318 if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
319 room.latest = Some((timestamp, latest_message_text.clone()));
320 } else {
321 eprintln!("Error: couldn't find room {room_id} to update latest event");
322 }
323 }
324 RoomsListUpdate::UpdateNumUnreadMessages {
325 room_id,
326 count,
327 unread_mentions,
328 } => {
329 if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
330 (room.num_unread_messages, room.num_unread_mentions) = match count {
331 UnreadMessageCount::_Unknown => (0, 0),
332 UnreadMessageCount::Known(count) => (count, unread_mentions),
333 };
334 } else {
335 eprintln!(
336 "Error: couldn't find room {} to update unread messages count",
337 room_id
338 );
339 }
340 }
341 RoomsListUpdate::UpdateRoomName {
342 room_id,
343 new_room_name,
344 } => {
345 if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
346 let was_displayed = (self.display_filter)(room);
347 room.room_name = Some(new_room_name.to_string());
348 let should_display = (self.display_filter)(room);
349 match (was_displayed, should_display) {
350 (true, true) | (false, false) => {
351 }
353 (true, false) => {
354 self.displayed_joined_rooms
356 .iter()
357 .position(|r| r == &room_id)
358 .map(|index| self.displayed_joined_rooms.remove(index));
359 }
360 (false, true) => {
361 self.displayed_joined_rooms.push(room_id);
363 }
364 }
365 } else {
366 eprintln!("Error: couldn't find room {room_id} to update room name");
367 }
368 }
369 RoomsListUpdate::RemoveRoom {
370 room_id,
371 _new_state: _,
372 } => {
373 if let Some(_removed) = self.all_joined_rooms.remove(&room_id) {
374 println!("Removed room {room_id} from the list of all joined rooms");
375 if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) {
376 println!("Removed room {room_id} from the list of all invited rooms");
377 self.displayed_invited_rooms
378 .iter()
379 .position(|r| r == &room_id)
380 .map(|index| self.displayed_invited_rooms.remove(index));
381 } else {
382 self.displayed_joined_rooms
383 .iter()
384 .position(|r| r == &room_id)
385 .map(|index| self.displayed_joined_rooms.remove(index));
386 };
387 }
388 self.update_status_rooms_count();
389 }
390 RoomsListUpdate::ClearRooms => {
391 self.all_joined_rooms.clear();
392 self.displayed_joined_rooms.clear();
393 self.invited_rooms.borrow_mut().clear();
394 self.displayed_invited_rooms.clear();
395 self.update_status_rooms_count();
396 }
397 RoomsListUpdate::NotLoaded => {
398 self.status = RoomsCollectionStatus::Loading(
399 "Loading rooms (waiting for homeserver)...".to_string(),
400 );
401 }
402 RoomsListUpdate::LoadedRooms { max_rooms } => {
403 self.max_known_rooms = max_rooms;
404 self.update_status_rooms_count();
405 }
406 RoomsListUpdate::Tags { room_id, new_tags } => {
407 if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
408 room.tags = FrontendRoomTags::from(new_tags);
409 } else if let Some(_room) = self.invited_rooms.borrow().get(&room_id) {
410 println!("Ignoring updated tags update for invited room {room_id}");
411 } else {
412 eprintln!("Error: skipping updated Tags for unknown room {room_id}.");
413 }
414 }
415 RoomsListUpdate::Status { status } => {
416 self.status = status;
417 }
418 }
419 }
420 if num_updates > 0 {
421 println!(
422 "RoomsList: processed {} updates to the list of all rooms",
423 num_updates
424 );
425 self.update_frontend_state();
426 }
427 }
428
429 pub(crate) fn handle_current_active_room(
430 &mut self,
431 updated_current_active_room: Option<OwnedRoomId>,
432 mut room_name: Option<String>,
433 ) -> anyhow::Result<()> {
434 println!("{updated_current_active_room:?}");
435 self.current_active_room = updated_current_active_room;
436 match self.current_active_room.clone() {
437 Some(id) => {
438 let mut ui_subscriber = crate::init::singletons::subscribe_to_events()
439 .expect("Couldn't get UI subscriber event");
440 let (tx, mut rx) = oneshot::channel::<()>();
441 self.current_active_room_killer = Some(tx);
442 let updaters = self.state_updaters.clone();
443 Handle::current().spawn(async move {
444 let mut room_screen = RoomScreen::new(
445 updaters,
446 id,
447 room_name
448 .take()
449 .expect("Room name should be defined if room_id is"),
450 );
451 room_screen.show_timeline();
452
453 loop {
454 tokio::select! {
455 _ = ui_subscriber.recv() => {
456 room_screen.process_timeline_updates();
457 }
458 _ = &mut rx => {
459 break;
460 }
461 }
462 }
463 });
467 Ok(())
468 }
469 None => {
470 if let Some(sender) = self.current_active_room_killer.take() {
471 sender.send(()).map_err(|e| {
472 anyhow!("Error while sending message to terminate RoomScreen thread {e:?}")
473 })
474 } else {
475 bail!("Sender hasn't been set properly !");
476 }
477 }
478 }
479 }
484
485 fn update_status_rooms_count(&mut self) {
487 let num_rooms = self.all_joined_rooms.len() + self.invited_rooms.borrow().len();
488 self.status = if let Some(max_rooms) = self.max_known_rooms {
489 let message = format!("Loaded {num_rooms} of {max_rooms} total rooms.");
490 if num_rooms as u32 == max_rooms {
491 RoomsCollectionStatus::Loaded(message)
492 } else {
493 RoomsCollectionStatus::Loading(message)
494 }
495 } else {
496 RoomsCollectionStatus::Loaded(format!("Loaded {num_rooms} rooms."))
497 };
498 }
499
500 fn _update_status_matching_rooms(&mut self) {
503 let num_rooms = self.displayed_joined_rooms.len() + self.displayed_invited_rooms.len();
504 self.status = match num_rooms {
505 0 => RoomsCollectionStatus::Loaded("No matching rooms found.".to_string()),
506 1 => RoomsCollectionStatus::Loaded("Found 1 matching room.".to_string()),
507 n => RoomsCollectionStatus::Loaded(format!("Found {} matching rooms.", n)),
508 }
509 }
510
511 fn _is_room_displayable(&self, room: &OwnedRoomId) -> bool {
514 self.displayed_invited_rooms.contains(room) || self.displayed_joined_rooms.contains(room)
515 }
516
517 fn _update_displayed_rooms(&mut self, keywords: &str) {
520 if keywords.is_empty() {
521 self.display_filter = RoomDisplayFilter::default();
523 self.displayed_joined_rooms = self.all_joined_rooms.keys().cloned().collect();
524 self.displayed_invited_rooms = self.invited_rooms.borrow().keys().cloned().collect();
525 self.update_status_rooms_count();
526 return;
527 }
528
529 let (filter, sort_fn) = RoomDisplayFilterBuilder::new()
532 ._set_keywords(keywords.to_owned())
533 ._set_filter_criteria(RoomFilterCriteria::All)
534 ._build();
535 self.display_filter = filter;
536
537 fn generate_displayed_rooms<FR: FilterableRoom>(
539 rooms_map: &HashMap<OwnedRoomId, FR>,
540 display_filter: &RoomDisplayFilter,
541 sort_fn: Option<&SortFn>,
542 ) -> Vec<OwnedRoomId>
543 where
544 FR: FilterableRoom + Send + Sync + 'static,
545 {
546 if let Some(sort_fn) = sort_fn {
547 let mut filtered_rooms: Vec<_> = rooms_map
548 .iter()
549 .filter(|(_, room)| {
550 let room_trait: &(dyn FilterableRoom + Send + Sync) = *room;
551 display_filter(room_trait)
552 })
553 .collect();
554 filtered_rooms.sort_by(|(_, room_a), (_, room_b)| {
555 let room_a_trait: &(dyn FilterableRoom + Send + Sync) = *room_a;
556 let room_b_trait: &(dyn FilterableRoom + Send + Sync) = *room_b;
557 sort_fn(room_a_trait, room_b_trait)
558 });
559 filtered_rooms
560 .into_iter()
561 .map(|(room_id, _)| room_id.clone())
562 .collect()
563 } else {
564 rooms_map
565 .iter()
566 .filter(|(_, room)| display_filter(*room))
567 .map(|(room_id, _)| room_id.clone())
568 .collect()
569 }
570 }
571
572 self.displayed_joined_rooms = generate_displayed_rooms(
574 &self.all_joined_rooms,
575 &self.display_filter,
576 sort_fn.as_deref(),
577 );
578 self.displayed_invited_rooms = generate_displayed_rooms(
579 &self.invited_rooms.borrow(),
580 &self.display_filter,
581 sort_fn.as_deref(),
582 );
583 self._update_status_matching_rooms();
584 }
587}