Skip to main content

matrix_sdk_ui/room_list_service/
mod.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
15//! `RoomListService` API.
16//!
17//! The `RoomListService` is a UI API dedicated to present a list of Matrix
18//! rooms to the user. The syncing is handled by [`SlidingSync`]. The idea is to
19//! expose a simple API to handle most of the client app use cases, like:
20//! Showing and updating a list of rooms, filtering a list of rooms, handling
21//! particular updates of a range of rooms (the ones the client app is showing
22//! to the view, i.e. the rooms present in the viewport) etc.
23//!
24//! As such, the `RoomListService` works as an opinionated state machine. The
25//! states are defined by [`State`]. Actions are attached to the each state
26//! transition.
27//!
28//! The API is purposely small. Sliding Sync is versatile. `RoomListService` is
29//! _one_ specific usage of Sliding Sync.
30//!
31//! # Basic principle
32//!
33//! `RoomListService` works with 1 Sliding Sync List:
34//!
35//! * `all_rooms` (referred by the constant [`ALL_ROOMS_LIST_NAME`]) is the only
36//!   list. Its goal is to load all the user' rooms. It starts with a
37//!   [`SlidingSyncMode::Selective`] sync-mode with a small range (i.e. a small
38//!   set of rooms) to load the first rooms quickly, and then updates to a
39//!   [`SlidingSyncMode::Growing`] sync-mode to load the remaining rooms “in the
40//!   background”: it will sync the existing rooms and will fetch new rooms, by
41//!   a certain batch size.
42//!
43//! This behavior has proven to be empirically satisfying to provide a fast and
44//! fluid user experience for a Matrix client.
45//!
46//! [`RoomListService::all_rooms`] provides a way to get a [`RoomList`] for all
47//! the rooms. From that, calling [`RoomList::entries_with_dynamic_adapters`]
48//! provides a way to get a stream of rooms. This stream is sorted, can be
49//! filtered, and the filter can be changed over time.
50//!
51//! [`RoomListService::state`] provides a way to get a stream of the state
52//! machine's state, which can be pretty helpful for the client app.
53
54pub mod filters;
55mod room_list;
56pub mod sorters;
57mod state;
58
59use std::{sync::Arc, time::Duration};
60
61use async_stream::stream;
62use eyeball::Subscriber;
63use futures_util::{Stream, StreamExt, pin_mut};
64use matrix_sdk::{
65    Client, Error as SlidingSyncError, Room, SlidingSync, SlidingSyncList, SlidingSyncMode,
66    event_cache::EventCacheError, sliding_sync::PollTimeout, timeout::timeout,
67};
68pub use room_list::*;
69use ruma::{
70    OwnedRoomId, RoomId, UInt, api::client::sync::sync_events::v5 as http, assign,
71    events::StateEventType,
72};
73pub use state::*;
74use thiserror::Error;
75use tracing::{debug, error, warn};
76
77/// The default `required_state` constant value for sliding sync lists and
78/// sliding sync room subscriptions.
79const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[
80    (StateEventType::RoomName, ""),
81    (StateEventType::RoomEncryption, ""),
82    (StateEventType::RoomMember, "$LAZY"),
83    (StateEventType::RoomMember, "$ME"),
84    (StateEventType::RoomTopic, ""),
85    // Temporary workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5285
86    (StateEventType::RoomAvatar, ""),
87    (StateEventType::RoomCanonicalAlias, ""),
88    (StateEventType::RoomPowerLevels, ""),
89    (StateEventType::CallMember, "*"),
90    (StateEventType::RoomJoinRules, ""),
91    (StateEventType::RoomTombstone, ""),
92    // Those two events are required to properly compute room previews.
93    // `StateEventType::RoomCreate` is also necessary to compute the room
94    // version, and thus handling the tombstoned room correctly.
95    (StateEventType::RoomCreate, ""),
96    (StateEventType::RoomHistoryVisibility, ""),
97    // Required to correctly calculate the room display name.
98    (StateEventType::MemberHints, ""),
99    (StateEventType::SpaceParent, "*"),
100    (StateEventType::SpaceChild, "*"),
101    // Required for live location sharing to work - beacon events reference this state.
102    (StateEventType::BeaconInfo, "*"),
103    // Required for `Room::retention`/`Room::effective_retention` (MSC1763) to
104    // see room-level retention overrides.
105    (StateEventType::RoomRetention, ""),
106];
107
108/// The default `required_state` constant value for sliding sync room
109/// subscriptions that must be added to `DEFAULT_REQUIRED_STATE`.
110const DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE: &[(StateEventType, &str)] =
111    &[(StateEventType::RoomPinnedEvents, "")];
112
113/// The default Sliding Sync connection ID for the room list service.
114pub(crate) const DEFAULT_CONNECTION_ID: &str = "room-list";
115
116/// The default timeline limit for the room list service.
117pub(crate) const DEFAULT_LIST_TIMELINE_LIMIT: u32 = 1;
118
119/// The default `timeline_limit` value when used with room subscriptions.
120const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20;
121
122/// The [`RoomListService`] type. See the module's documentation to learn more.
123#[derive(Debug)]
124pub struct RoomListService {
125    /// Client that has created this [`RoomListService`].
126    client: Client,
127
128    /// The Sliding Sync instance.
129    sliding_sync: Arc<SlidingSync>,
130
131    /// The current state of the `RoomListService`.
132    ///
133    /// `RoomListService` is a simple state-machine.
134    state_machine: StateMachine,
135}
136
137impl RoomListService {
138    /// Create a new `RoomList`.
139    ///
140    /// A [`matrix_sdk::SlidingSync`] client will be created, with a cached list
141    /// already pre-configured.
142    ///
143    /// This won't start an encryption sync, and it's the user's responsibility
144    /// to create one in this case using
145    /// [`EncryptionSyncService`][crate::encryption_sync_service::EncryptionSyncService].
146    pub async fn new(client: Client) -> Result<Self, Error> {
147        Self::new_with(client, true, DEFAULT_CONNECTION_ID, DEFAULT_LIST_TIMELINE_LIMIT).await
148    }
149
150    /// Like [`RoomListService::new`] but with additional configuration options.
151    ///
152    /// - `share_pos`: toggles [`SlidingSyncBuilder::share_pos`] for
153    ///   cross-process position sharing.
154    /// - `connection_id`: the Sliding Sync connection ID
155    /// - `timeline_limit`: the timeline limit
156    ///
157    /// [`SlidingSyncBuilder::share_pos`]: matrix_sdk::sliding_sync::SlidingSyncBuilder::share_pos
158    pub async fn new_with(
159        client: Client,
160        share_pos: bool,
161        connection_id: &str,
162        timeline_limit: u32,
163    ) -> Result<Self, Error> {
164        let mut builder = client
165            .sliding_sync(connection_id)
166            .map_err(Error::SlidingSync)?
167            .with_account_data_extension(
168                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
169            )
170            .with_receipt_extension(assign!(http::request::Receipts::default(), {
171                enabled: Some(true),
172                rooms: Some(vec![http::request::ExtensionRoomConfig::AllSubscribed])
173            }))
174            .with_typing_extension(assign!(http::request::Typing::default(), {
175                enabled: Some(true),
176            }))
177            .with_profiles_extension(assign!(http::request::Profiles::default(), {
178                enabled: Some(true),
179            }));
180
181        #[cfg(feature = "unstable-msc4354")]
182        {
183            builder = builder.with_sticky_events_extension(assign!(
184                http::request::StickyEvents::default(),
185                { enabled: Some(true) }
186            ));
187        }
188
189        match client.enabled_thread_subscriptions().await {
190            Ok(true) => {
191                debug!("Client requested thread subscriptions extension");
192
193                builder = builder.with_thread_subscriptions_extension(
194                    assign!(http::request::ThreadSubscriptions::default(), {
195                        enabled: Some(true),
196                        limit: Some(ruma::uint!(10))
197                    }),
198                );
199            }
200
201            Ok(false) => {
202                debug!(
203                    "Thread subscriptions extension either not requested on the client, or the server doesn't advertise support for it: not enabling."
204                );
205            }
206
207            Err(error) => {
208                warn!(
209                    ?error,
210                    "Failed to check whether the client requested thread subscriptions extension: not enabling."
211                );
212            }
213        }
214
215        if share_pos {
216            // The e2ee extensions aren't enabled in this sliding sync instance,
217            // and this is the only one that could be used from a different
218            // process. So it's fine to enable position sharing (i.e. reloading
219            // it from disk), since it's always exclusively owned by the current
220            // process.
221            debug!("Enabling `share_pos` for the room list sliding sync");
222            builder = builder.share_pos();
223        }
224
225        let state_machine = StateMachine::new();
226        let observable_state = state_machine.cloned_state();
227
228        let sliding_sync = builder
229            .add_cached_list(
230                SlidingSyncList::builder(ALL_ROOMS_LIST_NAME)
231                    .sync_mode(
232                        SlidingSyncMode::new_selective()
233                            .add_range(ALL_ROOMS_DEFAULT_SELECTIVE_RANGE),
234                    )
235                    .timeline_limit(timeline_limit)
236                    .required_state(
237                        DEFAULT_REQUIRED_STATE
238                            .iter()
239                            .map(|(state_event, value)| (state_event.clone(), (*value).to_owned()))
240                            .collect(),
241                    )
242                    .filters(Some(assign!(http::request::ListFilters::default(), {
243                        // As defined in the [SlidingSync MSC] If unset, both
244                        // invited and joined rooms are returned. If false, no
245                        // invited rooms are returned. If true, only invited
246                        // rooms are returned.
247                        //
248                        // [SlidingSync MSC]: https://github.com/matrix-org/matrix-spec-proposals/blob/9450ced7fb9cf5ea9077d029b3adf36aebfa8709/proposals/3575-sync.md?plain=1#L444
249                        is_invite: None,
250                    })))
251                    .requires_timeout(move |request_generator| {
252                        // We want Sliding Sync to apply the poll + network
253                        // timeout —i.e. to do the long-polling— in some
254                        // particular cases. Let's define them.
255                        match observable_state.get() {
256                            // These are the states where we want an immediate
257                            // response from the server, with no long-polling.
258                            State::Init
259                            | State::SettingUp
260                            | State::Recovering
261                            | State::Error { .. }
262                            | State::Terminated { .. } => PollTimeout::Some(0),
263
264                            // Otherwise we want long-polling if the list is fully-loaded.
265                            State::Running => {
266                                if request_generator.is_fully_loaded() {
267                                    // Long-polling.
268                                    PollTimeout::Default
269                                } else {
270                                    // No long-polling yet.
271                                    PollTimeout::Some(0)
272                                }
273                            }
274                        }
275                    }),
276            )
277            .await
278            .map_err(Error::SlidingSync)?
279            .build()
280            .await
281            .map(Arc::new)
282            .map_err(Error::SlidingSync)?;
283
284        // Eagerly subscribe the event cache to sync responses.
285        client.event_cache().subscribe()?;
286
287        Ok(Self { client, sliding_sync, state_machine })
288    }
289
290    /// Start to sync the room list.
291    ///
292    /// It's the main method of this entire API. Calling `sync` allows to
293    /// receive updates on the room list: new rooms, rooms updates etc. Those
294    /// updates can be read with `RoomList::entries` for example. This method
295    /// returns a [`Stream`] where produced items only hold an empty value in
296    /// case of a sync success, otherwise an error.
297    ///
298    /// The `RoomListService`' state machine is run by this method.
299    ///
300    /// Stopping the [`Stream`] (i.e. by calling [`Self::stop_sync`]), and
301    /// calling [`Self::sync`] again will resume from the previous state of the
302    /// state machine.
303    ///
304    /// This should be used only for testing. In practice, most users should be
305    /// using the [`SyncService`](crate::sync_service::SyncService) instead.
306    #[doc(hidden)]
307    pub fn sync(&self) -> impl Stream<Item = Result<(), Error>> + '_ {
308        stream! {
309            let sync = self.sliding_sync.sync();
310            pin_mut!(sync);
311
312            // This is a state machine implementation. Things happen in this
313            // order:
314            //
315            // 1. The next state is calculated,
316            // 2. The actions associated to the next state are run,
317            // 3. A sync is done,
318            // 4. The next state is stored.
319            loop {
320                debug!("Run a sync iteration");
321
322                // Calculate the next state, and run the associated actions.
323                let next_state = self.state_machine.next(&self.sliding_sync).await?;
324
325                // Do the sync.
326                match sync.next().await {
327                    // Got a successful result while syncing.
328                    Some(Ok(_update_summary)) => {
329                        debug!(state = ?next_state, "New state");
330
331                        // Update the state.
332                        self.state_machine.set(next_state);
333
334                        yield Ok(());
335                    }
336
337                    // Got an error while syncing.
338                    Some(Err(error)) => {
339                        debug!(expected_state = ?next_state, "New state is an error");
340
341                        let next_state = State::Error { from: Box::new(next_state) };
342                        self.state_machine.set(next_state);
343
344                        yield Err(Error::SlidingSync(error));
345
346                        break;
347                    }
348
349                    // Sync loop has terminated.
350                    None => {
351                        debug!(expected_state = ?next_state, "New state is a termination");
352
353                        let next_state = State::Terminated { from: Box::new(next_state) };
354                        self.state_machine.set(next_state);
355
356                        break;
357                    }
358                }
359            }
360        }
361    }
362
363    /// Force to stop the sync of the `RoomListService` started by
364    /// [`Self::sync`].
365    ///
366    /// It's of utter importance to call this method rather than stop polling
367    /// the `Stream` returned by [`Self::sync`] because it will force the
368    /// cancellation and exit the sync loop, i.e. it will cancel any in-flight
369    /// HTTP requests, cancel any pending futures etc. and put the service into
370    /// a termination state.
371    ///
372    /// Ideally, one wants to consume the `Stream` returned by [`Self::sync`]
373    /// until it returns `None`, because of [`Self::stop_sync`], so that it
374    /// ensures the states are correctly placed.
375    ///
376    /// Stopping the sync of the room list via this method will put the
377    /// state-machine into the [`State::Terminated`] state.
378    ///
379    /// This should be used only for testing. In practice, most users should be
380    /// using the [`SyncService`](crate::sync_service::SyncService) instead.
381    #[doc(hidden)]
382    pub fn stop_sync(&self) -> Result<(), Error> {
383        self.sliding_sync.stop_sync().map_err(Error::SlidingSync)
384    }
385
386    /// Force the sliding sync session to expire.
387    ///
388    /// This is used by [`SyncService`](crate::sync_service::SyncService).
389    ///
390    /// **Warning**: This method **must not** be called while the sync loop is
391    /// running!
392    pub(crate) async fn expire_sync_session(&self) {
393        self.sliding_sync.expire_session().await;
394
395        // Usually, when the session expires, it leads the state to be `Error`,
396        // thus some actions (like refreshing the lists) are executed. However,
397        // if the sync loop has been stopped manually, the state is
398        // `Terminated`, and when the session is forced to expire, the state
399        // remains `Terminated`, thus the actions aren't executed as expected.
400        // Consequently, let's update the state.
401        if let State::Terminated { from } = self.state_machine.get() {
402            self.state_machine.set(State::Error { from });
403        }
404    }
405
406    /// Get a [`Stream`] of [`SyncIndicator`].
407    ///
408    /// Read the documentation of [`SyncIndicator`] to learn more about it.
409    pub fn sync_indicator(
410        &self,
411        delay_before_showing: Duration,
412        delay_before_hiding: Duration,
413    ) -> impl Stream<Item = SyncIndicator> + use<> {
414        let mut state = self.state();
415
416        stream! {
417            // Ensure the `SyncIndicator` is always hidden to start with.
418            yield SyncIndicator::Hide;
419
420            // Let's not wait for an update to happen. The `SyncIndicator` must
421            // be computed as fast as possible.
422            let mut current_state = state.next_now();
423
424            loop {
425                let (sync_indicator, yield_delay) = match current_state {
426                    State::SettingUp | State::Error { .. } => {
427                        (SyncIndicator::Show, delay_before_showing)
428                    }
429
430                    State::Init | State::Recovering | State::Running | State::Terminated { .. } => {
431                        (SyncIndicator::Hide, delay_before_hiding)
432                    }
433                };
434
435                // `state.next().await` has a maximum of `yield_delay` time to execute…
436                let next_state = match timeout(state.next(), yield_delay).await {
437                    // A new state has been received before `yield_delay` time.
438                    // The new `sync_indicator` value won't be yielded.
439                    Ok(next_state) => next_state,
440
441                    // No new state has been received before `yield_delay` time.
442                    // The `sync_indicator` value can be yielded.
443                    Err(_) => {
444                        yield sync_indicator;
445
446                        // Now that `sync_indicator` has been yielded, let's
447                        // wait on the next state again.
448                        state.next().await
449                    }
450                };
451
452                if let Some(next_state) = next_state {
453                    // Update the `current_state`.
454                    current_state = next_state;
455                } else {
456                    // Something is broken with the state. Let's stop this stream too.
457                    break;
458                }
459            }
460        }
461    }
462
463    /// Get the [`Client`] that has been used to create [`Self`].
464    pub fn client(&self) -> &Client {
465        &self.client
466    }
467
468    /// Get a subscriber to the state.
469    pub fn state(&self) -> Subscriber<State> {
470        self.state_machine.subscribe()
471    }
472
473    async fn list_for(&self, sliding_sync_list_name: &str) -> Result<RoomList, Error> {
474        RoomList::new(&self.client, &self.sliding_sync, sliding_sync_list_name, self.state()).await
475    }
476
477    /// Get a [`RoomList`] for all rooms.
478    pub async fn all_rooms(&self) -> Result<RoomList, Error> {
479        self.list_for(ALL_ROOMS_LIST_NAME).await
480    }
481
482    /// Get a [`Room`] if it exists.
483    pub fn room(&self, room_id: &RoomId) -> Result<Room, Error> {
484        self.client.get_room(room_id).ok_or_else(|| Error::RoomNotFound(room_id.to_owned()))
485    }
486
487    /// Set the room subscriptions to exactly `room_ids`.
488    ///
489    /// It means that all events from these rooms will be received every time,
490    /// no matter how the `RoomList` is configured.
491    ///
492    /// [`LatestEvents::listen_to_room`][listen_to_room] will be called for each
493    /// room in `room_ids`, so that the [`LatestEventValue`] will automatically
494    /// be calculated and updated for these rooms, for free.
495    ///
496    /// [listen_to_room]: matrix_sdk::latest_events::LatestEvents::listen_to_room
497    /// [`LatestEventValue`]: matrix_sdk::latest_events::LatestEventValue
498    pub async fn set_room_subscriptions(&self, room_ids: &[&RoomId]) {
499        // Read the state before the await: the state machine can drift
500        // meanwhile.
501        let cancel_in_flight_request = self.must_cancel_in_flight_request();
502
503        self.listen_to_latest_events(room_ids).await;
504
505        self.sliding_sync.set_room_subscriptions(
506            room_ids,
507            Some(room_subscription_settings()),
508            cancel_in_flight_request,
509        )
510    }
511
512    /// Remove the room subscriptions of `room_ids`.
513    ///
514    /// The latest events of these rooms are still listened to.
515    pub fn remove_room_subscriptions(&self, room_ids: &[&RoomId]) {
516        self.sliding_sync.remove_room_subscriptions(room_ids, self.must_cancel_in_flight_request())
517    }
518
519    /// Remove all the room subscriptions, then subscribe to `room_ids`.
520    ///
521    /// Contrary to [`Self::set_room_subscriptions`], the members of every room
522    /// of `room_ids` are marked as missing, so that they are re-fetched.
523    pub async fn reset_and_add_room_subscriptions(&self, room_ids: &[&RoomId]) {
524        // Read the state before the await: the state machine can drift
525        // meanwhile.
526        let cancel_in_flight_request = self.must_cancel_in_flight_request();
527
528        self.listen_to_latest_events(room_ids).await;
529
530        self.sliding_sync.reset_and_add_room_subscriptions(
531            room_ids,
532            Some(room_subscription_settings()),
533            cancel_in_flight_request,
534        )
535    }
536
537    async fn listen_to_latest_events(&self, room_ids: &[&RoomId]) {
538        if !self.client.event_cache().has_subscribed() {
539            return;
540        }
541
542        let latest_events = self.client.latest_events().await;
543
544        for room_id in room_ids {
545            if let Err(error) = latest_events.listen_to_room(room_id).await {
546                // A failure here must not fail the room subscription.
547                error!(?error, ?room_id, "Failed to listen to the latest event for this room");
548            }
549        }
550    }
551
552    fn must_cancel_in_flight_request(&self) -> bool {
553        match self.state_machine.get() {
554            State::Init | State::Recovering | State::Error { .. } | State::Terminated { .. } => {
555                false
556            }
557            State::SettingUp | State::Running => true,
558        }
559    }
560
561    #[cfg(test)]
562    pub fn sliding_sync(&self) -> &SlidingSync {
563        &self.sliding_sync
564    }
565}
566
567fn room_subscription_settings() -> http::request::RoomSubscription {
568    assign!(http::request::RoomSubscription::default(), {
569        required_state: DEFAULT_REQUIRED_STATE.iter().map(|(state_event, value)| {
570            (state_event.clone(), (*value).to_owned())
571        })
572        .chain(
573            DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE.iter().map(|(state_event, value)| {
574                (state_event.clone(), (*value).to_owned())
575            })
576        )
577        .collect(),
578        timeline_limit: UInt::from(DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT),
579    })
580}
581
582/// [`RoomList`]'s errors.
583#[derive(Debug, Error)]
584pub enum Error {
585    /// Error from [`matrix_sdk::SlidingSync`].
586    #[error(transparent)]
587    SlidingSync(SlidingSyncError),
588
589    /// An operation has been requested on an unknown list.
590    #[error("Unknown list `{0}`")]
591    UnknownList(String),
592
593    /// The requested room doesn't exist.
594    #[error("Room `{0}` not found")]
595    RoomNotFound(OwnedRoomId),
596
597    #[error(transparent)]
598    EventCache(#[from] EventCacheError),
599}
600
601/// An hint whether a _sync spinner/loader/toaster_ should be prompted to the
602/// user, indicating that the [`RoomListService`] is syncing.
603///
604/// This is entirely arbitrary and optinionated. Of course, once
605/// [`RoomListService::sync`] has been called, it's going to be constantly
606/// syncing, until [`RoomListService::stop_sync`] is called, or until an error
607/// happened. But in some cases, it's better for the user experience to prompt
608/// to the user that a sync is happening. It's usually the first sync, or the
609/// recovering sync. However, the sync indicator must be prompted if the
610/// aforementioned sync is “slow”, otherwise the indicator is likely to “blink”
611/// pretty fast, which can be very confusing. It's also common to indicate to
612/// the user that a syncing is happening in case of a network error, that
613/// something is catching up etc.
614#[derive(Debug, Eq, PartialEq)]
615pub enum SyncIndicator {
616    /// Show the sync indicator.
617    Show,
618
619    /// Hide the sync indicator.
620    Hide,
621}
622
623#[cfg(test)]
624mod tests {
625    use std::future::ready;
626
627    use futures_util::{StreamExt, pin_mut};
628    use matrix_sdk::{SlidingSyncMode, test_utils::mocks::MatrixMockServer};
629    use matrix_sdk_test::{TestError, async_test};
630    use ruma::{api::client::sync::sync_events::v5, assign, uint};
631
632    use super::{ALL_ROOMS_LIST_NAME, Error, RoomListService, State};
633
634    #[async_test]
635    async fn test_all_rooms_are_declared() -> Result<(), TestError> {
636        let server = MatrixMockServer::new().await;
637        let client = server.client_builder().build().await;
638        let room_list = RoomListService::new(client).await?;
639
640        let sliding_sync = room_list.sliding_sync();
641
642        // List is present, in Selective mode.
643        assert_eq!(
644            sliding_sync
645                .on_list(ALL_ROOMS_LIST_NAME, |list| ready(matches!(
646                    list.sync_mode(),
647                    SlidingSyncMode::Selective { ranges } if ranges == vec![0..=19]
648                )))
649                .await,
650            Some(true)
651        );
652
653        Ok(())
654    }
655
656    #[async_test]
657    async fn test_expire_sliding_sync_session_manually() -> Result<(), Error> {
658        let server = MatrixMockServer::new().await;
659        let client = server.client_builder().build().await;
660
661        let room_list = RoomListService::new(client).await?;
662
663        let sync = room_list.sync();
664        pin_mut!(sync);
665
666        // Run a first sync.
667        {
668            let _mock_guard = server
669                .mock_sliding_sync()
670                .ok({
671                    let mut response = v5::Response::new("0".to_owned());
672                    response.lists.insert(
673                        ALL_ROOMS_LIST_NAME.to_owned(),
674                        assign!(v5::response::List::default(), { count: uint!(0) }),
675                    );
676                    response
677                })
678                .mount_as_scoped()
679                .await;
680
681            let _ = sync.next().await;
682        }
683
684        assert_eq!(room_list.state().get(), State::SettingUp);
685
686        // Stop the sync.
687        room_list.stop_sync()?;
688
689        // Do another sync.
690        let _ = sync.next().await;
691
692        // State is `Terminated`, as expected!
693        assert_eq!(
694            room_list.state_machine.get(),
695            State::Terminated { from: Box::new(State::Running) }
696        );
697
698        // Now, let's make the sliding sync session to expire.
699        room_list.expire_sync_session().await;
700
701        // State is `Error`, as a regular session expiration would generate!
702        assert_eq!(room_list.state_machine.get(), State::Error { from: Box::new(State::Running) });
703
704        Ok(())
705    }
706}