Skip to main content

matrix_sdk/sliding_sync/
client.rs

1use std::collections::BTreeSet;
2
3use futures_util::future::try_join_all;
4use matrix_sdk_base::{
5    RequestedRequiredStates, ThreadSubscriptionCatchupToken, sync::SyncResponse, timer,
6};
7use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
8use ruma::{
9    OwnedRoomId,
10    api::{
11        FeatureFlag, SupportedVersions,
12        client::sync::sync_events::v5::{self as http, response},
13    },
14    events::GlobalAccountDataEventType,
15};
16use tokio::sync::MutexGuard;
17use tracing::error;
18
19use super::{SlidingSync, SlidingSyncBuilder};
20use crate::{Client, Result, sync::subscribe_to_room_latest_events};
21
22/// A sliding sync version.
23#[derive(Clone, Debug)]
24pub enum Version {
25    /// No version. Useful to represent that sliding sync is disabled for
26    /// example, and that the version is unknown.
27    None,
28
29    /// Use the version of the sliding sync implementation inside Synapse, i.e.
30    /// MSC4186.
31    Native,
32}
33
34impl Version {
35    #[cfg(test)]
36    pub(crate) fn is_native(&self) -> bool {
37        matches!(self, Self::Native)
38    }
39}
40
41/// An error when building a version.
42#[derive(thiserror::Error, Debug)]
43pub enum VersionBuilderError {
44    /// The `/versions` response is not set.
45    #[error("The `/versions` response is not set")]
46    MissingVersionsResponse,
47
48    /// `/versions` does not contain `org.matrix.simplified_msc3575` in its
49    /// `unstable_features`, or it's not set to true.
50    #[error(
51        "`/versions` does not contain `org.matrix.simplified_msc3575` in its `unstable_features`, \
52         or it's not set to true."
53    )]
54    NativeVersionIsUnset,
55}
56
57/// A builder for [`Version`].
58#[derive(Clone, Debug)]
59pub enum VersionBuilder {
60    /// Build a [`Version::None`].
61    None,
62
63    /// Build a [`Version::Native`].
64    Native,
65
66    /// Build a [`Version::Native`] by auto-discovering it.
67    ///
68    /// It is available if the server enables it via `/versions`.
69    DiscoverNative,
70}
71
72impl VersionBuilder {
73    pub(crate) fn needs_get_supported_versions(&self) -> bool {
74        matches!(self, Self::DiscoverNative)
75    }
76
77    /// Build a [`Version`].
78    ///
79    /// It can fail if auto-discovering fails, e.g. if `/versions` do contain
80    /// invalid data.
81    pub fn build(
82        self,
83        supported: Option<&SupportedVersions>,
84    ) -> Result<Version, VersionBuilderError> {
85        Ok(match self {
86            Self::None => Version::None,
87
88            Self::Native => Version::Native,
89
90            Self::DiscoverNative => {
91                let Some(supported) = supported else {
92                    return Err(VersionBuilderError::MissingVersionsResponse);
93                };
94
95                if supported.features.contains(&FeatureFlag::Msc4186) {
96                    Version::Native
97                } else {
98                    return Err(VersionBuilderError::NativeVersionIsUnset);
99                }
100            }
101        })
102    }
103}
104
105impl Client {
106    /// Find all sliding sync versions that are available.
107    ///
108    /// Be careful: This method may hit the store and will send new requests for
109    /// each call. It can be costly to call it repeatedly.
110    ///
111    /// If `/versions` is unreachable, it will simply move potential sliding
112    /// sync versions aside. No error will be reported.
113    pub async fn available_sliding_sync_versions(&self) -> Vec<Version> {
114        let supported_versions = self.supported_versions().await.ok();
115
116        [VersionBuilder::DiscoverNative]
117            .into_iter()
118            .filter_map(|version_builder| version_builder.build(supported_versions.as_ref()).ok())
119            .collect()
120    }
121
122    /// Create a [`SlidingSyncBuilder`] tied to this client, with the given
123    /// identifier.
124    ///
125    /// Note: the identifier must not be more than 16 chars long!
126    pub fn sliding_sync(&self, id: impl Into<String>) -> Result<SlidingSyncBuilder> {
127        Ok(SlidingSync::builder(id.into(), self.clone())?)
128    }
129
130    /// Handle all the information provided in a sliding sync response, except
131    /// for the e2ee bits.
132    ///
133    /// If you need to handle encryption too, use the internal
134    /// `SlidingSyncResponseProcessor` instead.
135    #[cfg(any(test, feature = "testing"))]
136    #[tracing::instrument(skip(self, response))]
137    pub async fn process_sliding_sync_test_helper(
138        &self,
139        response: &http::Response,
140        requested_required_states: &RequestedRequiredStates,
141    ) -> Result<SyncResponse> {
142        let response = self
143            .base_client()
144            .process_sliding_sync(
145                response,
146                requested_required_states,
147                &self.base_client().state_store_lock().lock().await,
148            )
149            .await?;
150
151        tracing::debug!("done processing on base_client");
152        self.call_sync_response_handlers(&response).await?;
153
154        Ok(response)
155    }
156}
157
158/// Small helper to handle a `SlidingSync` response's sub parts.
159///
160/// This will properly handle the encryption and the room response
161/// independently, if needs be, making sure that both are properly processed by
162/// event handlers.
163#[must_use]
164pub(crate) struct SlidingSyncResponseProcessor {
165    client: Client,
166    to_device_events: Vec<ProcessedToDeviceEvent>,
167    response: Option<SyncResponse>,
168}
169
170impl SlidingSyncResponseProcessor {
171    pub fn new(client: Client) -> Self {
172        Self { client, to_device_events: Vec::new(), response: None }
173    }
174
175    #[cfg(feature = "e2e-encryption")]
176    pub async fn handle_encryption(
177        &mut self,
178        extensions: &response::Extensions,
179        state_store_guard: &MutexGuard<'_, ()>,
180    ) -> Result<()> {
181        // This is an internal API misuse if this is triggered (calling
182        // `handle_room_response` before this function), so panic is fine.
183        assert!(self.response.is_none());
184
185        self.to_device_events = if let Some(to_device_events) = self
186            .client
187            .base_client()
188            .process_sliding_sync_e2ee(
189                extensions.to_device.as_ref(),
190                &extensions.e2ee,
191                state_store_guard,
192            )
193            .await?
194        {
195            // Some new keys might have been received, so trigger a backup if
196            // needed.
197            self.client.encryption().backups().maybe_trigger_backup();
198
199            to_device_events
200        } else {
201            Vec::new()
202        };
203
204        Ok(())
205    }
206
207    pub async fn handle_room_response(
208        &mut self,
209        response: &http::Response,
210        requested_required_states: &RequestedRequiredStates,
211        state_store_guard: &MutexGuard<'_, ()>,
212    ) -> Result<()> {
213        subscribe_to_room_latest_events(&self.client, response.rooms.keys()).await;
214
215        let previously_joined_rooms = self
216            .client
217            .joined_rooms()
218            .into_iter()
219            .map(|r| r.room_id().to_owned())
220            .collect::<BTreeSet<_>>();
221
222        let mut sync_response = self
223            .client
224            .base_client()
225            .process_sliding_sync(response, requested_required_states, state_store_guard)
226            .await?;
227
228        handle_receipts_extension(&self.client, response, &mut sync_response, state_store_guard)
229            .await?;
230
231        update_in_memory_caches(&self.client, &previously_joined_rooms, &sync_response).await;
232
233        self.response = Some(sync_response);
234
235        Ok(())
236    }
237
238    pub async fn handle_thread_subscriptions(
239        &mut self,
240        previous_pos: Option<&str>,
241        thread_subs: response::ThreadSubscriptions,
242    ) -> Result<()> {
243        let catchup_token =
244            thread_subs.prev_batch.map(|prev_batch| ThreadSubscriptionCatchupToken {
245                from: prev_batch,
246                to: previous_pos.map(|s| s.to_owned()),
247            });
248
249        self.client
250            .thread_subscription_catchup()
251            .sync_subscriptions(thread_subs.subscribed, thread_subs.unsubscribed, catchup_token)
252            .await?;
253
254        Ok(())
255    }
256
257    pub async fn process_and_take_response(mut self) -> Result<SyncResponse> {
258        let mut response = self.response.take().unwrap_or_default();
259
260        response.to_device.extend(self.to_device_events);
261
262        self.client.call_sync_response_handlers(&response).await?;
263
264        Ok(response)
265    }
266}
267
268/// Update the caches for the rooms that received updates.
269///
270/// This will only fill the in-memory caches, not save the info on disk.
271async fn update_in_memory_caches(
272    client: &Client,
273    previously_joined_rooms: &BTreeSet<OwnedRoomId>,
274    response: &SyncResponse,
275) {
276    let _timer = timer!(tracing::Level::TRACE, "update_in_memory_caches");
277
278    // If the push rules have changed, update the cached notification mode for
279    // _all_ the joined rooms.
280    if response.account_data.iter().any(|event| {
281        event
282            .get_field::<GlobalAccountDataEventType>("type")
283            .ok()
284            .flatten()
285            .is_some_and(|event_type| event_type == GlobalAccountDataEventType::PushRules)
286    }) {
287        let notification_settings = client.notification_settings().await;
288        let rules = notification_settings.rules().await;
289
290        // Update all joined rooms.
291        for room in client.joined_rooms() {
292            if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
293                room.update_cached_user_defined_notification_mode(mode);
294            } else {
295                room.clear_user_defined_notification_mode();
296            }
297        }
298    } else {
299        // Otherwise, precompute the cached user-defined notification mode only
300        // for the newly joined rooms.
301
302        // We'll compute the rules only once, lazily, if needs be.
303        let mut rules = None;
304
305        for room_id in response
306            .rooms
307            .joined
308            .keys()
309            .filter(|room_id| !previously_joined_rooms.contains(*room_id))
310        {
311            let Some(room) = client.get_room(room_id) else {
312                error!(?room_id, "The room must exist since it has been joined");
313                continue;
314            };
315
316            // Reuse the previous `Rules` instance, or compute it once and for
317            // all.
318            let rules = if let Some(rules) = &mut rules {
319                rules
320            } else {
321                rules.insert(client.notification_settings().await.rules().await.clone())
322            };
323
324            // Define an initial value for the cached user-defined notification
325            // mode.
326            if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
327                room.update_cached_user_defined_notification_mode(mode);
328            }
329        }
330    }
331}
332
333/// Update the receipts extension and compute the read receipt accordingly.
334async fn handle_receipts_extension(
335    client: &Client,
336    response: &http::Response,
337    sync_response: &mut SyncResponse,
338    state_store_guard: &MutexGuard<'_, ()>,
339) -> Result<()> {
340    let _timer = timer!(tracing::Level::TRACE, "handle_receipts_extension");
341
342    // We need to compute read receipts for each joined room that has received
343    // an update, or from each room that has received a receipt ephemeral event.
344    let room_ids = BTreeSet::from_iter(
345        sync_response
346            .rooms
347            .joined
348            .keys()
349            .cloned()
350            .chain(response.extensions.receipts.rooms.keys().cloned()),
351    );
352
353    // Process each room concurrently.
354    let futures = room_ids.into_iter().map(|room_id| async {
355        let receipt_event = client
356            .base_client()
357            .process_sliding_sync_receipts_extension_for_room(&room_id, response, state_store_guard)
358            .await?;
359
360        Result::<_, crate::Error>::Ok(Some((room_id, receipt_event)))
361    });
362
363    let updates = try_join_all(futures).await?;
364
365    for (room_id, receipt_event_content) in updates.into_iter().flatten() {
366        if let Some(event) = receipt_event_content {
367            sync_response.rooms.joined.entry(room_id).or_default().ephemeral.push(event.cast());
368        }
369    }
370
371    Ok(())
372}
373
374#[cfg(all(test, not(target_family = "wasm")))]
375mod tests {
376    use std::{collections::BTreeMap, ops::Not};
377
378    use assert_matches::assert_matches;
379    use matrix_sdk_base::{
380        RequestedRequiredStates, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
381        notification_settings::RoomNotificationMode,
382    };
383    use matrix_sdk_test::{async_test, event_factory::EventFactory};
384    use ruma::{
385        api::client::discovery::get_supported_versions, assign, event_id, room_id, serde::Raw,
386        user_id,
387    };
388    use serde_json::json;
389    use tokio::task::yield_now;
390
391    use super::{Version, VersionBuilder};
392    use crate::{
393        SlidingSyncList, SlidingSyncMode,
394        error::Result,
395        sliding_sync::{VersionBuilderError, client::SlidingSyncResponseProcessor, http},
396        test_utils::{client::MockClientBuilder, mocks::MatrixMockServer},
397    };
398
399    #[test]
400    fn test_version_builder_none() {
401        assert_matches!(VersionBuilder::None.build(None), Ok(Version::None));
402    }
403
404    #[test]
405    fn test_version_builder_native() {
406        assert_matches!(VersionBuilder::Native.build(None), Ok(Version::Native));
407    }
408
409    #[test]
410    fn test_version_builder_discover_native() {
411        let mut response = get_supported_versions::Response::new(vec![]);
412        response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), true)].into();
413
414        assert_matches!(
415            VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
416            Ok(Version::Native)
417        );
418    }
419
420    #[test]
421    fn test_version_builder_discover_native_no_supported_versions() {
422        assert_matches!(
423            VersionBuilder::DiscoverNative.build(None),
424            Err(VersionBuilderError::MissingVersionsResponse)
425        );
426    }
427
428    #[test]
429    fn test_version_builder_discover_native_unstable_features_is_disabled() {
430        let mut response = get_supported_versions::Response::new(vec![]);
431        response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), false)].into();
432
433        assert_matches!(
434            VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
435            Err(VersionBuilderError::NativeVersionIsUnset)
436        );
437    }
438
439    #[async_test]
440    async fn test_available_sliding_sync_versions_none() {
441        let client = MockClientBuilder::new(None).build().await;
442        let available_versions = client.available_sliding_sync_versions().await;
443
444        // `.well-known` and `/versions` aren't available. It's impossible to
445        // find any versions.
446        assert!(available_versions.is_empty());
447    }
448
449    #[async_test]
450    async fn test_available_sliding_sync_versions_native() {
451        let server = MatrixMockServer::new().await;
452        let client = server.client_builder().no_server_versions().build().await;
453
454        server.mock_versions().with_simplified_sliding_sync().ok().mock_once().mount().await;
455
456        let available_versions = client.available_sliding_sync_versions().await;
457
458        // `/versions` is available.
459        assert_eq!(available_versions.len(), 1);
460        assert_matches!(available_versions[0], Version::Native);
461    }
462
463    #[async_test]
464    async fn test_cache_user_defined_notification_mode() -> Result<()> {
465        let client = MockClientBuilder::new(None).build().await;
466        let room_id = room_id!("!r0:matrix.org");
467
468        let sliding_sync = client
469            .sliding_sync("test")?
470            .with_account_data_extension(
471                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
472            )
473            .add_list(
474                SlidingSyncList::builder("all")
475                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
476            )
477            .build()
478            .await?;
479
480        // Mock a sync response. A `m.push_rules` with `room` is cached during
481        // the sync.
482        {
483            let server_response = assign!(http::Response::new("0".to_owned()), {
484                rooms: BTreeMap::from([(
485                    room_id.to_owned(),
486                    http::response::Room::default(),
487                )]),
488                extensions: assign!(http::response::Extensions::default(), {
489                    account_data: assign!(http::response::AccountData::default(), {
490                        global: vec![
491                            Raw::from_json_string(
492                                json!({
493                                    "type": "m.push_rules",
494                                    "content": {
495                                        "global": {
496                                            "room": [
497                                                {
498                                                    "actions": ["notify"],
499                                                    "rule_id": room_id,
500                                                    "default": false,
501                                                    "enabled": true,
502                                                },
503                                            ],
504                                        },
505                                    },
506                                })
507                                .to_string(),
508                            ).unwrap()
509                        ]
510                    })
511                })
512            });
513
514            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
515            sliding_sync
516                .handle_response(
517                    server_response.clone(),
518                    &mut pos_guard,
519                    RequestedRequiredStates::default(),
520                )
521                .await?;
522        }
523
524        // The room must exist, since it's been synced.
525        let room = client.get_room(room_id).unwrap();
526
527        // The room has a cached user-defined notification mode.
528        assert_eq!(
529            room.cached_user_defined_notification_mode(),
530            Some(RoomNotificationMode::AllMessages),
531        );
532
533        // Mock a sync response. A `m.push_rules` with `room` is cached during
534        // the sync. It overwrites the previous cache.
535        {
536            let server_response = assign!(http::Response::new("0".to_owned()), {
537                rooms: BTreeMap::from([(
538                    room_id.to_owned(),
539                    http::response::Room::default(),
540                )]),
541                extensions: assign!(http::response::Extensions::default(), {
542                    account_data: assign!(http::response::AccountData::default(), {
543                        global: vec![
544                            Raw::from_json_string(
545                                json!({
546                                    "type": "m.push_rules",
547                                    "content": {
548                                        "global": {
549                                            "room": [
550                                                {
551                                                    "actions": [],
552                                                    "rule_id": room_id,
553                                                    "default": false,
554                                                    "enabled": true,
555                                                },
556                                            ],
557                                        },
558                                    },
559                                })
560                                .to_string(),
561                            ).unwrap()
562                        ]
563                    })
564                })
565            });
566
567            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
568            sliding_sync
569                .handle_response(
570                    server_response.clone(),
571                    &mut pos_guard,
572                    RequestedRequiredStates::default(),
573                )
574                .await?;
575        }
576
577        // The room has an updated cached user-defined notification mode.
578        assert_eq!(
579            room.cached_user_defined_notification_mode(),
580            Some(RoomNotificationMode::MentionsAndKeywordsOnly),
581        );
582
583        // Mock a sync response. Even if the room doesn't appear in the
584        // response, its notification mode will be updated immediately if a new
585        // `m.push_rules` is received.
586        {
587            let server_response = assign!(http::Response::new("0".to_owned()), {
588                extensions: assign!(http::response::Extensions::default(), {
589                    account_data: assign!(http::response::AccountData::default(), {
590                        global: vec![
591                            Raw::from_json_string(
592                                json!({
593                                    "type": "m.push_rules",
594                                    "content": {
595                                        "global": {
596                                            "room": [
597                                                {
598                                                    "actions": ["notify"],
599                                                    "rule_id": room_id,
600                                                    "default": false,
601                                                    "enabled": true,
602                                                },
603                                            ],
604                                        },
605                                    },
606                                })
607                                .to_string(),
608                            ).unwrap()
609                        ]
610                    })
611                })
612            });
613
614            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
615            sliding_sync
616                .handle_response(
617                    server_response.clone(),
618                    &mut pos_guard,
619                    RequestedRequiredStates::default(),
620                )
621                .await?;
622        }
623
624        // The room notification mode has been updated again!
625        assert_eq!(
626            room.cached_user_defined_notification_mode(),
627            Some(RoomNotificationMode::AllMessages),
628        );
629
630        Ok(())
631    }
632
633    #[async_test]
634    async fn test_auto_listen_to_latest_events() -> Result<()> {
635        let client = MockClientBuilder::new(None).build().await;
636        let room_id = room_id!("!r0");
637
638        // Create the room beforehand.
639        client.base_client().get_or_create_room(room_id, RoomState::Joined);
640
641        // Enable the event cache (required for the latest events).
642        client.event_cache().subscribe()?;
643
644        // The latest event “listener” for this room has NOT been enabled.
645        assert!(client.latest_events().await.is_listening_to_room(room_id).await.not());
646
647        // Create the sliding sync client.
648        let sliding_sync = client
649            .sliding_sync("test")?
650            .add_list(
651                SlidingSyncList::builder("all")
652                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
653            )
654            .build()
655            .await?;
656
657        // Receive a (mocked) response.
658        {
659            let server_response = assign!(http::Response::new("0".to_owned()), {
660                rooms: BTreeMap::from([(
661                    room_id.to_owned(),
662                    http::response::Room::default(),
663                )]),
664            });
665
666            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
667
668            sliding_sync
669                .handle_response(
670                    server_response.clone(),
671                    &mut pos_guard,
672                    RequestedRequiredStates::default(),
673                )
674                .await?;
675        }
676
677        // The room still exists.
678        assert!(client.get_room(room_id).is_some());
679
680        // The latest event “listener” for this room has been enabled.
681        assert!(client.latest_events().await.is_listening_to_room(room_id).await);
682
683        Ok(())
684    }
685
686    #[async_test]
687    async fn test_read_receipt_can_trigger_a_notable_update_reason() {
688        use ruma::api::client::sync::sync_events::v5 as http;
689
690        // Given a logged-in client.
691        let client = MockClientBuilder::new(None).build().await;
692        client.event_cache().subscribe().unwrap();
693
694        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
695
696        // When I send sliding sync response containing a new room.
697        let room_id = room_id!("!r:e.uk");
698        let room = http::response::Room::new();
699        let mut response = http::Response::new("5".to_owned());
700        response.rooms.insert(room_id.to_owned(), room);
701
702        let mut processor = SlidingSyncResponseProcessor::new(client.clone());
703        {
704            let state_store_guard = client.base_client().state_store_lock().lock().await;
705            processor
706                .handle_room_response(
707                    &response,
708                    &RequestedRequiredStates::default(),
709                    &state_store_guard,
710                )
711                .await
712                .expect("Failed to process sync");
713        }
714        processor.process_and_take_response().await.expect("Failed to finish processing sync");
715
716        // Then room info notable updates are received.
717        assert_matches!(
718            room_info_notable_update_stream.recv().await,
719            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
720                assert_eq!(received_room_id, room_id);
721                assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
722            }
723        );
724        assert_matches!(
725            room_info_notable_update_stream.recv().await,
726            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
727                assert_eq!(received_room_id, room_id);
728                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
729            }
730        );
731        assert!(room_info_notable_update_stream.is_empty());
732
733        // When I send sliding sync response containing a couple of events with
734        // no read receipt.
735        let room_id = room_id!("!r:e.uk");
736        let f = EventFactory::new().room(room_id).sender(user_id!("@u:h.uk"));
737        let events = vec![
738            f.text_msg("hi").event_id(event_id!("$3")).into_raw_sync(),
739            f.text_msg("hi").event_id(event_id!("$4")).into_raw_sync(),
740        ];
741        let room = assign!(http::response::Room::new(), {
742            timeline: events,
743        });
744        let mut response = http::Response::new("5".to_owned());
745        response.rooms.insert(room_id.to_owned(), room);
746
747        let mut processor = SlidingSyncResponseProcessor::new(client.clone());
748        {
749            let state_store_guard = client.base_client().state_store_lock().lock().await;
750            processor
751                .handle_room_response(
752                    &response,
753                    &RequestedRequiredStates::default(),
754                    &state_store_guard,
755                )
756                .await
757                .expect("Failed to process sync");
758        }
759        processor.process_and_take_response().await.expect("Failed to finish processing sync");
760
761        // Then room info notable updates are received.
762        //
763        // `NONE` because the regular sync process ends up to updating nothing.
764        assert_matches!(
765            room_info_notable_update_stream.recv().await,
766            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
767                assert_eq!(received_room_id, room_id);
768                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
769            }
770        );
771        // `READ_RECEIPT` because this is what we expect.
772        assert_matches!(
773            room_info_notable_update_stream.recv().await,
774            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
775                assert_eq!(received_room_id, room_id);
776                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
777            }
778        );
779
780        // At some point, we receive an update for the `LATEST_EVENT` too, since
781        // this is enabled by default.
782        assert_matches!(
783            room_info_notable_update_stream.recv().await,
784            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
785                assert_eq!(received_room_id, room_id);
786                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT), "{received_reasons:?}");
787            }
788        );
789
790        yield_now().await;
791
792        // Then the stream gets quiet.
793        assert!(room_info_notable_update_stream.is_empty());
794    }
795}