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
112    /// potential sliding 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 needed.
196            self.client.encryption().backups().maybe_trigger_backup();
197
198            to_device_events
199        } else {
200            Vec::new()
201        };
202
203        Ok(())
204    }
205
206    pub async fn handle_room_response(
207        &mut self,
208        response: &http::Response,
209        requested_required_states: &RequestedRequiredStates,
210        state_store_guard: &MutexGuard<'_, ()>,
211    ) -> Result<()> {
212        subscribe_to_room_latest_events(&self.client, response.rooms.keys()).await;
213
214        let previously_joined_rooms = self
215            .client
216            .joined_rooms()
217            .into_iter()
218            .map(|r| r.room_id().to_owned())
219            .collect::<BTreeSet<_>>();
220
221        let mut sync_response = self
222            .client
223            .base_client()
224            .process_sliding_sync(response, requested_required_states, state_store_guard)
225            .await?;
226
227        handle_receipts_extension(&self.client, response, &mut sync_response, state_store_guard)
228            .await?;
229
230        update_in_memory_caches(&self.client, &previously_joined_rooms, &sync_response).await;
231
232        self.response = Some(sync_response);
233
234        Ok(())
235    }
236
237    pub async fn handle_thread_subscriptions(
238        &mut self,
239        previous_pos: Option<&str>,
240        thread_subs: response::ThreadSubscriptions,
241    ) -> Result<()> {
242        let catchup_token =
243            thread_subs.prev_batch.map(|prev_batch| ThreadSubscriptionCatchupToken {
244                from: prev_batch,
245                to: previous_pos.map(|s| s.to_owned()),
246            });
247
248        self.client
249            .thread_subscription_catchup()
250            .sync_subscriptions(thread_subs.subscribed, thread_subs.unsubscribed, catchup_token)
251            .await?;
252
253        Ok(())
254    }
255
256    pub async fn process_and_take_response(mut self) -> Result<SyncResponse> {
257        let mut response = self.response.take().unwrap_or_default();
258
259        response.to_device.extend(self.to_device_events);
260
261        self.client.call_sync_response_handlers(&response).await?;
262
263        Ok(response)
264    }
265}
266
267/// Update the caches for the rooms that received updates.
268///
269/// This will only fill the in-memory caches, not save the info on disk.
270async fn update_in_memory_caches(
271    client: &Client,
272    previously_joined_rooms: &BTreeSet<OwnedRoomId>,
273    response: &SyncResponse,
274) {
275    let _timer = timer!(tracing::Level::TRACE, "update_in_memory_caches");
276
277    // If the push rules have changed, update the cached notification mode for *all*
278    // the joined rooms.
279    if response.account_data.iter().any(|event| {
280        event
281            .get_field::<GlobalAccountDataEventType>("type")
282            .ok()
283            .flatten()
284            .is_some_and(|event_type| event_type == GlobalAccountDataEventType::PushRules)
285    }) {
286        let notification_settings = client.notification_settings().await;
287        let rules = notification_settings.rules().await;
288
289        // Update all joined rooms.
290        for room in client.joined_rooms() {
291            if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
292                room.update_cached_user_defined_notification_mode(mode);
293            } else {
294                room.clear_user_defined_notification_mode();
295            }
296        }
297    } else {
298        // Otherwise, precompute the cached user-defined notification mode only for the
299        // newly joined rooms.
300
301        // We'll compute the rules only once, lazily, if needs be.
302        let mut rules = None;
303
304        for room_id in response
305            .rooms
306            .joined
307            .keys()
308            .filter(|room_id| !previously_joined_rooms.contains(*room_id))
309        {
310            let Some(room) = client.get_room(room_id) else {
311                error!(?room_id, "The room must exist since it has been joined");
312                continue;
313            };
314
315            // Reuse the previous `Rules` instance, or compute it once and for all.
316            let rules = if let Some(rules) = &mut rules {
317                rules
318            } else {
319                rules.insert(client.notification_settings().await.rules().await.clone())
320            };
321
322            // Define an initial value for the cached user-defined notification mode.
323            if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
324                room.update_cached_user_defined_notification_mode(mode);
325            }
326        }
327    }
328}
329
330/// Update the receipts extension and compute the read receipt accordingly.
331async fn handle_receipts_extension(
332    client: &Client,
333    response: &http::Response,
334    sync_response: &mut SyncResponse,
335    state_store_guard: &MutexGuard<'_, ()>,
336) -> Result<()> {
337    let _timer = timer!(tracing::Level::TRACE, "handle_receipts_extension");
338
339    // We need to compute read receipts for each joined room that has received an
340    // update, or from each room that has received a receipt ephemeral event.
341    let room_ids = BTreeSet::from_iter(
342        sync_response
343            .rooms
344            .joined
345            .keys()
346            .cloned()
347            .chain(response.extensions.receipts.rooms.keys().cloned()),
348    );
349
350    // Process each room concurrently.
351    let futures = room_ids.into_iter().map(|room_id| async {
352        let receipt_event = client
353            .base_client()
354            .process_sliding_sync_receipts_extension_for_room(&room_id, response, state_store_guard)
355            .await?;
356
357        Result::<_, crate::Error>::Ok(Some((room_id, receipt_event)))
358    });
359
360    let updates = try_join_all(futures).await?;
361
362    for (room_id, receipt_event_content) in updates.into_iter().flatten() {
363        if let Some(event) = receipt_event_content {
364            sync_response.rooms.joined.entry(room_id).or_default().ephemeral.push(event.cast());
365        }
366    }
367
368    Ok(())
369}
370
371#[cfg(all(test, not(target_family = "wasm")))]
372mod tests {
373    use std::{collections::BTreeMap, ops::Not};
374
375    use assert_matches::assert_matches;
376    use matrix_sdk_base::{
377        RequestedRequiredStates, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
378        notification_settings::RoomNotificationMode,
379    };
380    use matrix_sdk_test::{async_test, event_factory::EventFactory};
381    use ruma::{
382        api::client::discovery::get_supported_versions, assign, event_id, room_id, serde::Raw,
383        user_id,
384    };
385    use serde_json::json;
386    use tokio::task::yield_now;
387
388    use super::{Version, VersionBuilder};
389    use crate::{
390        SlidingSyncList, SlidingSyncMode,
391        error::Result,
392        sliding_sync::{VersionBuilderError, client::SlidingSyncResponseProcessor, http},
393        test_utils::{client::MockClientBuilder, mocks::MatrixMockServer},
394    };
395
396    #[test]
397    fn test_version_builder_none() {
398        assert_matches!(VersionBuilder::None.build(None), Ok(Version::None));
399    }
400
401    #[test]
402    fn test_version_builder_native() {
403        assert_matches!(VersionBuilder::Native.build(None), Ok(Version::Native));
404    }
405
406    #[test]
407    fn test_version_builder_discover_native() {
408        let mut response = get_supported_versions::Response::new(vec![]);
409        response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), true)].into();
410
411        assert_matches!(
412            VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
413            Ok(Version::Native)
414        );
415    }
416
417    #[test]
418    fn test_version_builder_discover_native_no_supported_versions() {
419        assert_matches!(
420            VersionBuilder::DiscoverNative.build(None),
421            Err(VersionBuilderError::MissingVersionsResponse)
422        );
423    }
424
425    #[test]
426    fn test_version_builder_discover_native_unstable_features_is_disabled() {
427        let mut response = get_supported_versions::Response::new(vec![]);
428        response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), false)].into();
429
430        assert_matches!(
431            VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
432            Err(VersionBuilderError::NativeVersionIsUnset)
433        );
434    }
435
436    #[async_test]
437    async fn test_available_sliding_sync_versions_none() {
438        let client = MockClientBuilder::new(None).build().await;
439        let available_versions = client.available_sliding_sync_versions().await;
440
441        // `.well-known` and `/versions` aren't available. It's impossible to find any
442        // versions.
443        assert!(available_versions.is_empty());
444    }
445
446    #[async_test]
447    async fn test_available_sliding_sync_versions_native() {
448        let server = MatrixMockServer::new().await;
449        let client = server.client_builder().no_server_versions().build().await;
450
451        server.mock_versions().with_simplified_sliding_sync().ok().mock_once().mount().await;
452
453        let available_versions = client.available_sliding_sync_versions().await;
454
455        // `/versions` is available.
456        assert_eq!(available_versions.len(), 1);
457        assert_matches!(available_versions[0], Version::Native);
458    }
459
460    #[async_test]
461    async fn test_cache_user_defined_notification_mode() -> Result<()> {
462        let client = MockClientBuilder::new(None).build().await;
463        let room_id = room_id!("!r0:matrix.org");
464
465        let sliding_sync = client
466            .sliding_sync("test")?
467            .with_account_data_extension(
468                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
469            )
470            .add_list(
471                SlidingSyncList::builder("all")
472                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
473            )
474            .build()
475            .await?;
476
477        // Mock a sync response.
478        // A `m.push_rules` with `room` is cached during the sync.
479        {
480            let server_response = assign!(http::Response::new("0".to_owned()), {
481                rooms: BTreeMap::from([(
482                    room_id.to_owned(),
483                    http::response::Room::default(),
484                )]),
485                extensions: assign!(http::response::Extensions::default(), {
486                    account_data: assign!(http::response::AccountData::default(), {
487                        global: vec![
488                            Raw::from_json_string(
489                                json!({
490                                    "type": "m.push_rules",
491                                    "content": {
492                                        "global": {
493                                            "room": [
494                                                {
495                                                    "actions": ["notify"],
496                                                    "rule_id": room_id,
497                                                    "default": false,
498                                                    "enabled": true,
499                                                },
500                                            ],
501                                        },
502                                    },
503                                })
504                                .to_string(),
505                            ).unwrap()
506                        ]
507                    })
508                })
509            });
510
511            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
512            sliding_sync
513                .handle_response(
514                    server_response.clone(),
515                    &mut pos_guard,
516                    RequestedRequiredStates::default(),
517                )
518                .await?;
519        }
520
521        // The room must exist, since it's been synced.
522        let room = client.get_room(room_id).unwrap();
523
524        // The room has a cached user-defined notification mode.
525        assert_eq!(
526            room.cached_user_defined_notification_mode(),
527            Some(RoomNotificationMode::AllMessages),
528        );
529
530        // Mock a sync response.
531        // A `m.push_rules` with `room` is cached during the sync.
532        // It overwrites the previous cache.
533        {
534            let server_response = assign!(http::Response::new("0".to_owned()), {
535                rooms: BTreeMap::from([(
536                    room_id.to_owned(),
537                    http::response::Room::default(),
538                )]),
539                extensions: assign!(http::response::Extensions::default(), {
540                    account_data: assign!(http::response::AccountData::default(), {
541                        global: vec![
542                            Raw::from_json_string(
543                                json!({
544                                    "type": "m.push_rules",
545                                    "content": {
546                                        "global": {
547                                            "room": [
548                                                {
549                                                    "actions": [],
550                                                    "rule_id": room_id,
551                                                    "default": false,
552                                                    "enabled": true,
553                                                },
554                                            ],
555                                        },
556                                    },
557                                })
558                                .to_string(),
559                            ).unwrap()
560                        ]
561                    })
562                })
563            });
564
565            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
566            sliding_sync
567                .handle_response(
568                    server_response.clone(),
569                    &mut pos_guard,
570                    RequestedRequiredStates::default(),
571                )
572                .await?;
573        }
574
575        // The room has an updated cached user-defined notification mode.
576        assert_eq!(
577            room.cached_user_defined_notification_mode(),
578            Some(RoomNotificationMode::MentionsAndKeywordsOnly),
579        );
580
581        // Mock a sync response.
582        // Even if the room doesn't appear in the response, its notification mode will
583        // be updated immediately if a new `m.push_rules` is received.
584        {
585            let server_response = assign!(http::Response::new("0".to_owned()), {
586                extensions: assign!(http::response::Extensions::default(), {
587                    account_data: assign!(http::response::AccountData::default(), {
588                        global: vec![
589                            Raw::from_json_string(
590                                json!({
591                                    "type": "m.push_rules",
592                                    "content": {
593                                        "global": {
594                                            "room": [
595                                                {
596                                                    "actions": ["notify"],
597                                                    "rule_id": room_id,
598                                                    "default": false,
599                                                    "enabled": true,
600                                                },
601                                            ],
602                                        },
603                                    },
604                                })
605                                .to_string(),
606                            ).unwrap()
607                        ]
608                    })
609                })
610            });
611
612            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
613            sliding_sync
614                .handle_response(
615                    server_response.clone(),
616                    &mut pos_guard,
617                    RequestedRequiredStates::default(),
618                )
619                .await?;
620        }
621
622        // The room notification mode has been updated again!
623        assert_eq!(
624            room.cached_user_defined_notification_mode(),
625            Some(RoomNotificationMode::AllMessages),
626        );
627
628        Ok(())
629    }
630
631    #[async_test]
632    async fn test_auto_listen_to_latest_events() -> Result<()> {
633        let client = MockClientBuilder::new(None).build().await;
634        let room_id = room_id!("!r0");
635
636        // Create the room beforehand.
637        client.base_client().get_or_create_room(room_id, RoomState::Joined);
638
639        // Enable the event cache (required for the latest events).
640        client.event_cache().subscribe()?;
641
642        // The latest event “listener” for this room has NOT been enabled.
643        assert!(client.latest_events().await.is_listening_to_room(room_id).await.not());
644
645        // Create the sliding sync client.
646        let sliding_sync = client
647            .sliding_sync("test")?
648            .add_list(
649                SlidingSyncList::builder("all")
650                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
651            )
652            .build()
653            .await?;
654
655        // Receive a (mocked) response.
656        {
657            let server_response = assign!(http::Response::new("0".to_owned()), {
658                rooms: BTreeMap::from([(
659                    room_id.to_owned(),
660                    http::response::Room::default(),
661                )]),
662            });
663
664            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
665
666            sliding_sync
667                .handle_response(
668                    server_response.clone(),
669                    &mut pos_guard,
670                    RequestedRequiredStates::default(),
671                )
672                .await?;
673        }
674
675        // The room still exists.
676        assert!(client.get_room(room_id).is_some());
677
678        // The latest event “listener” for this room has been enabled.
679        assert!(client.latest_events().await.is_listening_to_room(room_id).await);
680
681        Ok(())
682    }
683
684    #[async_test]
685    async fn test_read_receipt_can_trigger_a_notable_update_reason() {
686        use ruma::api::client::sync::sync_events::v5 as http;
687
688        // Given a logged-in client.
689        let client = MockClientBuilder::new(None).build().await;
690        client.event_cache().subscribe().unwrap();
691
692        let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
693
694        // When I send sliding sync response containing a new room.
695        let room_id = room_id!("!r:e.uk");
696        let room = http::response::Room::new();
697        let mut response = http::Response::new("5".to_owned());
698        response.rooms.insert(room_id.to_owned(), room);
699
700        let mut processor = SlidingSyncResponseProcessor::new(client.clone());
701        {
702            let state_store_guard = client.base_client().state_store_lock().lock().await;
703            processor
704                .handle_room_response(
705                    &response,
706                    &RequestedRequiredStates::default(),
707                    &state_store_guard,
708                )
709                .await
710                .expect("Failed to process sync");
711        }
712        processor.process_and_take_response().await.expect("Failed to finish processing sync");
713
714        // Then room info notable updates are received.
715        assert_matches!(
716            room_info_notable_update_stream.recv().await,
717            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
718                assert_eq!(received_room_id, room_id);
719                assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
720            }
721        );
722        assert_matches!(
723            room_info_notable_update_stream.recv().await,
724            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
725                assert_eq!(received_room_id, room_id);
726                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
727            }
728        );
729        assert!(room_info_notable_update_stream.is_empty());
730
731        // When I send sliding sync response containing a couple of events with no read
732        // receipt.
733        let room_id = room_id!("!r:e.uk");
734        let f = EventFactory::new().room(room_id).sender(user_id!("@u:h.uk"));
735        let events = vec![
736            f.text_msg("hi").event_id(event_id!("$3")).into_raw_sync(),
737            f.text_msg("hi").event_id(event_id!("$4")).into_raw_sync(),
738        ];
739        let room = assign!(http::response::Room::new(), {
740            timeline: events,
741        });
742        let mut response = http::Response::new("5".to_owned());
743        response.rooms.insert(room_id.to_owned(), room);
744
745        let mut processor = SlidingSyncResponseProcessor::new(client.clone());
746        {
747            let state_store_guard = client.base_client().state_store_lock().lock().await;
748            processor
749                .handle_room_response(
750                    &response,
751                    &RequestedRequiredStates::default(),
752                    &state_store_guard,
753                )
754                .await
755                .expect("Failed to process sync");
756        }
757        processor.process_and_take_response().await.expect("Failed to finish processing sync");
758
759        // Then room info notable updates are received.
760        //
761        // `NONE` because the regular sync process ends up to updating nothing.
762        assert_matches!(
763            room_info_notable_update_stream.recv().await,
764            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
765                assert_eq!(received_room_id, room_id);
766                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
767            }
768        );
769        // `READ_RECEIPT` because this is what we expect.
770        assert_matches!(
771            room_info_notable_update_stream.recv().await,
772            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
773                assert_eq!(received_room_id, room_id);
774                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
775            }
776        );
777
778        // At some point, we receive an update for the `LATEST_EVENT` too, since this is
779        // enabled by default.
780        assert_matches!(
781            room_info_notable_update_stream.recv().await,
782            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
783                assert_eq!(received_room_id, room_id);
784                assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT), "{received_reasons:?}");
785            }
786        );
787
788        yield_now().await;
789
790        // Then the stream gets quiet.
791        assert!(room_info_notable_update_stream.is_empty());
792    }
793}