Skip to main content

matrix_sdk_ui/room_list_service/
room_list.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for that specific language governing permissions and
13// limitations under the License.
14
15use std::{future::ready, ops::Deref, sync::Arc};
16
17use async_cell::sync::AsyncCell;
18use async_rx::StreamExt as _;
19use async_stream::stream;
20use eyeball::{SharedObservable, Subscriber};
21use eyeball_im::{Vector, VectorDiff};
22use eyeball_im_util::vector::VectorObserverExt;
23use futures_util::{Stream, StreamExt as _, pin_mut, stream};
24use matrix_sdk::{
25    Client, Room, RoomRecencyStamp, RoomState, SlidingSync, SlidingSyncList,
26    task_monitor::BackgroundTaskHandle,
27};
28use matrix_sdk_base::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
29use ruma::MilliSecondsSinceUnixEpoch;
30use tokio::{
31    select,
32    sync::broadcast::{self, error::RecvError},
33};
34use tracing::{error, trace};
35
36use super::{
37    Error, State,
38    filters::BoxedFilterFn,
39    sorters::{
40        BoxedSorterFn, new_sorter_latest_event, new_sorter_lexicographic, new_sorter_name,
41        new_sorter_recency,
42    },
43};
44
45/// A `RoomList` represents a list of rooms, from a
46/// [`RoomListService`](super::RoomListService).
47#[derive(Debug)]
48pub struct RoomList {
49    client: Client,
50    sliding_sync_list: SlidingSyncList,
51    loading_state: SharedObservable<RoomListLoadingState>,
52    _loading_state_task: BackgroundTaskHandle,
53}
54
55impl RoomList {
56    pub(super) async fn new(
57        client: &Client,
58        sliding_sync: &Arc<SlidingSync>,
59        sliding_sync_list_name: &str,
60        room_list_service_state: Subscriber<State>,
61    ) -> Result<Self, Error> {
62        let sliding_sync_list = sliding_sync
63            .on_list(sliding_sync_list_name, |list| ready(list.clone()))
64            .await
65            .ok_or_else(|| Error::UnknownList(sliding_sync_list_name.to_owned()))?;
66
67        let loading_state =
68            SharedObservable::new(match sliding_sync_list.maximum_number_of_rooms() {
69                Some(maximum_number_of_rooms) => RoomListLoadingState::Loaded {
70                    maximum_number_of_rooms: Some(maximum_number_of_rooms),
71                },
72                None => RoomListLoadingState::NotLoaded,
73            });
74
75        Ok(Self {
76            client: client.clone(),
77            sliding_sync_list: sliding_sync_list.clone(),
78            loading_state: loading_state.clone(),
79            _loading_state_task: client
80                .task_monitor()
81                .spawn_background_task("room_list::loading_state_task", async move {
82                    pin_mut!(room_list_service_state);
83
84                    // As soon as `RoomListService` changes its state, if it isn't
85                    // `Terminated` nor `Error`, we know we have fetched something,
86                    // so the room list is loaded.
87                    while let Some(state) = room_list_service_state.next().await {
88                        use State::*;
89
90                        match state {
91                            Terminated { .. } | Error { .. } | Init => (),
92                            SettingUp | Recovering | Running => break,
93                        }
94                    }
95
96                    // Let's jump from `NotLoaded` to `Loaded`.
97                    let maximum_number_of_rooms = sliding_sync_list.maximum_number_of_rooms();
98
99                    loading_state.set(RoomListLoadingState::Loaded { maximum_number_of_rooms });
100
101                    // Wait for updates on the maximum number of rooms to update again.
102                    let mut maximum_number_of_rooms_stream =
103                        sliding_sync_list.maximum_number_of_rooms_stream();
104
105                    while let Some(maximum_number_of_rooms) =
106                        maximum_number_of_rooms_stream.next().await
107                    {
108                        loading_state.set(RoomListLoadingState::Loaded { maximum_number_of_rooms });
109                    }
110                })
111                .abort_on_drop(),
112        })
113    }
114
115    /// Get a subscriber to the room list loading state.
116    ///
117    /// This method will send out the current loading state as the first update.
118    ///
119    /// See [`RoomListLoadingState`].
120    pub fn loading_state(&self) -> Subscriber<RoomListLoadingState> {
121        self.loading_state.subscribe_reset()
122    }
123
124    /// Get a stream of rooms.
125    fn entries(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + '_) {
126        self.client.rooms_stream()
127    }
128
129    /// Get a configurable stream of rooms.
130    ///
131    /// It's possible to provide a filter that will filter out room list
132    /// entries, and that it's also possible to “paginate” over the entries by
133    /// `page_size`. The rooms are also sorted.
134    ///
135    /// The returned stream will only start yielding diffs once a filter is set
136    /// through the returned [`RoomListDynamicEntriesController`]. For every
137    /// call to [`RoomListDynamicEntriesController::set_filter`], the stream
138    /// will yield a [`VectorDiff::Reset`] followed by any updates of the
139    /// room list under that filter (until the next reset).
140    pub fn entries_with_dynamic_adapters(
141        &self,
142        page_size: usize,
143    ) -> (impl Stream<Item = Vec<VectorDiff<RoomListItem>>> + '_, RoomListDynamicEntriesController)
144    {
145        self.entries_with_dynamic_adapters_impl(page_size, false)
146    }
147
148    #[doc(hidden)]
149    pub fn entries_with_dynamic_adapters_with(
150        &self,
151        page_size: usize,
152        enable_latest_event_sorter: bool,
153    ) -> (impl Stream<Item = Vec<VectorDiff<RoomListItem>>> + '_, RoomListDynamicEntriesController)
154    {
155        self.entries_with_dynamic_adapters_impl(page_size, enable_latest_event_sorter)
156    }
157
158    fn entries_with_dynamic_adapters_impl(
159        &self,
160        page_size: usize,
161        enable_latest_event_sorter: bool,
162    ) -> (impl Stream<Item = Vec<VectorDiff<RoomListItem>>> + '_, RoomListDynamicEntriesController)
163    {
164        let room_info_notable_update_receiver = self.client.room_info_notable_update_receiver();
165        let list = self.sliding_sync_list.clone();
166
167        let filter_fn_cell = AsyncCell::shared();
168
169        let limit = SharedObservable::<usize>::new(page_size);
170        let limit_stream = limit.subscribe();
171
172        let dynamic_entries_controller = RoomListDynamicEntriesController::new(
173            filter_fn_cell.clone(),
174            page_size,
175            limit,
176            list.maximum_number_of_rooms_stream(),
177        );
178
179        let stream = stream! {
180            loop {
181                let filter_fn = filter_fn_cell.take().await;
182
183                let (raw_values, raw_stream) = self.entries();
184                let values = raw_values.into_iter().map(Into::into).collect::<Vector<RoomListItem>>();
185
186                // Combine normal stream events with other updates from rooms
187                let stream = merge_stream_and_receiver(values.clone(), raw_stream, room_info_notable_update_receiver.resubscribe());
188
189                let mut sorters: Vec<BoxedSorterFn> = Vec::with_capacity(3);
190
191                if enable_latest_event_sorter {
192                    // Sort by latest event's kind, i.e. put the rooms with a
193                    // **local** latest event first.
194                    sorters.push(Box::new(new_sorter_latest_event()));
195                }
196
197                // Sort rooms by their recency (either by looking
198                // at their latest event's timestamp, or their
199                // `recency_stamp`).
200                sorters.push(Box::new(new_sorter_recency()));
201                // Finally, sort by name.
202                sorters.push(Box::new(new_sorter_name()));
203
204                let (values, stream) = (values, stream)
205                    .filter(filter_fn)
206                    .sort_by(new_sorter_lexicographic(sorters))
207                    .dynamic_head_with_initial_value(page_size, limit_stream.clone());
208
209                // Clearing the stream before chaining with the real stream.
210                yield stream::once(ready(vec![VectorDiff::Reset { values }]))
211                    .chain(stream);
212            }
213        }
214        .switch();
215
216        (stream, dynamic_entries_controller)
217    }
218}
219
220/// This function remembers the current state of the unfiltered room list, so it
221/// knows where all rooms are. When the receiver is triggered, a Set operation
222/// for the room position is inserted to the stream.
223fn merge_stream_and_receiver(
224    mut current_values: Vector<RoomListItem>,
225    raw_stream: impl Stream<Item = Vec<VectorDiff<Room>>>,
226    mut room_info_notable_update_receiver: broadcast::Receiver<RoomInfoNotableUpdate>,
227) -> impl Stream<Item = Vec<VectorDiff<RoomListItem>>> {
228    stream! {
229        pin_mut!(raw_stream);
230
231        loop {
232            select! {
233                // We want to give priority on updates from `raw_stream` as it will necessarily trigger a “refresh” of the rooms.
234                biased;
235
236                diffs = raw_stream.next() => {
237                    if let Some(diffs) = diffs {
238                        let diffs = diffs.into_iter().map(|diff| diff.map(RoomListItem::from)).collect::<Vec<_>>();
239
240                        for diff in &diffs {
241                            diff.clone().map(|room| {
242                                trace!(room = %room.room_id(), "updated in response");
243                                room
244                            }).apply(&mut current_values);
245                        }
246
247                        yield diffs;
248                    } else {
249                        // Restart immediately, don't keep on waiting for the receiver
250                        break;
251                    }
252                }
253
254                update = room_info_notable_update_receiver.recv() => {
255                    match update {
256                        Ok(update) => {
257                            // Filter which _reason_ can trigger an update of
258                            // the room list.
259                            //
260                            // If the update is strictly about the
261                            // `RECENCY_STAMP`, let's ignore it, because the
262                            // Latest Event type is used to sort the room list
263                            // by recency already. We don't want to trigger an
264                            // update because of `RECENCY_STAMP`.
265                            //
266                            // If the update contains more reasons than
267                            // `RECENCY_STAMP`, then it's fine. That's why we
268                            // are using `==` instead of `contains`.
269                            if update.reasons == RoomInfoNotableUpdateReasons::RECENCY_STAMP {
270                                continue;
271                            }
272
273                            // Emit a `VectorDiff::Set` for the specific rooms.
274                            if let Some(index) = current_values.iter().position(|room| room.room_id() == update.room_id) {
275                                let mut room = current_values[index].clone();
276                                room.refresh_cached_data();
277
278                                yield vec![VectorDiff::Set { index, value: room }];
279                            }
280                        }
281
282                        Err(RecvError::Closed) => {
283                            error!("Cannot receive room info notable updates because the sender has been closed");
284
285                            break;
286                        }
287
288                        Err(RecvError::Lagged(n)) => {
289                            error!(number_of_missed_updates = n, "Lag when receiving room info notable update");
290                        }
291                    }
292                }
293            }
294        }
295    }
296}
297
298/// The loading state of a [`RoomList`].
299///
300/// When a [`RoomList`] is displayed to the user, it can be in various states.
301/// This enum tries to represent those states with a correct level of
302/// abstraction.
303///
304/// See [`RoomList::loading_state`].
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub enum RoomListLoadingState {
307    /// The [`RoomList`] has not been loaded yet, i.e. a sync might run
308    /// or not run at all, there is nothing to show in this `RoomList` yet.
309    /// It's a good opportunity to show a placeholder to the user.
310    ///
311    /// From [`Self::NotLoaded`], it's only possible to move to
312    /// [`Self::Loaded`].
313    NotLoaded,
314
315    /// The [`RoomList`] has been loaded, i.e. a sync has been run, or more
316    /// syncs are running, there is probably something to show to the user.
317    /// Either the user has 0 room, in this case, it's a good opportunity to
318    /// show a special screen for that, or the user has multiple rooms, and it's
319    /// the classical room list.
320    ///
321    /// The number of rooms is represented by `maximum_number_of_rooms`.
322    ///
323    /// From [`Self::Loaded`], it's not possible to move back to
324    /// [`Self::NotLoaded`].
325    Loaded {
326        /// The maximum number of rooms a [`RoomList`] contains.
327        ///
328        /// It does not mean that there are exactly this many rooms to display.
329        /// The room entries are represented by [`RoomListItem`]. The room entry
330        /// might have been synced or not synced yet, but we know for sure
331        /// (from the server), that there will be this amount of rooms in the
332        /// list at the end.
333        ///
334        /// Note that it's an `Option`, because it may be possible that the
335        /// server did miss to send us this value. It's up to you, dear reader,
336        /// to know which default to adopt in case of `None`.
337        maximum_number_of_rooms: Option<u32>,
338    },
339}
340
341/// Controller for the [`RoomList`] dynamic entries.
342///
343/// To get one value of this type, use
344/// [`RoomList::entries_with_dynamic_adapters`]
345pub struct RoomListDynamicEntriesController {
346    filter: Arc<AsyncCell<BoxedFilterFn>>,
347    page_size: usize,
348    limit: SharedObservable<usize>,
349    maximum_number_of_rooms: Subscriber<Option<u32>>,
350}
351
352impl RoomListDynamicEntriesController {
353    fn new(
354        filter: Arc<AsyncCell<BoxedFilterFn>>,
355        page_size: usize,
356        limit_stream: SharedObservable<usize>,
357        maximum_number_of_rooms: Subscriber<Option<u32>>,
358    ) -> Self {
359        Self { filter, page_size, limit: limit_stream, maximum_number_of_rooms }
360    }
361
362    /// Set the filter.
363    ///
364    /// If the associated stream has been dropped, returns `false` to indicate
365    /// the operation didn't have an effect.
366    pub fn set_filter(&self, filter: BoxedFilterFn) -> bool {
367        if Arc::strong_count(&self.filter) == 1 {
368            // there is no other reference to the boxed filter fn, setting it
369            // would be pointless (no new references can be created from self,
370            // either)
371            false
372        } else {
373            self.filter.set(filter);
374            true
375        }
376    }
377
378    /// Add one page, i.e. view `page_size` more entries in the room list if
379    /// any.
380    pub fn add_one_page(&self) {
381        let Some(max) = self.maximum_number_of_rooms.get() else {
382            return;
383        };
384
385        let max: usize = max.try_into().unwrap();
386        let limit = self.limit.get();
387
388        if limit < max {
389            // With this logic, it is possible that `limit` becomes greater than `max` if
390            // `max - limit < page_size`, and that's perfectly fine. It's OK to have a
391            // `limit` greater than `max`, but it's not OK to increase the limit
392            // indefinitely.
393            self.limit.set_if_not_eq(limit + self.page_size);
394        }
395    }
396
397    /// Reset the one page, i.e. forget all pages and move back to the first
398    /// page.
399    pub fn reset_to_one_page(&self) {
400        self.limit.set_if_not_eq(self.page_size);
401    }
402}
403
404/// A facade type that derefs to [`Room`] and that caches data from
405/// [`RoomInfo`].
406///
407/// Why caching data? [`RoomInfo`] is behind a lock. Every time a filter or a
408/// sorter calls a method on [`Room`], it's likely to hit the lock in front of
409/// [`RoomInfo`]. It creates a big contention. By caching the data, it avoids
410/// hitting the lock, improving the performance greatly.
411///
412/// Data are refreshed in `merge_stream_and_receiver` (private function).
413///
414/// [`RoomInfo`]: matrix_sdk::RoomInfo
415#[derive(Clone, Debug)]
416pub struct RoomListItem {
417    /// The inner room.
418    inner: Room,
419
420    /// Cache of `Room::latest_event_timestamp`.
421    pub(super) cached_latest_event_timestamp: Option<MilliSecondsSinceUnixEpoch>,
422
423    /// Cache of `Room::latest_event_is_unsent`.
424    pub(super) cached_latest_event_is_unsent: bool,
425
426    /// Cache of `Room::recency_stamp`.
427    pub(super) cached_recency_stamp: Option<RoomRecencyStamp>,
428
429    /// Cache of `Room::cached_display_name`, already as a string.
430    pub(super) cached_display_name: Option<String>,
431
432    /// Cache of `Room::is_space`.
433    pub(super) cached_is_space: bool,
434
435    // Cache of `Room::state`.
436    pub(super) cached_state: RoomState,
437}
438
439impl RoomListItem {
440    /// Deconstruct to the inner room value.
441    pub fn into_inner(self) -> Room {
442        self.inner
443    }
444
445    /// Refresh the cached data.
446    pub(super) fn refresh_cached_data(&mut self) {
447        self.cached_latest_event_timestamp = self.inner.latest_event_timestamp();
448        self.cached_latest_event_is_unsent = self.inner.latest_event_is_unsent();
449        self.cached_recency_stamp = self.inner.recency_stamp();
450        self.cached_display_name = self.inner.cached_display_name().map(|name| name.to_string());
451        self.cached_is_space = self.inner.is_space();
452        self.cached_state = self.inner.state();
453    }
454}
455
456impl From<Room> for RoomListItem {
457    fn from(inner: Room) -> Self {
458        let cached_latest_event_timestamp = inner.latest_event_timestamp();
459        let cached_latest_event_is_unsent = inner.latest_event_is_unsent();
460        let cached_recency_stamp = inner.recency_stamp();
461        let cached_display_name = inner.cached_display_name().map(|name| name.to_string());
462        let cached_is_space = inner.is_space();
463        let cached_state = inner.state();
464
465        Self {
466            inner,
467            cached_latest_event_timestamp,
468            cached_latest_event_is_unsent,
469            cached_recency_stamp,
470            cached_display_name,
471            cached_is_space,
472            cached_state,
473        }
474    }
475}
476
477impl Deref for RoomListItem {
478    type Target = Room;
479
480    fn deref(&self) -> &Self::Target {
481        &self.inner
482    }
483}