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, pin::pin, 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 _, stream};
24use matrix_sdk::{
25    Client, Room, RoomRecencyStamp, RoomState, SlidingSync, SlidingSyncList,
26    task_monitor::BackgroundTaskHandle,
27};
28use matrix_sdk_base::RoomInfoNotableUpdate;
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        new_sorter_latest_event, new_sorter_lexicographic, new_sorter_name, new_sorter_recency,
41    },
42};
43
44/// A `RoomList` represents a list of rooms, from a
45/// [`RoomListService`](super::RoomListService).
46#[derive(Debug)]
47pub struct RoomList {
48    client: Client,
49    sliding_sync_list: SlidingSyncList,
50    loading_state: SharedObservable<RoomListLoadingState>,
51    _loading_state_task: BackgroundTaskHandle,
52}
53
54impl RoomList {
55    pub(super) async fn new(
56        client: &Client,
57        sliding_sync: &Arc<SlidingSync>,
58        sliding_sync_list_name: &str,
59        room_list_service_state: Subscriber<State>,
60    ) -> Result<Self, Error> {
61        let sliding_sync_list = sliding_sync
62            .on_list(sliding_sync_list_name, |list| ready(list.clone()))
63            .await
64            .ok_or_else(|| Error::UnknownList(sliding_sync_list_name.to_owned()))?;
65
66        let loading_state =
67            SharedObservable::new(match sliding_sync_list.maximum_number_of_rooms() {
68                Some(maximum_number_of_rooms) => RoomListLoadingState::Loaded {
69                    maximum_number_of_rooms: Some(maximum_number_of_rooms),
70                },
71                None => RoomListLoadingState::NotLoaded,
72            });
73
74        Ok(Self {
75            client: client.clone(),
76            sliding_sync_list: sliding_sync_list.clone(),
77            loading_state: loading_state.clone(),
78            _loading_state_task: client
79                .task_monitor()
80                .spawn_infinite_task("room_list::loading_state_task", async move {
81                    let mut room_list_service_state = pin!(room_list_service_state);
82
83                    // As soon as `RoomListService` changes its state, if it
84                    // isn't `Terminated` nor `Error`, we know we have fetched
85                    // something, so the room list is loaded.
86                    while let Some(state) = room_list_service_state.next().await {
87                        use State::*;
88
89                        match state {
90                            Terminated { .. } | Error { .. } | Init => (),
91                            SettingUp | Recovering | Running => break,
92                        }
93                    }
94
95                    // Let's jump from `NotLoaded` to `Loaded`.
96                    let maximum_number_of_rooms = sliding_sync_list.maximum_number_of_rooms();
97
98                    loading_state.set(RoomListLoadingState::Loaded { maximum_number_of_rooms });
99
100                    // Wait for updates on the maximum number of rooms to update
101                    // 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 room
139    /// 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        let room_info_notable_update_receiver = self.client.room_info_notable_update_receiver();
146        let list = self.sliding_sync_list.clone();
147
148        let filter_fn_cell = AsyncCell::shared();
149
150        let limit = SharedObservable::<usize>::new(page_size);
151        let limit_stream = limit.subscribe();
152
153        let dynamic_entries_controller = RoomListDynamicEntriesController::new(
154            filter_fn_cell.clone(),
155            page_size,
156            limit,
157            list.maximum_number_of_rooms_stream(),
158        );
159
160        let stream = stream! {
161            loop {
162                let filter_fn = filter_fn_cell.take().await;
163
164                let (raw_values, raw_stream) = self.entries();
165                let values = raw_values.into_iter().map(Into::into).collect::<Vector<RoomListItem>>();
166
167                // Combine normal stream events with other updates from rooms
168                let stream = merge_stream_and_receiver(values.clone(), raw_stream, room_info_notable_update_receiver.resubscribe());
169
170                let (values, stream) = (values, stream)
171                    .filter(filter_fn)
172                    .sort_by(new_sorter_lexicographic(vec![
173                        // Sort by latest event's kind, i.e. put the rooms with
174                        // a **local** latest event first.
175                        Box::new(new_sorter_latest_event()),
176                        // Sort rooms by their recency (either by looking at
177                        // their latest event's timestamp, or their
178                        // `recency_stamp`).
179                        Box::new(new_sorter_recency()),
180                        // Finally, sort by name.
181                        Box::new(new_sorter_name()),
182                    ]))
183                    .dynamic_head_with_initial_value(page_size, limit_stream.clone());
184
185                // Clearing the stream before chaining with the real stream.
186                yield stream::once(ready(vec![VectorDiff::Reset { values }]))
187                    .chain(stream);
188            }
189        }
190        .fuse()
191        .switch();
192
193        (stream, dynamic_entries_controller)
194    }
195}
196
197/// This function remembers the current state of the unfiltered room list, so it
198/// knows where all rooms are. When the receiver is triggered, a Set operation
199/// for the room position is inserted to the stream.
200fn merge_stream_and_receiver(
201    mut current_values: Vector<RoomListItem>,
202    raw_stream: impl Stream<Item = Vec<VectorDiff<Room>>>,
203    mut room_info_notable_update_receiver: broadcast::Receiver<RoomInfoNotableUpdate>,
204) -> impl Stream<Item = Vec<VectorDiff<RoomListItem>>> {
205    stream! {
206        let mut raw_stream = pin!(raw_stream);
207
208        loop {
209            select! {
210                // We want to give priority on updates from `raw_stream` as it will necessarily trigger a “refresh” of the rooms.
211                biased;
212
213                diffs = raw_stream.next() => {
214                    if let Some(diffs) = diffs {
215                        let diffs = diffs.into_iter().map(|diff| diff.map(RoomListItem::from)).collect::<Vec<_>>();
216
217                        for diff in &diffs {
218                            diff.clone().map(|room| {
219                                trace!(room = %room.room_id(), "updated in response");
220                                room
221                            }).apply(&mut current_values);
222                        }
223
224                        yield diffs;
225                    } else {
226                        // Restart immediately, don't keep on waiting for the receiver
227                        break;
228                    }
229                }
230
231                update = room_info_notable_update_receiver.recv() => {
232                    match update {
233                        Ok(update) => {
234                            // Drain every update already sitting in the
235                            // channel, so that a burst of updates (e.g. a
236                            // batch of freshly computed latest events, or the
237                            // per-room updates of one sync response) is
238                            // processed as a single batch of `VectorDiff`s.
239                            // Each room is kept once, in arrival order.
240                            let mut room_ids = Vec::new();
241                            let mut seen = std::collections::HashSet::new();
242                            let mut add = |update: RoomInfoNotableUpdate| {
243                                if seen.insert(update.room_id.clone()) {
244                                    room_ids.push(update.room_id);
245                                }
246                            };
247
248                            add(update);
249
250                            loop {
251                                use broadcast::error::TryRecvError;
252
253                                match room_info_notable_update_receiver.try_recv() {
254                                    Ok(update) => add(update),
255                                    Err(TryRecvError::Lagged(n)) => {
256                                        error!(number_of_missed_updates = n, "Lag when receiving room info notable update");
257                                    }
258                                    Err(TryRecvError::Empty | TryRecvError::Closed) => break,
259                                }
260                            }
261
262                            // Emit a `VectorDiff::Set` for each updated room.
263                            let mut diffs = Vec::with_capacity(room_ids.len());
264
265                            for room_id in room_ids {
266                                if let Some(index) = current_values.iter().position(|room| room.room_id() == room_id) {
267                                    let room = &mut current_values[index];
268                                    room.refresh_cached_data();
269
270                                    diffs.push(VectorDiff::Set { index, value: room.clone() });
271                                }
272                            }
273
274                            if !diffs.is_empty() {
275                                yield diffs;
276                            }
277                        }
278
279                        Err(RecvError::Closed) => {
280                            error!("Cannot receive room info notable updates because the sender has been closed");
281
282                            break;
283                        }
284
285                        Err(RecvError::Lagged(n)) => {
286                            error!(number_of_missed_updates = n, "Lag when receiving room info notable update");
287                        }
288                    }
289                }
290            }
291        }
292    }
293}
294
295/// The loading state of a [`RoomList`].
296///
297/// When a [`RoomList`] is displayed to the user, it can be in various states.
298/// This enum tries to represent those states with a correct level of
299/// abstraction.
300///
301/// See [`RoomList::loading_state`].
302#[derive(Clone, Debug, PartialEq, Eq)]
303pub enum RoomListLoadingState {
304    /// The [`RoomList`] has not been loaded yet, i.e. a sync might run or not
305    /// run at all, there is nothing to show in this `RoomList` yet. It's a good
306    /// opportunity to show a placeholder to the user.
307    ///
308    /// From [`Self::NotLoaded`], it's only possible to move to
309    /// [`Self::Loaded`].
310    NotLoaded,
311
312    /// The [`RoomList`] has been loaded, i.e. a sync has been run, or more
313    /// syncs are running, there is probably something to show to the user.
314    /// Either the user has 0 room, in this case, it's a good opportunity to
315    /// show a special screen for that, or the user has multiple rooms, and it's
316    /// the classical room list.
317    ///
318    /// The number of rooms is represented by `maximum_number_of_rooms`.
319    ///
320    /// From [`Self::Loaded`], it's not possible to move back to
321    /// [`Self::NotLoaded`].
322    Loaded {
323        /// The maximum number of rooms a [`RoomList`] contains.
324        ///
325        /// It does not mean that there are exactly this many rooms to display.
326        /// The room entries are represented by [`RoomListItem`]. The room entry
327        /// might have been synced or not synced yet, but we know for sure (from
328        /// the server), that there will be this amount of rooms in the list at
329        /// the end.
330        ///
331        /// Note that it's an `Option`, because it may be possible that the
332        /// server did miss to send us this value. It's up to you, dear reader,
333        /// to know which default to adopt in case of `None`.
334        maximum_number_of_rooms: Option<u32>,
335    },
336}
337
338/// Controller for the [`RoomList`] dynamic entries.
339///
340/// To get one value of this type, use
341/// [`RoomList::entries_with_dynamic_adapters`]
342pub struct RoomListDynamicEntriesController {
343    filter: Arc<AsyncCell<BoxedFilterFn>>,
344    page_size: usize,
345    limit: SharedObservable<usize>,
346    maximum_number_of_rooms: Subscriber<Option<u32>>,
347}
348
349impl RoomListDynamicEntriesController {
350    fn new(
351        filter: Arc<AsyncCell<BoxedFilterFn>>,
352        page_size: usize,
353        limit_stream: SharedObservable<usize>,
354        maximum_number_of_rooms: Subscriber<Option<u32>>,
355    ) -> Self {
356        Self { filter, page_size, limit: limit_stream, maximum_number_of_rooms }
357    }
358
359    /// Set the filter.
360    ///
361    /// If the associated stream has been dropped, returns `false` to indicate
362    /// the operation didn't have an effect.
363    pub fn set_filter(&self, filter: BoxedFilterFn) -> bool {
364        if Arc::strong_count(&self.filter) == 1 {
365            // there is no other reference to the boxed filter fn, setting it
366            // would be pointless (no new references can be created from self,
367            // either)
368            false
369        } else {
370            self.filter.set(filter);
371            true
372        }
373    }
374
375    /// Add one page, i.e. view `page_size` more entries in the room list if
376    /// any.
377    pub fn add_one_page(&self) {
378        let Some(max) = self.maximum_number_of_rooms.get() else {
379            return;
380        };
381
382        let max: usize = max.try_into().unwrap();
383        let limit = self.limit.get();
384
385        if limit < max {
386            // With this logic, it is possible that `limit` becomes greater than
387            // `max` if `max - limit < page_size`, and that's perfectly fine.
388            // It's OK to have a `limit` greater than `max`, but it's not OK to
389            // increase the limit indefinitely.
390            self.limit.set_if_not_eq(limit + self.page_size);
391        }
392    }
393
394    /// Reset the one page, i.e. forget all pages and move back to the first
395    /// page.
396    pub fn reset_to_one_page(&self) {
397        self.limit.set_if_not_eq(self.page_size);
398    }
399}
400
401/// A facade type that derefs to [`Room`] and that caches data from
402/// [`RoomInfo`].
403///
404/// Why caching data? [`RoomInfo`] is behind a lock. Every time a filter or a
405/// sorter calls a method on [`Room`], it's likely to hit the lock in front of
406/// [`RoomInfo`]. It creates a big contention. By caching the data, it avoids
407/// hitting the lock, improving the performance greatly.
408///
409/// Data are refreshed in `merge_stream_and_receiver` (private function).
410///
411/// [`RoomInfo`]: matrix_sdk::RoomInfo
412#[derive(Clone, Debug)]
413pub struct RoomListItem {
414    /// The inner room.
415    inner: Room,
416
417    /// Cache of `Room::latest_event_timestamp`.
418    pub(super) cached_latest_event_timestamp: Option<MilliSecondsSinceUnixEpoch>,
419
420    /// Cache of `Room::latest_event_is_unsent`.
421    pub(super) cached_latest_event_is_unsent: bool,
422
423    /// Cache of `Room::recency_stamp`.
424    pub(super) cached_recency_stamp: Option<RoomRecencyStamp>,
425
426    /// Cache of `Room::cached_display_name`, already as a string.
427    pub(super) cached_display_name: Option<String>,
428
429    /// Cache of `Room::is_space`.
430    pub(super) cached_is_space: bool,
431
432    // Cache of `Room::state`.
433    pub(super) cached_state: RoomState,
434}
435
436impl RoomListItem {
437    #[cfg(test)]
438    pub(super) fn inner(&self) -> &Room {
439        &self.inner
440    }
441
442    /// Deconstruct to the inner room value.
443    pub fn into_inner(self) -> Room {
444        self.inner
445    }
446
447    /// Refresh the cached data.
448    pub(super) fn refresh_cached_data(&mut self) {
449        self.cached_latest_event_timestamp = self.inner.latest_event_timestamp();
450        self.cached_latest_event_is_unsent = self.inner.latest_event_is_unsent();
451        self.cached_recency_stamp = self.inner.recency_stamp();
452        self.cached_display_name = self.inner.cached_display_name().map(|name| name.to_string());
453        self.cached_is_space = self.inner.is_space();
454        self.cached_state = self.inner.state();
455    }
456}
457
458impl From<Room> for RoomListItem {
459    fn from(inner: Room) -> Self {
460        let cached_latest_event_timestamp = inner.latest_event_timestamp();
461        let cached_latest_event_is_unsent = inner.latest_event_is_unsent();
462        let cached_recency_stamp = inner.recency_stamp();
463        let cached_display_name = inner.cached_display_name().map(|name| name.to_string());
464        let cached_is_space = inner.is_space();
465        let cached_state = inner.state();
466
467        Self {
468            inner,
469            cached_latest_event_timestamp,
470            cached_latest_event_is_unsent,
471            cached_recency_stamp,
472            cached_display_name,
473            cached_is_space,
474            cached_state,
475        }
476    }
477}
478
479impl Deref for RoomListItem {
480    type Target = Room;
481
482    fn deref(&self) -> &Self::Target {
483        &self.inner
484    }
485}