matrix_ui_serializable/room/
rooms_list.rs

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/// The possible updates that should be displayed by the single list of all rooms.
38///
39/// These updates are enqueued by the `enqueue_rooms_list_update` function
40/// (which is called from background async tasks that receive updates from the matrix server),
41/// and then dequeued by the `RoomsList` widget's `handle_event` function.
42#[derive(Debug)]
43pub enum RoomsListUpdate {
44    /// No rooms have been loaded yet.
45    NotLoaded,
46    /// Some rooms were loaded, and the server optionally told us
47    /// the max number of rooms that will ever be loaded.
48    LoadedRooms { max_rooms: Option<u32> },
49    /// Add a new room to the list of rooms the user has been invited to.
50    /// This will be maintained and displayed separately from joined rooms.
51    AddInvitedRoom(InvitedRoomInfo),
52    /// Add a new room to the list of all rooms that the user has joined.
53    AddJoinedRoom(JoinedRoomInfo),
54    /// Clear all rooms in the list of all rooms.
55    ClearRooms,
56    /// Update the latest event content and timestamp for the given room.
57    UpdateLatestEvent {
58        room_id: OwnedRoomId,
59        timestamp: MilliSecondsSinceUnixEpoch,
60        /// The Html-formatted text preview of the latest message.
61        latest_message_text: String,
62    },
63    /// Update the number of unread messages for the given room.
64    UpdateNumUnreadMessages {
65        room_id: OwnedRoomId,
66        count: UnreadMessageCount,
67        unread_mentions: u64,
68    },
69    /// Update the displayable name for the given room.
70    UpdateRoomName {
71        room_id: OwnedRoomId,
72        new_room_name: RoomDisplayName,
73    },
74    /// Update the avatar (image) for the given room.
75    UpdateRoomAvatar {
76        room_id: OwnedRoomId,
77        avatar: OwnedMxcUri,
78    },
79    /// Remove the given room from the rooms list
80    RemoveRoom {
81        room_id: OwnedRoomId,
82        /// The new state of the room (which caused its removal).
83        _new_state: RoomState,
84    },
85    /// Update the tags for the given room.
86    Tags {
87        room_id: OwnedRoomId,
88        new_tags: Tags,
89    },
90    /// Update the status label at the bottom of the list of all rooms.
91    Status { status: RoomsCollectionStatus },
92}
93
94static PENDING_ROOM_UPDATES: SegQueue<RoomsListUpdate> = SegQueue::new();
95
96/// Enqueue a new room update for the list of all rooms
97/// and signals the UI that a new update is available to be handled.
98pub 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/// UI-related info about a joined room.
104///
105/// This includes info needed display a preview of that room in the RoomsList
106/// and to filter the list of rooms based on the current search filter.
107#[derive(Debug, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct JoinedRoomInfo {
110    /// The matrix ID of this room.
111    pub(crate) room_id: OwnedRoomId,
112    /// The displayable name of this room, if known.
113    pub(crate) room_name: Option<String>,
114    /// The number of unread messages in this room.
115    pub(crate) num_unread_messages: u64,
116    /// The number of unread mentions in this room.
117    pub(crate) num_unread_mentions: u64,
118    /// The canonical alias for this room, if any.
119    pub(crate) canonical_alias: Option<OwnedRoomAliasId>,
120    /// The alternative aliases for this room, if any.
121    pub(crate) alt_aliases: Vec<OwnedRoomAliasId>,
122    /// The tags associated with this room, if any.
123    /// This includes things like is_favourite, is_low_priority,
124    /// whether the room is a server notice room, etc.
125    pub(crate) tags: FrontendRoomTags,
126    /// The timestamp and Html text content of the latest message in this room.
127    pub(crate) latest: Option<(MilliSecondsSinceUnixEpoch, String)>,
128    /// The avatar for this room
129    pub(crate) avatar: Option<OwnedMxcUri>,
130    /// Whether this room has been paginated at least once.
131    /// We pre-paginate visible rooms at least once in order to
132    /// be able to display the latest message in the room preview,
133    /// and to have something to immediately show when a user first opens a room.
134    pub(crate) has_been_paginated: bool,
135    /// Whether this room is currently selected in the UI.
136    pub(crate) is_selected: bool,
137    /// Whether this a direct room.
138    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/// The struct containing all the data related to the homepage rooms list.
166/// Fields are not exposed to the adapter directly, the adapter can only serialize this struct.
167#[derive(Debug, Serialize)]
168#[serde(rename_all = "camelCase")]
169pub struct RoomsList {
170    /// The list of all rooms that the user has been invited to.
171    invited_rooms: HashMap<OwnedRoomId, InvitedRoomInfo>,
172
173    /// The set of all joined rooms and their cached preview info.
174    /// Frontend Svelte : `allJoinedRooms` Record param of the rooms-collection store
175    all_joined_rooms: HashMap<OwnedRoomId, JoinedRoomInfo>,
176
177    /// The currently-active filter function for the list of rooms.
178    ///
179    /// Note: for performance reasons, this does not get automatically applied
180    /// when its value changes. Instead, you must manually invoke it on the set of `all_joined_rooms`
181    /// in order to update the set of `displayed_rooms` accordingly.
182    /// Frontend Svelte : not implemented yet
183    #[serde(skip)]
184    display_filter: RoomDisplayFilter,
185
186    /// The list of invited rooms currently displayed in the UI, in order from top to bottom.
187    /// This is a strict subset of the rooms present in `all_invited_rooms`, and should be determined
188    /// by applying the `display_filter` to the set of `all_invited_rooms`.
189    displayed_invited_rooms: Vec<OwnedRoomId>,
190
191    /// The list of joined rooms currently displayed in the UI, in order from top to bottom.
192    /// This is a strict subset of the rooms present in `all_joined_rooms`, and should be determined
193    /// by applying the `display_filter` to the set of `all_joined_rooms`.
194    /// Frontend Svelte : `displayedJoinedRooms` array of the rooms-collection store
195    displayed_joined_rooms: Vec<OwnedRoomId>,
196
197    /// The latest status message that should be displayed in the bottom status label.
198    /// Frontend Svelte : `status` param of the rooms-collection store
199    status: RoomsCollectionStatus,
200    /// The ID of the currently-selected room.
201    /// Frontend Svelte : `currentActiveRoom` param of the rooms-collection store
202    current_active_room: Option<OwnedRoomId>,
203    /// The current active room sender to interrupt the task when room is closed.
204    /// Backend only
205    #[serde(skip)]
206    current_active_room_killer: Option<Sender<()>>,
207    /// The maximum number of rooms that will ever be loaded.
208    /// Frontend Svelte : `maxKnownRooms` param of the rooms-collection store
209    max_known_rooms: Option<u32>,
210    /// The state updater passed by the adapter for this struct
211    #[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    /// Handle all pending updates to the list of all rooms.
252    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                            // Create frontend state store
286                            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 this room was added as a result of accepting an invite, we must:
294                    // 1. Remove the room from the list of invited rooms.
295                    // 2. Update the displayed invited rooms list to remove this room.
296                    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                                // No need to update the displayed rooms list.
352                            }
353                            (true, false) => {
354                                // Room was displayed but should no longer be displayed.
355                                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                                // Room was not displayed but should now be displayed.
362                                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                    // as soon as this task is done,
464                    // the room_screen will be dropped,
465                    // and hide_timeline() will be called on drop
466                });
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        // match the option
480        // if none, then send a message to the running thread to end task
481        // if yes then try to spawn the corresponding RoomScreen with a receiver and store the sender somewhere in a map
482        // this thread will also handle room actions such as sending messages
483    }
484
485    /// Updates the status message to show how many rooms have been loaded.
486    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    /// Updates the status message to show how many rooms are currently displayed
501    /// that match the current search filter.
502    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    /// Returns true if the given room is contained in any of the displayed room sets,
512    /// i.e., either the invited rooms or the joined rooms.
513    fn _is_room_displayable(&self, room: &OwnedRoomId) -> bool {
514        self.displayed_invited_rooms.contains(room) || self.displayed_joined_rooms.contains(room)
515    }
516
517    /// Updates the lists of displayed rooms based on the current search filter
518    /// and redraws the RoomsList.
519    fn _update_displayed_rooms(&mut self, keywords: &str) {
520        if keywords.is_empty() {
521            // Reset the displayed rooms list to show all rooms.
522            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        // Create a new filter function based on the given keywords
530        // and store it in this RoomsList such that we can apply it to newly-added rooms.
531        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        /// An inner function that generates a sorted, filtered list of rooms to display.
538        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        // Update the displayed rooms list and redraw it.
573        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        // portal_list.set_first_id_and_scroll(0, 0.0);
585        // self.redraw(cx);
586    }
587}