diff --git a/bindings/matrix-sdk-ffi/changelog.d/6672.added.md b/bindings/matrix-sdk-ffi/changelog.d/6672.added.md new file mode 100644 index 00000000000..9cee09cafc0 --- /dev/null +++ b/bindings/matrix-sdk-ffi/changelog.d/6672.added.md @@ -0,0 +1 @@ +Expose client-level presence configuration with optional immediate updates. diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index c4be6afcc26..749dd9030f8 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -141,7 +141,7 @@ use crate::{ room_preview::RoomPreview, ruma::{ AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig, - MediaPreviews, MediaSource, RoomAccountDataEvent, + MediaPreviews, MediaSource, PresenceState, RoomAccountDataEvent, }, runtime::get_runtime_handle, spaces::SpaceService, @@ -1175,6 +1175,20 @@ impl Client { self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect() } + /// Set the presence state for the current user. + /// + /// This updates the presence state used by future generated sync requests, + /// regardless of `immediate`. The initial default is `Unavailable`. If + /// `immediate` is `true`, it also sends an immediate presence update to the + /// homeserver. + pub async fn set_presence( + &self, + presence: PresenceState, + immediate: bool, + ) -> Result<(), ClientError> { + Ok(self.inner.set_presence(presence.into(), None, immediate).await?) + } + /// Sets the [ClientDelegate] which will inform about authentication errors. /// Returns an error if the delegate was already set. pub fn set_delegate( diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index a6d795ef799..591aed60bd1 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -78,6 +78,7 @@ use ruma::{ }, }, matrix_uri::MatrixId as RumaMatrixId, + presence::PresenceState as RumaPresenceState, push::{ ConditionalPushRule as RumaConditionalPushRule, PatternedPushRule as RumaPatternedPushRule, Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule, @@ -190,6 +191,35 @@ impl From<&RumaMatrixId> for MatrixId { } } +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum, Default)] +pub enum PresenceState { + Online, + Offline, + #[default] + Unavailable, +} + +impl From for RumaPresenceState { + fn from(value: PresenceState) -> Self { + match value { + PresenceState::Online => Self::Online, + PresenceState::Offline => Self::Offline, + PresenceState::Unavailable => Self::Unavailable, + } + } +} + +impl From for PresenceState { + fn from(value: RumaPresenceState) -> Self { + match value { + RumaPresenceState::Online => Self::Online, + RumaPresenceState::Offline => Self::Offline, + RumaPresenceState::Unavailable => Self::Unavailable, + _ => Self::default(), + } + } +} + #[matrix_sdk_ffi_macros::export] pub fn message_event_content_new( msgtype: MessageType, diff --git a/crates/matrix-sdk-ui/changelog.d/6672.added.md b/crates/matrix-sdk-ui/changelog.d/6672.added.md new file mode 100644 index 00000000000..36ba620194c --- /dev/null +++ b/crates/matrix-sdk-ui/changelog.d/6672.added.md @@ -0,0 +1,3 @@ +Make `matrix-sdk-ui` sliding sync services use the client-owned sync presence +value for generated requests, so background sync flows can avoid marking the +user online without UI-service-specific presence APIs. diff --git a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs index 7d53b06c17d..c33cf4ffaab 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -22,7 +22,10 @@ use wiremock::{ }; use crate::{ - sliding_sync::{PartialSlidingSyncRequest, SlidingSyncMatcher, check_requests}, + sliding_sync::{ + PartialSlidingSyncRequest, SlidingSyncMatcher, assert_sliding_sync_presence_for_conn_ids, + check_requests, + }, sliding_sync_then_assert_request_and_fake_response, }; @@ -170,6 +173,24 @@ async fn setup_mocking_sliding_sync_server(server: &MockServer) -> MockGuard { .await } +#[async_test] +async fn test_encryption_sync_default_sync_presence_is_online() -> anyhow::Result<()> { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let _guard = setup_mocking_sliding_sync_server(&server).await; + + let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing())); + let sync_permit_guard = sync_permit.lock_owned().await; + let encryption_sync = EncryptionSyncService::new(client, None).await?; + + encryption_sync.run_fixed_iterations(1, sync_permit_guard).await?; + + assert_sliding_sync_presence_for_conn_ids(&server, None, &["encryption"]).await; + + Ok(()) +} + #[async_test] async fn test_encryption_sync_one_fixed_iteration() -> anyhow::Result<()> { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index 7ed49bbf87b..a157884a624 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -30,7 +30,10 @@ use wiremock::{ matchers::{header, method, path}, }; -use crate::sliding_sync::{PartialSlidingSyncRequest, SlidingSyncMatcher, check_requests}; +use crate::sliding_sync::{ + PartialSlidingSyncRequest, SlidingSyncMatcher, assert_sliding_sync_presence_for_conn_ids, + check_requests, +}; #[async_test] async fn test_notification_client_with_context() { @@ -494,6 +497,7 @@ async fn test_notification_client_sliding_sync() { })], ) .await; + assert_sliding_sync_presence_for_conn_ids(&server, None, &["notifications"]).await; let Some(Ok(item)) = result.remove(event_id) else { panic!("fetching notification for {event_id} failed"); diff --git a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs index 00c006a46b9..20783d73725 100644 --- a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs @@ -1,5 +1,7 @@ //! Helpers for integration tests involving sliding sync. +use std::collections::BTreeSet; + use wiremock::{Match, MockServer, Request, http::Method}; pub(crate) async fn check_requests(server: &MockServer, expected_requests: &[serde_json::Value]) { @@ -167,3 +169,42 @@ macro_rules! sliding_sync_then_assert_request_and_fake_response { (@assertion_config >=) => { assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive) }; (@assertion_config =) => { assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict) }; } + +pub(crate) async fn assert_sliding_sync_presence_for_conn_ids( + server: &MockServer, + expected_presence: Option<&str>, + expected_conn_ids: &[&str], +) { + let num_expected_conn_ids = expected_conn_ids.len(); + let expected_conn_ids = + expected_conn_ids.iter().map(|conn_id| (*conn_id).to_owned()).collect::>(); + assert_eq!(expected_conn_ids.len(), num_expected_conn_ids, "duplicate expected conn IDs"); + + let mut seen_conn_ids = BTreeSet::new(); + + for request in &server.received_requests().await.expect("Request recording has been disabled") { + if !SlidingSyncMatcher.matches(request) { + continue; + } + + let json_value = serde_json::from_slice::(&request.body).unwrap(); + let Some(conn_id) = json_value.get("conn_id").and_then(|obj| obj.as_str()) else { + panic!("sliding sync request missing conn_id: {json_value:?}"); + }; + assert!( + expected_conn_ids.contains(conn_id), + "unexpected conn id seen server side: {conn_id}" + ); + + seen_conn_ids.insert(conn_id.to_owned()); + + let set_presence = request + .url + .query_pairs() + .find_map(|(key, value)| (key == "set_presence").then_some(value.into_owned())); + + assert_eq!(set_presence.as_deref(), expected_presence, "conn_id: {conn_id}"); + } + + assert_eq!(seen_conn_ids, expected_conn_ids); +} diff --git a/crates/matrix-sdk-ui/tests/integration/sync_service.rs b/crates/matrix-sdk-ui/tests/integration/sync_service.rs index 26355e2abb2..f6a8fc9dd7e 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -21,11 +21,14 @@ use assert_matches::assert_matches; use matrix_sdk::{assert_next_matches_with_timeout, test_utils::mocks::MatrixMockServer}; use matrix_sdk_test::async_test; use matrix_sdk_ui::sync_service::{State, SyncService}; +use ruma::presence::PresenceState; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; use wiremock::{Match as _, Mock, MockGuard, MockServer, Request, ResponseTemplate}; -use crate::sliding_sync::{PartialSlidingSyncRequest, SlidingSyncMatcher}; +use crate::sliding_sync::{ + PartialSlidingSyncRequest, SlidingSyncMatcher, assert_sliding_sync_presence_for_conn_ids, +}; /// Sets up a sliding sync server that use different `pos` values for the /// encrptyion and the room sync. @@ -205,6 +208,49 @@ async fn test_sync_service_state() -> anyhow::Result<()> { Ok(()) } +#[async_test] +async fn test_sync_service_client_sync_presence_is_used_by_both_syncs() -> anyhow::Result<()> { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let encryption_pos = Arc::new(Mutex::new(0)); + let room_pos = Arc::new(Mutex::new(0)); + let _guard = setup_mocking_sliding_sync_server(&server, encryption_pos, room_pos).await; + + let sync_service = SyncService::builder(client).build().await.unwrap(); + sync_service.start().await; + + tokio::time::sleep(Duration::from_millis(150)).await; + + sync_service.stop().await; + + assert_sliding_sync_presence_for_conn_ids(&server, None, &["encryption", "room-list"]).await; + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + client.set_presence(PresenceState::Offline, None, false).await?; + + let encryption_pos = Arc::new(Mutex::new(0)); + let room_pos = Arc::new(Mutex::new(0)); + let _guard = setup_mocking_sliding_sync_server(&server, encryption_pos, room_pos).await; + + let sync_service = SyncService::builder(client).build().await.unwrap(); + sync_service.start().await; + + tokio::time::sleep(Duration::from_millis(150)).await; + + sync_service.stop().await; + + assert_sliding_sync_presence_for_conn_ids( + &server, + Some("offline"), + &["encryption", "room-list"], + ) + .await; + + Ok(()) +} + #[async_test] async fn test_sync_service_offline_mode() { let mock_server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/changelog.d/6672.added.md b/crates/matrix-sdk/changelog.d/6672.added.md new file mode 100644 index 00000000000..9a01ce26dc2 --- /dev/null +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -0,0 +1,4 @@ +Add client-owned presence configuration that can optionally send an immediate +presence update. Generated sync requests use the client presence by default, +which starts as unavailable, while classic sync settings can still override it +per request. diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 30585779b79..16e4c23a087 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -21,7 +21,11 @@ use std::collections::HashMap; use std::path::Path; #[cfg(any(feature = "experimental-search", feature = "sqlite"))] use std::path::PathBuf; -use std::{collections::BTreeSet, fmt, sync::Arc}; +use std::{ + collections::BTreeSet, + fmt, + sync::{Arc, RwLock as StdRwLock}, +}; #[cfg(feature = "sqlite")] use futures_util::try_join; @@ -41,6 +45,7 @@ use reqwest::Certificate; use ruma::{ OwnedServerName, ServerName, api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError}, + presence::PresenceState, }; use thiserror::Error; #[cfg(feature = "experimental-search")] @@ -648,6 +653,7 @@ impl ClientBuilder { server, homeserver, sliding_sync_version, + Arc::new(StdRwLock::new(PresenceState::Online)), http_client, base_client, supported_versions, diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index bb104eeb499..180eb28eb1b 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -60,6 +60,7 @@ use ruma::{ knock::knock_room, media, membership::{join_room_by_id, join_room_by_id_or_alias}, + presence::set_presence as set_presence_status, room::create_room, rtc::RtcTransport, session::login::v3::DiscoveryInfo, @@ -73,6 +74,7 @@ use ruma::{ }, assign, events::{beacon_info::OriginalSyncBeaconInfoEvent, direct::DirectUserIdentifier}, + presence::PresenceState, push::Ruleset, time::Instant, }; @@ -307,6 +309,12 @@ pub(crate) struct ClientInner { /// The sliding sync version. sliding_sync_version: StdRwLock, + /// Default presence state to send with generated sync requests. + /// + /// This is process-local. Consumers that create clients in multiple + /// processes must configure it in each process. + sync_presence: Arc>, + /// The underlying HTTP client. pub(crate) http_client: HttpClient, @@ -425,6 +433,7 @@ impl ClientInner { server: Option, homeserver: Url, sliding_sync_version: SlidingSyncVersion, + sync_presence: Arc>, http_client: HttpClient, base_client: BaseClient, supported_versions: CachedValue>, @@ -452,6 +461,7 @@ impl ClientInner { homeserver: StdRwLock::new(homeserver), auth_ctx, sliding_sync_version: StdRwLock::new(sliding_sync_version), + sync_presence, http_client, base_client, caches, @@ -679,6 +689,11 @@ impl Client { *lock = version; } + /// Get the default presence state used by generated sync requests. + pub(crate) fn sync_presence(&self) -> PresenceState { + self.inner.sync_presence.read().unwrap().clone() + } + /// Get the Matrix user session meta information. /// /// If the client is currently logged in, this will return a @@ -734,6 +749,34 @@ impl Client { self.auth_ctx().access_token() } + /// Set the presence state for the current user. + /// + /// The presence state is stored as the default used by future generated + /// sync requests, regardless of `immediate`. The initial default is + /// [`PresenceState::Online`]. If `immediate` is `true`, this also + /// calls the Matrix presence endpoint directly. `status_msg` is only sent + /// when `immediate` is `true`. + pub async fn set_presence( + &self, + presence: PresenceState, + status_msg: Option, + immediate: bool, + ) -> Result<()> { + *self.inner.sync_presence.write().unwrap() = presence.clone(); + + if !immediate { + return Ok(()); + } + + let user_id = self.user_id().ok_or(Error::AuthenticationRequired)?.to_owned(); + let mut request = set_presence_status::v3::Request::new(user_id, presence); + request.status_msg = status_msg; + + self.send(request).await?; + + Ok(()) + } + /// Get the current tokens for this session. /// /// To be notified of changes in the session tokens, use @@ -2783,8 +2826,11 @@ impl Client { /// and where we wish to continue syncing. /// * [`full_state`] - To tell the server that we wish to receive all /// state events, regardless of our configured [`token`]. - /// * [`set_presence`] - To tell the server to set the presence and to - /// which state. + /// * [`set_presence`] - To override the presence state sent with this + /// classic `/sync` request. If this is not set, the request uses the + /// client-owned sync presence configured with + /// [`Client::set_presence`], which defaults to + /// [`PresenceState::Online`]. /// /// # Examples /// @@ -2822,7 +2868,7 @@ impl Client { /// [`token`]: crate::config::SyncSettings#method.token /// [`timeout`]: crate::config::SyncSettings#method.timeout /// [`full_state`]: crate::config::SyncSettings#method.full_state - /// [`set_presence`]: ruma::presence::PresenceState + /// [`set_presence`]: crate::config::SyncSettings::set_presence /// [`filter`]: crate::config::SyncSettings#method.filter /// [`Filter`]: ruma::api::client::sync::sync_events::v3::Filter /// [`next_batch`]: SyncResponse#structfield.next_batch @@ -2855,7 +2901,7 @@ impl Client { filter: sync_settings.filter.map(|f| *f), since: token, full_state: sync_settings.full_state, - set_presence: sync_settings.set_presence, + set_presence: sync_settings.set_presence.unwrap_or_else(|| self.sync_presence()), timeout: sync_settings.timeout, use_state_after: true, }); @@ -3241,6 +3287,7 @@ impl Client { self.server().cloned(), self.homeserver(), self.sliding_sync_version(), + self.inner.sync_presence.clone(), self.inner.http_client.clone(), self.inner .base_client @@ -3664,7 +3711,9 @@ pub(crate) mod tests { ignored_user_list::IgnoredUserListEventContent, media_preview_config::{InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews}, }, - owned_device_id, owned_room_id, owned_user_id, room_alias_id, room_id, user_id, + owned_device_id, owned_room_id, owned_user_id, + presence::PresenceState, + room_alias_id, room_id, user_id, }; use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; @@ -3684,6 +3733,137 @@ pub(crate) mod tests { test_utils::{client::MockClientBuilder, mocks::MatrixMockServer}, }; + #[async_test] + async fn test_sync_presence_is_shared_by_client_clones_and_notification_child() { + let client = MockClientBuilder::new(None).build().await; + let clone = client.clone(); + let notification_client = + client.notification_client(CrossProcessLockConfig::SingleProcess).await.unwrap(); + + assert_eq!(client.sync_presence(), PresenceState::Online); + assert_eq!(clone.sync_presence(), PresenceState::Online); + assert_eq!(notification_client.sync_presence(), PresenceState::Online); + + client + .set_presence(PresenceState::Unavailable, None, false) + .await + .expect("presence should update"); + + assert_eq!(client.sync_presence(), PresenceState::Unavailable); + assert_eq!(clone.sync_presence(), PresenceState::Unavailable); + assert_eq!(notification_client.sync_presence(), PresenceState::Unavailable); + + notification_client + .set_presence(PresenceState::Offline, None, false) + .await + .expect("presence should update"); + + assert_eq!(client.sync_presence(), PresenceState::Offline); + assert_eq!(clone.sync_presence(), PresenceState::Offline); + assert_eq!(notification_client.sync_presence(), PresenceState::Offline); + } + + #[async_test] + async fn test_sync_once_uses_client_sync_presence_unless_overridden() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + { + let _sync_guard = server + .mock_sync() + .set_presence_missing() + .ok(|_| {}) + .expect(1) + .mount_as_scoped() + .await; + + client.sync_once(SyncSettings::new()).await.expect("sync should succeed"); + } + + client + .set_presence(PresenceState::Offline, None, false) + .await + .expect("presence should update"); + + { + let _sync_guard = server + .mock_sync() + .set_presence("offline") + .ok(|_| {}) + .expect(1) + .mount_as_scoped() + .await; + + client.sync_once(SyncSettings::new()).await.expect("sync should succeed"); + } + + { + let _sync_guard = server + .mock_sync() + .set_presence("unavailable") + .ok(|_| {}) + .expect(1) + .mount_as_scoped() + .await; + + client + .sync_once(SyncSettings::new().set_presence(PresenceState::Unavailable)) + .await + .expect("sync should succeed"); + } + } + + #[async_test] + async fn test_set_presence_sends_presence_status_update() { + use wiremock::{ + Mock, ResponseTemplate, + matchers::{body_partial_json, method, path_regex}, + }; + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/(r0|v3)/presence/.*/status$")) + .and(body_partial_json(json!({ + "presence": "online", + "status_msg": "Here" + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .mount(server.server()) + .await; + + client + .set_presence(PresenceState::Online, Some("Here".to_owned()), true) + .await + .expect("presence update should succeed"); + + assert_eq!(client.sync_presence(), PresenceState::Online); + } + + #[async_test] + async fn test_set_presence_requires_authentication() { + let client = MockClientBuilder::new(None).unlogged().build().await; + + assert_matches!( + client.set_presence(PresenceState::Unavailable, None, true).await, + Err(Error::AuthenticationRequired) + ); + } + + #[async_test] + async fn test_set_presence_without_immediate_does_not_require_authentication() { + let client = MockClientBuilder::new(None).unlogged().build().await; + + client + .set_presence(PresenceState::Offline, None, false) + .await + .expect("presence should update"); + + assert_eq!(client.sync_presence(), PresenceState::Offline); + } + #[async_test] async fn test_account_data() { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/config/sync.rs b/crates/matrix-sdk/src/config/sync.rs index 7468a98c8a0..4ca0e6fd402 100644 --- a/crates/matrix-sdk/src/config/sync.rs +++ b/crates/matrix-sdk/src/config/sync.rs @@ -62,7 +62,7 @@ pub struct SyncSettings { pub(crate) ignore_timeout_on_first_sync: bool, pub(crate) token: SyncToken, pub(crate) full_state: bool, - pub(crate) set_presence: PresenceState, + pub(crate) set_presence: Option, } impl Default for SyncSettings { @@ -87,7 +87,7 @@ impl fmt::Debug for SyncSettings { .maybe_field("timeout", timeout) .field("ignore_timeout_on_first_sync", ignore_timeout_on_first_sync) .field("full_state", full_state) - .field("set_presence", set_presence) + .maybe_field("set_presence", set_presence) .finish() } } @@ -102,7 +102,7 @@ impl SyncSettings { ignore_timeout_on_first_sync: false, token: SyncToken::default(), full_state: false, - set_presence: PresenceState::Online, + set_presence: None, } } @@ -182,21 +182,31 @@ impl SyncSettings { self } - /// Set the presence state + /// Override the presence state for this classic `/sync` request. + /// + /// If this is not set, the request uses the client-owned sync presence + /// value configured with [`Client::set_presence`]. The client default is + /// [`PresenceState::Online`]. /// /// `PresenceState::Online` - The client is marked as being online. This is - /// the default preset. + /// the active preset and client default. /// /// `PresenceState::Offline` - The client is not marked as being online. /// - /// `PresenceState::Unavailable` - The client is marked as being idle. + /// `PresenceState::Unavailable` - The client is marked as being idle. This + /// is the idle preset. + /// + /// Sliding Sync requests do not use this per-request setting; they read the + /// client-owned sync presence value directly. /// /// # Arguments /// * `set_presence` - The `PresenceState` that the server should set for /// the client. + /// + /// [`Client::set_presence`]: crate::Client::set_presence #[must_use] pub fn set_presence(mut self, presence: PresenceState) -> Self { - self.set_presence = presence; + self.set_presence = Some(presence); self } } diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 67aafc2bfb2..5f32b2e43e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -506,6 +506,7 @@ impl SlidingSync { let mut request = assign!(http::Request::new(), { conn_id: Some(self.inner.id.clone()), pos, + set_presence: self.inner.client.sync_presence(), timeout, lists: requests_lists, }); @@ -968,7 +969,9 @@ mod tests { use ruma::{ OwnedRoomId, assign, events::{direct::DirectEvent, room::member::MembershipState}, - owned_room_id, room_id, + owned_room_id, + presence::PresenceState, + room_id, serde::Raw, uint, }; @@ -1014,6 +1017,36 @@ mod tests { Ok((server, sliding_sync)) } + #[async_test] + async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> { + let (_server, sliding_sync) = new_sliding_sync(vec![]).await?; + let client = sliding_sync.inner.client.clone(); + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Online); + } + + client.set_presence(PresenceState::Unavailable, None, false).await?; + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Unavailable); + } + + client.set_presence(PresenceState::Offline, None, false).await?; + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Offline); + } + + Ok(()) + } + #[async_test] async fn test_subscribe_to_rooms() -> Result<()> { let (server, sliding_sync) = new_sliding_sync(vec![ diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index df2a480d7e0..81efc2460a5 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -2813,6 +2813,18 @@ impl<'a> MockEndpoint<'a, SyncEndpoint> { self } + /// Expect the given `set_presence` value in the request. + pub fn set_presence(mut self, presence: impl Into) -> Self { + self.mock = self.mock.and(query_param("set_presence", presence.into())); + self + } + + /// Expect no explicit `set_presence` value in the request. + pub fn set_presence_missing(mut self) -> Self { + self.mock = self.mock.and(query_param_is_missing("set_presence")); + self + } + /// Mocks the sync endpoint, using the given function to generate the /// response. pub fn ok(self, func: F) -> MatrixMock<'a> {