Skip to main content

matrix_ui_serializable/room/
rooms_list.rs

1use std::{
2    borrow::{Borrow, BorrowMut},
3    collections::{HashMap, HashSet, VecDeque},
4    sync::Arc,
5};
6use tracing::{debug, error, info, warn};
7
8use crossbeam_queue::SegQueue;
9use eyeball::Subscriber;
10use matrix_sdk::{
11    RoomDisplayName, RoomHero, RoomState,
12    ruma::{
13        MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
14        events::tag::Tags,
15    },
16};
17use matrix_sdk_ui::room_list_service::RoomListLoadingState;
18use serde::Serialize;
19use tokio::{
20    runtime::Handle,
21    sync::oneshot::{self, Sender},
22};
23
24use crate::{
25    events::timeline::TimelineKind,
26    init::singletons::{ALL_ROOMS_LOADED, UIUpdateMessage, broadcast_event},
27    models::{
28        events::{ToastNotificationRequest, ToastNotificationVariant},
29        room_display_name::FrontendRoomDisplayName,
30        state_updater::StateUpdater,
31    },
32    room::{
33        invited_room::InvitedRoomInfo,
34        joined_room::UnreadMessageCount,
35        notifications::enqueue_toast_notification,
36        room_filter::{RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn},
37    },
38    utils::VecDiff,
39};
40
41use super::{room_filter::RoomDisplayFilter, room_screen::RoomScreen};
42
43/// The possible updates that should be displayed by the single list of all rooms.
44///
45/// These updates are enqueued by the `enqueue_rooms_list_update` function
46/// (which is called from background async tasks that receive updates from the matrix server),
47/// and then dequeued by the `RoomsList` widget's `handle_event` function.
48#[derive(Debug)]
49pub enum RoomsListUpdate {
50    /// No rooms have been loaded yet.
51    NotLoaded,
52    /// Some rooms were loaded, and the server optionally told us
53    /// the max number of rooms that will ever be loaded.
54    LoadedRooms { max_rooms: Option<u32> },
55    /// Add a new room to the list of rooms the user has been invited to.
56    /// This will be maintained and displayed separately from joined rooms.
57    AddInvitedRoom(InvitedRoomInfo),
58    /// Add a new room to the list of all rooms that the user has joined.
59    AddJoinedRoom(JoinedRoomInfo),
60    /// Clear all rooms in the list of all rooms.
61    ClearRooms,
62    /// Update the latest event content and timestamp for the given room.
63    UpdateLatestEvent {
64        room_id: OwnedRoomId,
65        timestamp: MilliSecondsSinceUnixEpoch,
66        /// The Html-formatted text preview of the latest message.
67        latest_message_text: String,
68    },
69    /// Update the number of unread messages and mentions for the given room.
70    UpdateNumUnreadMessages {
71        room_id: OwnedRoomId,
72        is_marked_unread: bool,
73        unread_messages: UnreadMessageCount,
74        unread_mentions: u64,
75    },
76    /// Update the displayable name for the given room.
77    UpdateRoomName {
78        room_id: OwnedRoomId,
79        new_room_name: RoomDisplayName,
80    },
81    /// Update the topic for the given room.
82    UpdateTopic {
83        room_id: OwnedRoomId,
84        new_topic: String,
85    },
86    /// Update the avatar (image) for the given room.
87    UpdateRoomAvatar {
88        room_id: OwnedRoomId,
89        avatar: OwnedMxcUri,
90    },
91    /// Update whether the given room is a direct room.
92    UpdateIsDirect {
93        room_id: OwnedRoomId,
94        is_direct: bool,
95    },
96    /// Remove the given room from the rooms list
97    RemoveRoom {
98        room_id: OwnedRoomId,
99        /// The new state of the room (which caused its removal).
100        _new_state: RoomState,
101    },
102    /// Update the tags for the given room.
103    Tags {
104        room_id: OwnedRoomId,
105        new_tags: Tags,
106    },
107    /// Update the status label at the bottom of the list of all rooms.
108    Status { status: RoomsCollectionStatus },
109    /// Mark the given room as tombstoned.
110    TombstonedRoom { room_id: OwnedRoomId },
111    /// Hide the given room from being displayed.
112    ///
113    /// This is useful for temporarily preventing a room from being shown,
114    /// e.g., after a room has been left but before the homeserver has registered
115    /// that we left it and removed it via the RoomListService.
116    _HideRoom { room_id: OwnedRoomId },
117    /// Update the ordering of rooms based on the given diff.
118    RoomOrderUpdate(VecDiff<OwnedRoomId>),
119    /// Apply a filter to the rooms list
120    ApplyFilter { keywords: String },
121}
122
123static PENDING_ROOM_UPDATES: SegQueue<RoomsListUpdate> = SegQueue::new();
124
125/// Enqueue a new room update for the list of all rooms
126/// and signals the UI that a new update is available to be handled.
127pub fn enqueue_rooms_list_update(update: RoomsListUpdate) {
128    PENDING_ROOM_UPDATES.push(update);
129    broadcast_event(UIUpdateMessage::RefreshUI);
130}
131
132/// UI-related info about a joined room.
133///
134/// This includes info needed display a preview of that room in the RoomsList
135/// and to filter the list of rooms based on the current search filter.
136#[derive(Debug, Clone, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct JoinedRoomInfo {
139    /// The matrix ID of this room.
140    pub(crate) room_id: OwnedRoomId,
141    /// The displayable name of this room, if known.
142    pub(crate) room_name: FrontendRoomDisplayName,
143    /// The number of unread messages in this room.
144    pub(crate) num_unread_messages: u64,
145    /// The number of unread mentions in this room.
146    pub(crate) num_unread_mentions: u64,
147    /// Whether the room is manually marked as unread.
148    pub(crate) is_marked_unread: bool,
149    /// The canonical alias for this room, if any.
150    pub(crate) canonical_alias: Option<OwnedRoomAliasId>,
151    /// The alternative aliases for this room, if any.
152    pub(crate) alt_aliases: Vec<OwnedRoomAliasId>,
153    /// The tags associated with this room, if any.
154    /// This includes things like is_favourite, is_low_priority,
155    /// whether the room is a server notice room, etc.
156    pub(crate) tags: Tags,
157    /// The topic of the current room
158    pub(crate) topic: Option<String>,
159    /// The timestamp and Html text content of the latest message in this room.
160    pub(crate) latest: Option<(MilliSecondsSinceUnixEpoch, String)>,
161    /// The avatar for this room
162    pub(crate) avatar: Option<OwnedMxcUri>,
163    /// Whether this room has been paginated at least once.
164    /// We pre-paginate visible rooms at least once in order to
165    /// be able to display the latest message in the room preview,
166    /// and to have something to immediately show when a user first opens a room.
167    pub(crate) has_been_paginated: bool,
168    /// Whether this room is currently selected in the UI.
169    pub(crate) is_selected: bool,
170    /// Whether this a DM room or not.
171    pub(crate) is_direct: bool,
172    /// UserId of the user if the room is direct
173    pub(crate) direct_user_id: Option<OwnedUserId>,
174    /// Whether this room is tombstoned (shut down and replaced with a successor room).
175    pub(crate) is_tombstoned: bool,
176    /// Room "heroes", ~ main users of this room
177    pub(crate) heroes: Vec<RoomHero>,
178}
179
180pub fn handle_rooms_loading_state(mut loading_state: Subscriber<RoomListLoadingState>) {
181    debug!(
182        "Initial room list loading state is {:?}",
183        loading_state.get()
184    );
185    Handle::current().spawn(async move {
186        while let Some(state) = loading_state.next().await {
187            debug!("Received a room list loading state update: {state:?}");
188            match state {
189                RoomListLoadingState::NotLoaded => {
190                    enqueue_rooms_list_update(RoomsListUpdate::NotLoaded);
191                }
192                RoomListLoadingState::Loaded {
193                    maximum_number_of_rooms,
194                } => {
195                    ALL_ROOMS_LOADED.get_or_init(|| true);
196                    enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms {
197                        max_rooms: maximum_number_of_rooms,
198                    });
199                }
200            }
201        }
202    });
203}
204
205/// The struct containing all the data related to the homepage rooms list.
206/// Fields are not exposed to the adapter directly, the adapter can only serialize this struct.
207#[derive(Debug, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct RoomsList {
210    // We manually put the Record type, otherwise it generates a weird type with undefined
211    /// The list of all rooms that the user has been invited to.
212    invited_rooms: HashMap<OwnedRoomId, InvitedRoomInfo>,
213
214    // We manually put the Record type, otherwise it generates a weird type with undefined
215    /// The set of all joined rooms and their cached preview info.
216    all_joined_rooms: HashMap<OwnedRoomId, JoinedRoomInfo>,
217
218    /// The list of all room IDs in display order, matching the order from the room list service.
219    all_known_rooms_order: VecDeque<OwnedRoomId>,
220
221    /// Rooms that are explicitly hidden and should never be shown in the rooms list.
222    hidden_rooms: HashSet<OwnedRoomId>,
223
224    /// The currently-active filter function for the list of rooms.
225    ///
226    /// Note: for performance reasons, this does not get automatically applied
227    /// when its value changes. Instead, you must manually invoke it on the set of `all_joined_rooms`
228    /// in order to update the set of `displayed_rooms` accordingly.
229    #[serde(skip)]
230    display_filter: RoomDisplayFilter,
231
232    /// The latest keywords entered into the `RoomFilterInputBar`.
233    ///
234    /// If empty, there are no filter keywords in use, and all rooms/spaces should be shown.
235    filter_keywords: String,
236
237    /// The list of invited rooms currently displayed in the UI, in order from top to bottom.
238    /// This is a strict subset of the rooms present in `all_invited_rooms`, and should be determined
239    /// by applying the `display_filter` to the set of `all_invited_rooms`.
240    displayed_invited_rooms: Vec<OwnedRoomId>,
241
242    /// The list of direct rooms currently displayed in the UI, in order from top to bottom.
243    /// This is a strict subset of the rooms present in `all_joined_rooms`,
244    /// and should be determined by applying the `display_filter && is_direct`
245    /// to the set of `all_joined_rooms`.
246    displayed_direct_rooms: Vec<OwnedRoomId>,
247
248    /// The list of regular (non-direct) joined rooms currently displayed in the UI,
249    /// in order from top to bottom.
250    /// This is a strict subset of the rooms in `all_joined_rooms`,
251    /// and should be determined by applying the `display_filter && !is_direct`
252    /// to the set of `all_joined_rooms`.
253    ///
254    /// **Direct rooms are excluded** from this; they are in `displayed_direct_rooms`.
255    displayed_regular_rooms: Vec<OwnedRoomId>,
256
257    /// The latest status message that should be displayed in the bottom status label.
258    status: RoomsCollectionStatus,
259    /// The ID of the currently-selected timeline.
260    current_active_room: Option<TimelineKind>,
261    /// The current active room sender to interrupt the task when room is closed.
262    /// Backend only
263    #[serde(skip)]
264    current_active_room_killer: Option<Sender<()>>,
265    /// The maximum number of rooms that will ever be loaded.
266    max_known_rooms: Option<u32>,
267    /// The state updater passed by the adapter for this struct
268    #[serde(skip)]
269    state_updaters: Arc<Box<dyn StateUpdater>>,
270}
271
272#[derive(Debug, Clone, Serialize)]
273#[serde(
274    rename_all = "camelCase",
275    rename_all_fields = "camelCase",
276    tag = "status",
277    content = "message"
278)]
279pub enum RoomsCollectionStatus {
280    NotLoaded(String),
281    Loading(String),
282    Loaded(String),
283    Error(String),
284}
285
286impl RoomsList {
287    pub(crate) fn new(updaters: Arc<Box<dyn StateUpdater>>) -> Self {
288        Self {
289            invited_rooms: HashMap::default(),
290            all_joined_rooms: HashMap::default(),
291            display_filter: RoomDisplayFilter::default(),
292            filter_keywords: "".to_owned(),
293            displayed_regular_rooms: Vec::new(),
294            all_known_rooms_order: VecDeque::new(),
295            hidden_rooms: HashSet::new(),
296            displayed_direct_rooms: Vec::new(),
297            displayed_invited_rooms: Vec::new(),
298            status: RoomsCollectionStatus::NotLoaded("Initiating".to_owned()),
299            current_active_room: None,
300            current_active_room_killer: None,
301            max_known_rooms: None,
302            state_updaters: updaters,
303        }
304    }
305
306    fn update_frontend_state(&self) {
307        if let Err(e) = self.state_updaters.update_rooms_list(self) {
308            enqueue_toast_notification(ToastNotificationRequest::new(
309                format!("Cannot update room list store. Error: {e}"),
310                None,
311                ToastNotificationVariant::Error,
312            ))
313        }
314    }
315
316    /// Handle all pending updates to the list of all rooms.
317    pub(crate) async fn handle_rooms_list_updates(&mut self) {
318        let mut num_updates: usize = 0;
319        let mut needs_sort = false;
320
321        while let Some(update) = PENDING_ROOM_UPDATES.pop() {
322            num_updates += 1;
323
324            debug!("Processing update type: {update:?}");
325
326            match update {
327                RoomsListUpdate::AddInvitedRoom(invited_room) => {
328                    let room_id = invited_room.room_id.clone();
329                    let should_display = (self.display_filter)(&invited_room);
330                    let _replaced = self
331                        .invited_rooms
332                        .borrow_mut()
333                        .insert(room_id.clone(), invited_room);
334                    if let Some(_old_room) = _replaced {
335                        error!("BUG: Added invited room {room_id} that already existed");
336                    } else if should_display {
337                        self.displayed_invited_rooms.push(room_id);
338                    }
339                    self.update_status_rooms_count();
340                }
341                RoomsListUpdate::AddJoinedRoom(joined_room) => {
342                    let room_id = joined_room.room_id.clone();
343                    let should_display = (self.display_filter)(&joined_room);
344                    let is_direct = joined_room.is_direct;
345
346                    let replaced = self.all_joined_rooms.insert(room_id.clone(), joined_room);
347
348                    if let Some(_old_room) = replaced {
349                        error!("BUG: Added joined room {room_id} that already existed");
350                    } else if should_display {
351                        if is_direct {
352                            self.displayed_direct_rooms.push(room_id.clone());
353                        } else {
354                            self.displayed_regular_rooms.push(room_id.clone());
355                        }
356                    }
357                    // If this room was added as a result of accepting an invite, we must:
358                    // 1. Remove the room from the list of invited rooms.
359                    // 2. Update the displayed invited rooms list to remove this room.
360                    if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id)
361                    {
362                        info!("Removed room {room_id} from the list of invited rooms");
363                        self.displayed_invited_rooms
364                            .iter()
365                            .position(|r| r == &room_id)
366                            .map(|index| self.displayed_invited_rooms.remove(index));
367                    }
368                    self.update_status_rooms_count();
369                }
370                RoomsListUpdate::UpdateRoomAvatar { room_id, avatar } => {
371                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
372                        room.avatar = Some(avatar.clone());
373                    } else {
374                        error!("Error: couldn't find room {room_id} to update avatar");
375                    }
376                }
377                RoomsListUpdate::UpdateLatestEvent {
378                    room_id,
379                    timestamp,
380                    latest_message_text,
381                } => {
382                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
383                        room.latest = Some((timestamp, latest_message_text.clone()));
384                    } else {
385                        warn!("Error: couldn't find room {room_id} to update latest event");
386                    }
387                }
388                RoomsListUpdate::UpdateNumUnreadMessages {
389                    room_id,
390                    is_marked_unread,
391                    unread_messages,
392                    unread_mentions,
393                } => {
394                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
395                        room.num_unread_messages = match unread_messages {
396                            UnreadMessageCount::_Unknown => 0,
397                            UnreadMessageCount::Known(count) => count,
398                        };
399                        room.num_unread_mentions = unread_mentions;
400                        room.is_marked_unread = is_marked_unread;
401                    } else {
402                        warn!(
403                            "Warning: couldn't find room {} to update unread messages count",
404                            room_id
405                        );
406                    }
407                }
408                RoomsListUpdate::UpdateRoomName {
409                    room_id,
410                    new_room_name,
411                } => {
412                    // Try to update joined room first
413                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
414                        let was_displayed = (self.display_filter)(room);
415                        // Update with the new RoomName (preserves EmptyWas semantics)
416                        room.room_name = new_room_name.into();
417                        let should_display = (self.display_filter)(room);
418                        match (was_displayed, should_display) {
419                            // No need to update the displayed rooms list.
420                            (true, true) | (false, false) => {}
421                            // Room was displayed but should no longer be displayed.
422                            (true, false) => {
423                                if room.is_direct {
424                                    self.displayed_direct_rooms
425                                        .iter()
426                                        .position(|r| r == &room_id)
427                                        .map(|index| self.displayed_direct_rooms.remove(index));
428                                } else {
429                                    self.displayed_regular_rooms
430                                        .iter()
431                                        .position(|r| r == &room_id)
432                                        .map(|index| self.displayed_regular_rooms.remove(index));
433                                }
434                            }
435                            // Room was not displayed but should now be displayed.
436                            (false, true) => {
437                                if room.is_direct {
438                                    self.displayed_direct_rooms.push(room_id);
439                                } else {
440                                    self.displayed_regular_rooms.push(room_id);
441                                }
442                            }
443                        }
444                    }
445                    // If not a joined room, try to update invited room
446                    else {
447                        let invited_rooms = self.invited_rooms.borrow_mut();
448                        if let Some(invited_room) = invited_rooms.get_mut(&room_id) {
449                            let was_displayed = (self.display_filter)(invited_room);
450                            invited_room.room_name = new_room_name.into();
451                            let should_display = (self.display_filter)(invited_room);
452                            match (was_displayed, should_display) {
453                                (true, true) | (false, false) => {}
454                                (true, false) => {
455                                    self.displayed_invited_rooms
456                                        .iter()
457                                        .position(|r| r == &room_id)
458                                        .map(|index| self.displayed_invited_rooms.remove(index));
459                                }
460                                (false, true) => {
461                                    self.displayed_invited_rooms.push(room_id.clone());
462                                }
463                            }
464                        } else {
465                            warn!(
466                                "Warning: couldn't find invited room {} to update room name",
467                                room_id
468                            );
469                        }
470                    }
471                }
472                RoomsListUpdate::UpdateTopic { room_id, new_topic } => {
473                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
474                        room.topic = Some(new_topic);
475                    }
476                }
477                RoomsListUpdate::UpdateIsDirect { room_id, is_direct } => {
478                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
479                        if room.is_direct == is_direct {
480                            continue;
481                        }
482                        enqueue_toast_notification(ToastNotificationRequest::new(
483                            format!(
484                                "{} was changed from {} to {}.",
485                                room.room_id,
486                                if room.is_direct { "direct" } else { "regular" },
487                                if is_direct { "direct" } else { "regular" }
488                            ),
489                            None,
490                            ToastNotificationVariant::Info,
491                        ));
492                        // If the room was currently displayed, remove it from the proper list.
493                        if (self.display_filter)(room) {
494                            let list_to_remove_from = if room.is_direct {
495                                &mut self.displayed_direct_rooms
496                            } else {
497                                &mut self.displayed_regular_rooms
498                            };
499                            list_to_remove_from
500                                .iter()
501                                .position(|r| r == &room_id)
502                                .map(|index| list_to_remove_from.remove(index));
503                        }
504                        // Update the room. If it should now be displayed, add it to the correct list.
505                        room.is_direct = is_direct;
506                        if (self.display_filter)(room) {
507                            if is_direct {
508                                self.displayed_direct_rooms.push(room_id);
509                            } else {
510                                self.displayed_regular_rooms.push(room_id);
511                            }
512                        }
513                    } else {
514                        error!("Error: couldn't find room {room_id} to update is_direct");
515                    }
516                }
517                RoomsListUpdate::RemoveRoom {
518                    room_id,
519                    _new_state: _,
520                } => {
521                    if let Some(removed) = self.all_joined_rooms.remove(&room_id) {
522                        info!("Removed room {room_id} from the list of all joined rooms");
523                        if removed.is_direct {
524                            self.displayed_direct_rooms
525                                .iter()
526                                .position(|r| r == &room_id)
527                                .map(|index| self.displayed_direct_rooms.remove(index));
528                        } else {
529                            self.displayed_regular_rooms
530                                .iter()
531                                .position(|r| r == &room_id)
532                                .map(|index| self.displayed_regular_rooms.remove(index));
533                        }
534                    } else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id)
535                    {
536                        info!("Removed room {room_id} from the list of all invited rooms");
537                        self.displayed_invited_rooms
538                            .iter()
539                            .position(|r| r == &room_id)
540                            .map(|index| self.displayed_invited_rooms.remove(index));
541                    }
542
543                    self.update_status_rooms_count();
544                }
545                RoomsListUpdate::ClearRooms => {
546                    self.all_joined_rooms.clear();
547                    self.displayed_direct_rooms.clear();
548                    self.displayed_regular_rooms.clear();
549                    self.invited_rooms.borrow_mut().clear();
550                    self.displayed_invited_rooms.clear();
551                    self.update_status_rooms_count();
552                }
553                RoomsListUpdate::NotLoaded => {
554                    self.status = RoomsCollectionStatus::Loading(
555                        "Loading rooms (waiting for homeserver)...".to_owned(),
556                    );
557                }
558                RoomsListUpdate::LoadedRooms { max_rooms } => {
559                    self.max_known_rooms = max_rooms;
560                    self.update_status_rooms_count();
561                }
562                RoomsListUpdate::Tags { room_id, new_tags } => {
563                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
564                        room.tags = new_tags;
565                    } else if let Some(_room) = self.invited_rooms.borrow().get(&room_id) {
566                        debug!("Ignoring updated tags update for invited room {room_id}");
567                    } else {
568                        warn!("Error: skipping updated Tags for unknown room {room_id}.");
569                    }
570                }
571                RoomsListUpdate::Status { status } => {
572                    self.status = status;
573                }
574                RoomsListUpdate::TombstonedRoom { room_id } => {
575                    if let Some(room) = self.all_joined_rooms.get_mut(&room_id) {
576                        let was_displayed = (self.display_filter)(room);
577                        room.is_tombstoned = true;
578                        let should_display = (self.display_filter)(room);
579                        match (was_displayed, should_display) {
580                            // No need to update the displayed rooms list.
581                            (true, true) | (false, false) => {}
582                            // Room was displayed but should no longer be displayed.
583                            (true, false) => {
584                                if room.is_direct {
585                                    self.displayed_direct_rooms
586                                        .iter()
587                                        .position(|r| r == &room_id)
588                                        .map(|index| self.displayed_direct_rooms.remove(index));
589                                } else {
590                                    self.displayed_regular_rooms
591                                        .iter()
592                                        .position(|r| r == &room_id)
593                                        .map(|index| self.displayed_regular_rooms.remove(index));
594                                }
595                            }
596                            // Room was not displayed but should now be displayed.
597                            (false, true) => {
598                                if room.is_direct {
599                                    self.displayed_direct_rooms.push(room_id);
600                                } else {
601                                    self.displayed_regular_rooms.push(room_id);
602                                }
603                            }
604                        }
605                    } else {
606                        warn!(
607                            "Warning: couldn't find room {room_id} to update the tombstone status"
608                        );
609                    }
610                }
611                RoomsListUpdate::_HideRoom { room_id } => {
612                    self.hidden_rooms.insert(room_id.clone());
613                    // Hiding a regular room is the most common case (e.g., after its successor is joined),
614                    // so we check that list first.
615                    if let Some(i) = self
616                        .displayed_regular_rooms
617                        .iter()
618                        .position(|r| r == &room_id)
619                    {
620                        self.displayed_regular_rooms.remove(i);
621                    } else if let Some(i) = self
622                        .displayed_direct_rooms
623                        .iter()
624                        .position(|r| r == &room_id)
625                    {
626                        self.displayed_direct_rooms.remove(i);
627                    } else if let Some(i) = self
628                        .displayed_invited_rooms
629                        .iter()
630                        .position(|r| r == &room_id)
631                    {
632                        self.displayed_invited_rooms.remove(i);
633                    }
634                }
635                RoomsListUpdate::RoomOrderUpdate(diff) => match diff {
636                    VecDiff::Append { values } => {
637                        self.all_known_rooms_order.extend(values);
638                        needs_sort = true;
639                    }
640                    VecDiff::Clear => {
641                        self.all_known_rooms_order.clear();
642                        needs_sort = true;
643                    }
644                    VecDiff::PushFront { value } => {
645                        self.all_known_rooms_order.push_front(value);
646                        needs_sort = true;
647                    }
648                    VecDiff::PushBack { value } => {
649                        self.all_known_rooms_order.push_back(value);
650                        needs_sort = true;
651                    }
652                    VecDiff::PopFront => {
653                        self.all_known_rooms_order.pop_front();
654                        needs_sort = true;
655                    }
656                    VecDiff::PopBack => {
657                        self.all_known_rooms_order.pop_back();
658                        needs_sort = true;
659                    }
660                    VecDiff::Insert { index, value } => {
661                        if index <= self.all_known_rooms_order.len() {
662                            self.all_known_rooms_order.insert(index, value);
663                            needs_sort = true;
664                        }
665                    }
666                    VecDiff::Set { index, value } => {
667                        if let Some(existing) = self.all_known_rooms_order.get_mut(index)
668                            && *existing != value
669                        {
670                            *existing = value;
671                            needs_sort = true;
672                        }
673                    }
674                    VecDiff::Remove { index } => {
675                        if index < self.all_known_rooms_order.len() {
676                            self.all_known_rooms_order.remove(index);
677                            needs_sort = true;
678                        }
679                    }
680                    VecDiff::Truncate { length } => {
681                        self.all_known_rooms_order.truncate(length);
682                        needs_sort = true;
683                    }
684                },
685                RoomsListUpdate::ApplyFilter { keywords } => {
686                    self.filter_keywords = keywords;
687                    // The filter will be applied at the end
688                }
689            }
690        }
691        if needs_sort {
692            // Only re-sort if there's no active filter
693            if self.filter_keywords.is_empty() {
694                self.update_displayed_rooms();
695            }
696        }
697        if num_updates > 0 {
698            debug!(
699                "RoomsList: processed {} updates to the list of all rooms",
700                num_updates
701            );
702            self.update_frontend_state();
703        }
704    }
705
706    pub(crate) fn handle_current_active_room(
707        &mut self,
708        updated_current_active_timeline: TimelineKind,
709        room_name: String,
710    ) -> anyhow::Result<()> {
711        // Don't do anything if the room is already active
712        if self
713            .current_active_room
714            .as_ref()
715            .is_some_and(|id| id == &updated_current_active_timeline)
716        {
717            debug!("Ignored room screen change.");
718            return Ok(());
719        } else if let Some(sender) = self.current_active_room_killer.take() {
720            sender
721                .send(())
722                .expect("Error while sending message to terminate RoomScreen thread {e:?}")
723        } else {
724            debug!("First time setting a room");
725        }
726
727        debug!("Current active room: {updated_current_active_timeline:?}");
728
729        self.current_active_room = Some(updated_current_active_timeline.clone());
730
731        let mut ui_subscriber = crate::init::singletons::subscribe_to_events()
732            .expect("Couldn't get UI subscriber event");
733        let (tx, mut rx) = oneshot::channel::<()>();
734        self.current_active_room_killer = Some(tx);
735        let updaters = self.state_updaters.clone();
736        Handle::current().spawn(async move {
737            let mut room_screen =
738                RoomScreen::new(updaters, Some(updated_current_active_timeline), room_name);
739            room_screen.show_timeline();
740
741            loop {
742                tokio::select! {
743                    _ = ui_subscriber.recv() => {
744                        room_screen.process_timeline_updates();
745                    }
746                    _ = &mut rx => {
747                        break;
748                    }
749                }
750            }
751            // as soon as this task is done,
752            // the room_screen will be dropped,
753            // and hide_timeline() will be called on drop
754        });
755        Ok(())
756
757        // This function closes the previous room thread and opens a new.
758        // this thread will also handle room actions such as sending messages
759    }
760
761    /// Updates the status message to show how many rooms have been loaded.
762    fn update_status_rooms_count(&mut self) {
763        let num_rooms = self.all_joined_rooms.len() + self.invited_rooms.borrow().len();
764        self.status = if let Some(max_rooms) = self.max_known_rooms {
765            let message = format!("Loaded {num_rooms} of {max_rooms} total rooms.");
766            if num_rooms as u32 == max_rooms {
767                RoomsCollectionStatus::Loaded(message)
768            } else {
769                RoomsCollectionStatus::Loading(message)
770            }
771        } else {
772            RoomsCollectionStatus::Loaded(format!("Loaded {num_rooms} rooms."))
773        };
774    }
775
776    /// Updates the status message to show how many rooms are currently displayed
777    /// that match the current search filter.
778    fn set_status_to_matching_rooms(&mut self) {
779        let num_rooms = self.displayed_invited_rooms.len()
780            + self.displayed_direct_rooms.len()
781            + self.displayed_regular_rooms.len();
782        self.status = match num_rooms {
783            0 => RoomsCollectionStatus::Loaded("No matching rooms found.".to_owned()),
784            1 => RoomsCollectionStatus::Loaded("Found 1 matching room.".to_owned()),
785            n => RoomsCollectionStatus::Loaded(format!("Found {} matching rooms.", n)),
786        }
787    }
788
789    /// Updates the lists of displayed rooms based on the current search filter
790    /// and redraws the RoomsList.
791    fn update_displayed_rooms(&mut self) {
792        let (filter, sort_fn) = if self.filter_keywords.is_empty() {
793            RoomDisplayFilterBuilder::default()
794                .sort_by_latest_ts()
795                .build()
796        } else {
797            RoomDisplayFilterBuilder::new()
798                .set_keywords(self.filter_keywords.clone())
799                .set_filter_criteria(RoomFilterCriteria::All)
800                .sort_by_latest_ts()
801                .build()
802        };
803        self.display_filter = filter;
804
805        self.displayed_invited_rooms = self.generate_displayed_invited_rooms(sort_fn.as_deref());
806
807        let (new_displayed_regular_rooms, new_displayed_direct_rooms) =
808            self.generate_displayed_joined_rooms(sort_fn.as_deref());
809
810        self.displayed_regular_rooms = new_displayed_regular_rooms;
811        self.displayed_direct_rooms = new_displayed_direct_rooms;
812
813        if self.filter_keywords.is_empty() {
814            self.update_status_rooms_count();
815        } else {
816            self.set_status_to_matching_rooms();
817        }
818    }
819
820    /// Generates the list of displayed invited rooms based on the current filter
821    /// and the given sort function.
822    fn generate_displayed_invited_rooms(&self, sort_fn: Option<&SortFn>) -> Vec<OwnedRoomId> {
823        let invited_rooms_ref = self.invited_rooms.borrow();
824        let filtered_invited_rooms_iter = invited_rooms_ref
825            .iter()
826            .filter(|(_room_id, room)| (self.display_filter)(*room));
827
828        if let Some(sort_fn) = sort_fn {
829            let mut filtered_invited_rooms = filtered_invited_rooms_iter.collect::<Vec<_>>();
830            filtered_invited_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b));
831            filtered_invited_rooms
832                .into_iter()
833                .map(|(room_id, _)| room_id.clone())
834                .collect()
835        } else {
836            filtered_invited_rooms_iter
837                .map(|(room_id, _)| room_id.clone())
838                .collect()
839        }
840    }
841
842    /// Generates the lists of displayed direct rooms and displayed regular rooms
843    /// based on the current filter and the given sort function.
844    fn generate_displayed_joined_rooms(
845        &self,
846        sort_fn: Option<&SortFn>,
847    ) -> (Vec<OwnedRoomId>, Vec<OwnedRoomId>) {
848        let mut new_displayed_regular_rooms = Vec::new();
849        let mut new_displayed_direct_rooms = Vec::new();
850        let mut push_room = |room_id: &OwnedRoomId, jr: &JoinedRoomInfo| {
851            let room_id = room_id.clone();
852            if jr.is_direct {
853                new_displayed_direct_rooms.push(room_id);
854            } else {
855                new_displayed_regular_rooms.push(room_id);
856            }
857        };
858
859        let filtered_joined_rooms_iter = self
860            .all_joined_rooms
861            .iter()
862            .filter(|(_room_id, room)| (self.display_filter)(*room));
863
864        if let Some(sort_fn) = sort_fn {
865            let mut filtered_rooms = filtered_joined_rooms_iter.collect::<Vec<_>>();
866            filtered_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b));
867            for (room_id, jr) in filtered_rooms.into_iter() {
868                push_room(room_id, jr)
869            }
870        } else {
871            for (room_id, jr) in filtered_joined_rooms_iter {
872                push_room(room_id, jr)
873            }
874        }
875
876        (new_displayed_regular_rooms, new_displayed_direct_rooms)
877    }
878}