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::{pin::pin, sync::Arc, time::Duration};
60
61use async_stream::stream;
62use eyeball::Subscriber;
63use futures_util::{Stream, StreamExt};
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 Ok(Self { client, sliding_sync, state_machine })
285 }
286
287 /// Start to sync the room list.
288 ///
289 /// It's the main method of this entire API. Calling `sync` allows to
290 /// receive updates on the room list: new rooms, rooms updates etc. Those
291 /// updates can be read with `RoomList::entries` for example. This method
292 /// returns a [`Stream`] where produced items only hold an empty value in
293 /// case of a sync success, otherwise an error.
294 ///
295 /// The `RoomListService`' state machine is run by this method.
296 ///
297 /// Stopping the [`Stream`] (i.e. by calling [`Self::stop_sync`]), and
298 /// calling [`Self::sync`] again will resume from the previous state of the
299 /// state machine.
300 ///
301 /// This should be used only for testing. In practice, most users should be
302 /// using the [`SyncService`](crate::sync_service::SyncService) instead.
303 #[doc(hidden)]
304 pub fn sync(&self) -> impl Stream<Item = Result<(), Error>> + '_ {
305 stream! {
306 let sync = self.sliding_sync.sync();
307 let mut sync = pin!(sync);
308
309 // This is a state machine implementation. Things happen in this
310 // order:
311 //
312 // 1. The next state is calculated,
313 // 2. The actions associated to the next state are run,
314 // 3. A sync is done,
315 // 4. The next state is stored.
316 loop {
317 debug!("Run a sync iteration");
318
319 // Calculate the next state, and run the associated actions.
320 let next_state = self.state_machine.next(&self.sliding_sync).await?;
321
322 // Do the sync.
323 match sync.next().await {
324 // Got a successful result while syncing.
325 Some(Ok(_update_summary)) => {
326 debug!(state = ?next_state, "New state");
327
328 // Update the state.
329 self.state_machine.set(next_state);
330
331 yield Ok(());
332 }
333
334 // Got an error while syncing.
335 Some(Err(error)) => {
336 debug!(expected_state = ?next_state, "New state is an error");
337
338 let next_state = State::Error { from: Box::new(next_state) };
339 self.state_machine.set(next_state);
340
341 yield Err(Error::SlidingSync(error));
342
343 break;
344 }
345
346 // Sync loop has terminated.
347 None => {
348 debug!(expected_state = ?next_state, "New state is a termination");
349
350 let next_state = State::Terminated { from: Box::new(next_state) };
351 self.state_machine.set(next_state);
352
353 break;
354 }
355 }
356 }
357 }
358 }
359
360 /// Force to stop the sync of the `RoomListService` started by
361 /// [`Self::sync`].
362 ///
363 /// It's of utter importance to call this method rather than stop polling
364 /// the `Stream` returned by [`Self::sync`] because it will force the
365 /// cancellation and exit the sync loop, i.e. it will cancel any in-flight
366 /// HTTP requests, cancel any pending futures etc. and put the service into
367 /// a termination state.
368 ///
369 /// Ideally, one wants to consume the `Stream` returned by [`Self::sync`]
370 /// until it returns `None`, because of [`Self::stop_sync`], so that it
371 /// ensures the states are correctly placed.
372 ///
373 /// Stopping the sync of the room list via this method will put the
374 /// state-machine into the [`State::Terminated`] state.
375 ///
376 /// This should be used only for testing. In practice, most users should be
377 /// using the [`SyncService`](crate::sync_service::SyncService) instead.
378 #[doc(hidden)]
379 pub fn stop_sync(&self) -> Result<(), Error> {
380 self.sliding_sync.stop_sync().map_err(Error::SlidingSync)
381 }
382
383 /// Force the sliding sync session to expire.
384 ///
385 /// This is used by [`SyncService`](crate::sync_service::SyncService).
386 ///
387 /// **Warning**: This method **must not** be called while the sync loop is
388 /// running!
389 pub(crate) async fn expire_sync_session(&self) {
390 self.sliding_sync.expire_session().await;
391
392 // Usually, when the session expires, it leads the state to be `Error`,
393 // thus some actions (like refreshing the lists) are executed. However,
394 // if the sync loop has been stopped manually, the state is
395 // `Terminated`, and when the session is forced to expire, the state
396 // remains `Terminated`, thus the actions aren't executed as expected.
397 // Consequently, let's update the state.
398 if let State::Terminated { from } = self.state_machine.get() {
399 self.state_machine.set(State::Error { from });
400 }
401 }
402
403 /// Get a [`Stream`] of [`SyncIndicator`].
404 ///
405 /// Read the documentation of [`SyncIndicator`] to learn more about it.
406 pub fn sync_indicator(
407 &self,
408 delay_before_showing: Duration,
409 delay_before_hiding: Duration,
410 ) -> impl Stream<Item = SyncIndicator> + use<> {
411 let mut state = self.state();
412
413 stream! {
414 // Ensure the `SyncIndicator` is always hidden to start with.
415 yield SyncIndicator::Hide;
416
417 // Let's not wait for an update to happen. The `SyncIndicator` must
418 // be computed as fast as possible.
419 let mut current_state = state.next_now();
420
421 loop {
422 let (sync_indicator, yield_delay) = match current_state {
423 State::SettingUp | State::Error { .. } => {
424 (SyncIndicator::Show, delay_before_showing)
425 }
426
427 State::Init | State::Recovering | State::Running | State::Terminated { .. } => {
428 (SyncIndicator::Hide, delay_before_hiding)
429 }
430 };
431
432 // `state.next().await` has a maximum of `yield_delay` time to execute…
433 let next_state = match timeout(state.next(), yield_delay).await {
434 // A new state has been received before `yield_delay` time.
435 // The new `sync_indicator` value won't be yielded.
436 Ok(next_state) => next_state,
437
438 // No new state has been received before `yield_delay` time.
439 // The `sync_indicator` value can be yielded.
440 Err(_) => {
441 yield sync_indicator;
442
443 // Now that `sync_indicator` has been yielded, let's
444 // wait on the next state again.
445 state.next().await
446 }
447 };
448
449 if let Some(next_state) = next_state {
450 // Update the `current_state`.
451 current_state = next_state;
452 } else {
453 // Something is broken with the state. Let's stop this stream too.
454 break;
455 }
456 }
457 }
458 }
459
460 /// Get the [`Client`] that has been used to create [`Self`].
461 pub fn client(&self) -> &Client {
462 &self.client
463 }
464
465 /// Get a subscriber to the state.
466 pub fn state(&self) -> Subscriber<State> {
467 self.state_machine.subscribe()
468 }
469
470 async fn list_for(&self, sliding_sync_list_name: &str) -> Result<RoomList, Error> {
471 RoomList::new(&self.client, &self.sliding_sync, sliding_sync_list_name, self.state()).await
472 }
473
474 /// Get a [`RoomList`] for all rooms.
475 pub async fn all_rooms(&self) -> Result<RoomList, Error> {
476 self.list_for(ALL_ROOMS_LIST_NAME).await
477 }
478
479 /// Get a [`Room`] if it exists.
480 pub fn room(&self, room_id: &RoomId) -> Result<Room, Error> {
481 self.client.get_room(room_id).ok_or_else(|| Error::RoomNotFound(room_id.to_owned()))
482 }
483
484 /// Set the room subscriptions to exactly `room_ids`.
485 ///
486 /// It means that all events from these rooms will be received every time,
487 /// no matter how the `RoomList` is configured.
488 ///
489 /// [`LatestEvents::listen_to_room`][listen_to_room] will be called for each
490 /// room in `room_ids`, so that the [`LatestEventValue`] will automatically
491 /// be calculated and updated for these rooms, for free.
492 ///
493 /// [listen_to_room]: matrix_sdk::latest_events::LatestEvents::listen_to_room
494 /// [`LatestEventValue`]: matrix_sdk::latest_events::LatestEventValue
495 pub async fn set_room_subscriptions(&self, room_ids: &[&RoomId]) {
496 // Read the state before the await: the state machine can drift
497 // meanwhile.
498 let cancel_in_flight_request = self.must_cancel_in_flight_request();
499
500 self.listen_to_latest_events(room_ids).await;
501
502 self.sliding_sync.set_room_subscriptions(
503 room_ids,
504 Some(room_subscription_settings()),
505 cancel_in_flight_request,
506 )
507 }
508
509 /// Remove the room subscriptions of `room_ids`.
510 ///
511 /// The latest events of these rooms are still listened to.
512 pub fn remove_room_subscriptions(&self, room_ids: &[&RoomId]) {
513 self.sliding_sync.remove_room_subscriptions(room_ids, self.must_cancel_in_flight_request())
514 }
515
516 /// Remove all the room subscriptions, then subscribe to `room_ids`.
517 ///
518 /// Contrary to [`Self::set_room_subscriptions`], the members of every room
519 /// of `room_ids` are marked as missing, so that they are re-fetched.
520 pub async fn reset_and_add_room_subscriptions(&self, room_ids: &[&RoomId]) {
521 // Read the state before the await: the state machine can drift
522 // meanwhile.
523 let cancel_in_flight_request = self.must_cancel_in_flight_request();
524
525 self.listen_to_latest_events(room_ids).await;
526
527 self.sliding_sync.reset_and_add_room_subscriptions(
528 room_ids,
529 Some(room_subscription_settings()),
530 cancel_in_flight_request,
531 )
532 }
533
534 async fn listen_to_latest_events(&self, room_ids: &[&RoomId]) {
535 if let Some(latest_events) = self.client.latest_events().await {
536 for room_id in room_ids {
537 if let Err(error) = latest_events.listen_to_room(room_id).await {
538 // A failure here must not fail the room subscription.
539 error!(?error, ?room_id, "Failed to listen to the latest event for this room");
540 }
541 }
542 }
543 }
544
545 fn must_cancel_in_flight_request(&self) -> bool {
546 match self.state_machine.get() {
547 State::Init | State::Recovering | State::Error { .. } | State::Terminated { .. } => {
548 false
549 }
550 State::SettingUp | State::Running => true,
551 }
552 }
553
554 #[cfg(test)]
555 pub fn sliding_sync(&self) -> &SlidingSync {
556 &self.sliding_sync
557 }
558}
559
560fn room_subscription_settings() -> http::request::RoomSubscription {
561 assign!(http::request::RoomSubscription::default(), {
562 required_state: DEFAULT_REQUIRED_STATE.iter().map(|(state_event, value)| {
563 (state_event.clone(), (*value).to_owned())
564 })
565 .chain(
566 DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE.iter().map(|(state_event, value)| {
567 (state_event.clone(), (*value).to_owned())
568 })
569 )
570 .collect(),
571 timeline_limit: UInt::from(DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT),
572 })
573}
574
575/// [`RoomList`]'s errors.
576#[derive(Debug, Error)]
577pub enum Error {
578 /// Error from [`matrix_sdk::SlidingSync`].
579 #[error(transparent)]
580 SlidingSync(SlidingSyncError),
581
582 /// An operation has been requested on an unknown list.
583 #[error("Unknown list `{0}`")]
584 UnknownList(String),
585
586 /// The requested room doesn't exist.
587 #[error("Room `{0}` not found")]
588 RoomNotFound(OwnedRoomId),
589
590 #[error(transparent)]
591 EventCache(#[from] EventCacheError),
592}
593
594/// An hint whether a _sync spinner/loader/toaster_ should be prompted to the
595/// user, indicating that the [`RoomListService`] is syncing.
596///
597/// This is entirely arbitrary and optinionated. Of course, once
598/// [`RoomListService::sync`] has been called, it's going to be constantly
599/// syncing, until [`RoomListService::stop_sync`] is called, or until an error
600/// happened. But in some cases, it's better for the user experience to prompt
601/// to the user that a sync is happening. It's usually the first sync, or the
602/// recovering sync. However, the sync indicator must be prompted if the
603/// aforementioned sync is “slow”, otherwise the indicator is likely to “blink”
604/// pretty fast, which can be very confusing. It's also common to indicate to
605/// the user that a syncing is happening in case of a network error, that
606/// something is catching up etc.
607#[derive(Debug, Eq, PartialEq)]
608pub enum SyncIndicator {
609 /// Show the sync indicator.
610 Show,
611
612 /// Hide the sync indicator.
613 Hide,
614}
615
616#[cfg(test)]
617mod tests {
618 use std::{future::ready, pin::pin};
619
620 use futures_util::StreamExt;
621 use matrix_sdk::{SlidingSyncMode, test_utils::mocks::MatrixMockServer};
622 use matrix_sdk_test::{TestError, async_test};
623 use ruma::{api::client::sync::sync_events::v5, assign, uint};
624
625 use super::{ALL_ROOMS_LIST_NAME, Error, RoomListService, State};
626
627 #[async_test]
628 async fn test_all_rooms_are_declared() -> Result<(), TestError> {
629 let server = MatrixMockServer::new().await;
630 let client = server.client_builder().build().await;
631 let room_list = RoomListService::new(client).await?;
632
633 let sliding_sync = room_list.sliding_sync();
634
635 // List is present, in Selective mode.
636 assert_eq!(
637 sliding_sync
638 .on_list(ALL_ROOMS_LIST_NAME, |list| ready(matches!(
639 list.sync_mode(),
640 SlidingSyncMode::Selective { ranges } if ranges == vec![0..=19]
641 )))
642 .await,
643 Some(true)
644 );
645
646 Ok(())
647 }
648
649 #[async_test]
650 async fn test_expire_sliding_sync_session_manually() -> Result<(), Error> {
651 let server = MatrixMockServer::new().await;
652 let client = server.client_builder().build().await;
653
654 let room_list = RoomListService::new(client).await?;
655
656 let sync = room_list.sync();
657 let mut sync = pin!(sync);
658
659 // Run a first sync.
660 {
661 let _mock_guard = server
662 .mock_sliding_sync()
663 .ok({
664 let mut response = v5::Response::new("0".to_owned());
665 response.lists.insert(
666 ALL_ROOMS_LIST_NAME.to_owned(),
667 assign!(v5::response::List::default(), { count: uint!(0) }),
668 );
669 response
670 })
671 .mount_as_scoped()
672 .await;
673
674 let _ = sync.next().await;
675 }
676
677 assert_eq!(room_list.state().get(), State::SettingUp);
678
679 // Stop the sync.
680 room_list.stop_sync()?;
681
682 // Do another sync.
683 let _ = sync.next().await;
684
685 // State is `Terminated`, as expected!
686 assert_eq!(
687 room_list.state_machine.get(),
688 State::Terminated { from: Box::new(State::Running) }
689 );
690
691 // Now, let's make the sliding sync session to expire.
692 room_list.expire_sync_session().await;
693
694 // State is `Error`, as a regular session expiration would generate!
695 assert_eq!(room_list.state_machine.get(), State::Error { from: Box::new(State::Running) });
696
697 Ok(())
698 }
699}