Skip to main content

matrix_sdk/sliding_sync/
client.rs

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