From dd577fdc3cd99ef9f5bc1a85d9b27a345ff132a6 Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Sun, 31 May 2026 18:20:53 +1000 Subject: [PATCH 1/9] feat(sliding-sync): allow configuring sync presence Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../src/encryption_sync_service.rs | 18 ++++++- .../src/room_list_service/mod.rs | 27 +++++++++- crates/matrix-sdk-ui/src/sync_service.rs | 27 +++++++++- .../tests/integration/sync_service.rs | 52 +++++++++++++++++++ crates/matrix-sdk/src/sliding_sync/builder.rs | 14 ++++- crates/matrix-sdk/src/sliding_sync/mod.rs | 52 ++++++++++++++++++- 6 files changed, 184 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-ui/src/encryption_sync_service.rs b/crates/matrix-sdk-ui/src/encryption_sync_service.rs index 27b33d18d15..bc36dc3fc66 100644 --- a/crates/matrix-sdk-ui/src/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/src/encryption_sync_service.rs @@ -33,7 +33,7 @@ use futures_core::stream::Stream; use futures_util::{StreamExt, pin_mut}; use matrix_sdk::{Client, LEASE_DURATION_MS, SlidingSync, sleep::sleep}; use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig; -use ruma::{api::client::sync::sync_events::v5 as http, assign}; +use ruma::{api::client::sync::sync_events::v5 as http, assign, presence::PresenceState}; use tokio::sync::OwnedMutexGuard; use tracing::{debug, instrument, trace}; @@ -74,6 +74,16 @@ impl EncryptionSyncService { pub async fn new( client: Client, poll_and_network_timeouts: Option<(Duration, Duration)>, + ) -> Result { + Self::new_with_presence(client, poll_and_network_timeouts, PresenceState::Online).await + } + + /// Creates a new instance of an `EncryptionSyncService` with an explicit + /// presence state for the generated sliding sync requests. + pub async fn new_with_presence( + client: Client, + poll_and_network_timeouts: Option<(Duration, Duration)>, + presence: PresenceState, ) -> Result { // Make sure to use the same `conn_id` and caching store identifier, whichever // process is running this sliding sync. There must be at most one @@ -81,6 +91,7 @@ impl EncryptionSyncService { let mut builder = client .sliding_sync("encryption") .map_err(Error::SlidingSync)? + .set_presence(presence) //.share_pos() // TODO: This is racy, needs cross-process lock :') .with_to_device_extension( assign!(http::request::ToDevice::default(), { enabled: Some(true)}), @@ -113,6 +124,11 @@ impl EncryptionSyncService { Ok(Self { client, sliding_sync }) } + /// Set the presence state to send with future sliding sync requests. + pub fn set_presence(&self, presence: PresenceState) { + self.sliding_sync.set_presence(presence); + } + /// Runs an `EncryptionSyncService` loop for a fixed number of iterations. /// /// This runs for the given number of iterations, or less than that, if it diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index ed882e3c256..9db06885818 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -68,7 +68,7 @@ use matrix_sdk::{ pub use room_list::*; use ruma::{ OwnedRoomId, RoomId, UInt, api::client::sync::sync_events::v5 as http, assign, - events::StateEventType, + events::StateEventType, presence::PresenceState, }; pub use state::*; use thiserror::Error; @@ -157,10 +157,30 @@ impl RoomListService { share_pos: bool, connection_id: &str, timeline_limit: u32, + ) -> Result { + Self::new_with_presence( + client, + share_pos, + connection_id, + timeline_limit, + PresenceState::Online, + ) + .await + } + + /// Like [`RoomListService::new_with`] but with an explicit presence state + /// for the generated sliding sync requests. + pub async fn new_with_presence( + client: Client, + share_pos: bool, + connection_id: &str, + timeline_limit: u32, + presence: PresenceState, ) -> Result { let mut builder = client .sliding_sync(connection_id) .map_err(Error::SlidingSync)? + .set_presence(presence) .with_account_data_extension( assign!(http::request::AccountData::default(), { enabled: Some(true) }), ) @@ -268,6 +288,11 @@ impl RoomListService { Ok(Self { client, sliding_sync, state_machine }) } + /// Set the presence state to send with future sliding sync requests. + pub fn set_presence(&self, presence: PresenceState) { + self.sliding_sync.set_presence(presence); + } + /// Start to sync the room list. /// /// It's the main method of this entire API. Calling `sync` allows to diff --git a/crates/matrix-sdk-ui/src/sync_service.rs b/crates/matrix-sdk-ui/src/sync_service.rs index 971fb6db6c9..b412da0d715 100644 --- a/crates/matrix-sdk-ui/src/sync_service.rs +++ b/crates/matrix-sdk-ui/src/sync_service.rs @@ -38,6 +38,7 @@ use matrix_sdk::{ executor::{JoinHandle, spawn}, sleep::sleep, }; +use ruma::presence::PresenceState; use thiserror::Error; use tokio::sync::{ Mutex as AsyncMutex, OwnedMutexGuard, @@ -618,6 +619,12 @@ impl SyncService { self.state.subscribe() } + /// Set the presence state to send with future sliding sync requests. + pub async fn set_presence(&self, presence: PresenceState) { + self.room_list_service.set_presence(presence.clone()); + self.inner.lock().await.encryption_sync_service.set_presence(presence); + } + /// Start (or restart) the underlying sliding syncs. /// /// This can be called multiple times safely: @@ -785,6 +792,9 @@ pub struct SyncServiceBuilder { /// [`room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`]. room_list_timeline_limit: u32, + /// Presence state to send with the generated sliding sync requests. + presence: PresenceState, + /// The parent tracing span to use for the tasks within this service. /// /// Normally this will be [`Span::none`], but it may be useful to assign a @@ -801,6 +811,7 @@ impl SyncServiceBuilder { with_share_pos: true, room_list_conn_id: DEFAULT_CONNECTION_ID.to_owned(), room_list_timeline_limit: DEFAULT_LIST_TIMELINE_LIMIT, + presence: PresenceState::Online, parent_span: Span::none(), } } @@ -834,6 +845,15 @@ impl SyncServiceBuilder { self } + /// Set the presence state to send with the generated sliding sync requests. + /// + /// The default is [`PresenceState::Online`], matching the Matrix + /// Client-Server API default when `set_presence` is not specified. + pub fn with_presence(mut self, presence: PresenceState) -> Self { + self.presence = presence; + self + } + /// Set the parent tracing span to be used for the tasks within this /// service. pub fn with_parent_span(mut self, parent_span: Span) -> Self { @@ -853,20 +873,23 @@ impl SyncServiceBuilder { with_share_pos, room_list_conn_id, room_list_timeline_limit, + presence, parent_span, } = self; let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new())); - let room_list = RoomListService::new_with( + let room_list = RoomListService::new_with_presence( client.clone(), with_share_pos, &room_list_conn_id, room_list_timeline_limit, + presence.clone(), ) .await?; - let encryption_sync = Arc::new(EncryptionSyncService::new(client, None).await?); + let encryption_sync = + Arc::new(EncryptionSyncService::new_with_presence(client, None, presence).await?); let room_list_service = Arc::new(room_list); let state = SharedObservable::new(State::Idle); diff --git a/crates/matrix-sdk-ui/tests/integration/sync_service.rs b/crates/matrix-sdk-ui/tests/integration/sync_service.rs index 26355e2abb2..82d27c4e073 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::{ + collections::BTreeSet, sync::{Arc, Mutex}, time::Duration, }; @@ -21,6 +22,7 @@ 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}; @@ -205,6 +207,56 @@ async fn test_sync_service_state() -> anyhow::Result<()> { Ok(()) } +#[async_test] +async fn test_sync_service_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.set_presence(PresenceState::Unavailable).await; + sync_service.start().await; + + tokio::time::sleep(Duration::from_millis(150)).await; + + sync_service.stop().await; + + let mut conn_ids_with_presence = 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 { + continue; + }; + + if conn_id != "encryption" && conn_id != "room-list" { + panic!("unexpected conn id seen server side: {conn_id}"); + } + + let set_presence = request + .url + .query_pairs() + .find_map(|(key, value)| (key == "set_presence").then_some(value.into_owned())); + + if set_presence.as_deref() == Some("unavailable") { + conn_ids_with_presence.insert(conn_id.to_owned()); + } + } + + assert_eq!( + conn_ids_with_presence, + BTreeSet::from(["encryption".to_owned(), "room-list".to_owned()]) + ); + + Ok(()) +} + #[async_test] async fn test_sync_service_offline_mode() { let mock_server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 00f237605d3..ba22e86af5a 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -7,7 +7,7 @@ use std::{ use cfg_if::cfg_if; use matrix_sdk_common::timer; -use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http}; +use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http, presence::PresenceState}; use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, broadcast::channel}; use super::{ @@ -29,6 +29,7 @@ pub struct SlidingSyncBuilder { lists: Vec, extensions: Option, room_subscriptions: BTreeMap, + presence: PresenceState, poll_timeout: Duration, network_timeout: Duration, #[cfg(feature = "e2e-encryption")] @@ -51,6 +52,7 @@ impl SlidingSyncBuilder { lists: Vec::new(), extensions: None, room_subscriptions: BTreeMap::new(), + presence: PresenceState::Online, poll_timeout: Duration::from_secs(30), network_timeout: Duration::from_secs(30), #[cfg(feature = "e2e-encryption")] @@ -88,6 +90,15 @@ impl SlidingSyncBuilder { Ok(self.add_list(list)) } + /// Set the presence state that will be sent with sliding sync requests. + /// + /// The default is [`PresenceState::Online`], matching the Matrix + /// Client-Server API default when `set_presence` is not specified. + pub fn set_presence(mut self, presence: PresenceState) -> Self { + self.presence = presence; + self + } + /// Activate e2ee, to-device-message, account data, typing and receipt /// extensions if not yet configured. /// @@ -290,6 +301,7 @@ impl SlidingSyncBuilder { room_subscriptions: StdRwLock::new(self.room_subscriptions), extensions: self.extensions.unwrap_or_default(), + presence: StdRwLock::new(self.presence), internal_channel: internal_channel_sender, diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 67aafc2bfb2..d28a9633b82 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -40,6 +40,7 @@ use ruma::{ OwnedRoomId, RoomId, api::{client::sync::sync_events::v5 as http, error::ErrorKind}, assign, + presence::PresenceState, }; use tokio::{ select, @@ -113,6 +114,9 @@ pub(super) struct SlidingSyncInner { /// calls. extensions: http::request::Extensions, + /// The presence state sent with sliding sync requests. + presence: StdRwLock, + /// Internal channel used to pass messages between Sliding Sync and other /// types. internal_channel: Sender, @@ -132,6 +136,18 @@ impl SlidingSync { SlidingSyncBuilder::new(id, client) } + /// Set the presence state to send with future sliding sync requests. + /// + /// The default is [`PresenceState::Online`], matching the Matrix + /// Client-Server API default when `set_presence` is not specified. + pub fn set_presence(&self, presence: PresenceState) { + *self.inner.presence.write().unwrap() = presence; + } + + fn presence(&self) -> PresenceState { + self.inner.presence.read().unwrap().clone() + } + /// Add subscriptions to many rooms. /// /// If the associated `Room`s exist, they will be marked as members are @@ -506,6 +522,7 @@ impl SlidingSync { let mut request = assign!(http::Request::new(), { conn_id: Some(self.inner.id.clone()), pos, + set_presence: self.presence(), timeout, lists: requests_lists, }); @@ -968,7 +985,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 +1033,37 @@ mod tests { Ok((server, sliding_sync)) } + #[async_test] + async fn test_sliding_sync_request_uses_configured_presence() -> Result<()> { + let (_server, sliding_sync) = new_sliding_sync(vec![]).await?; + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Online); + } + + sliding_sync.set_presence(PresenceState::Unavailable); + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Unavailable); + } + + let client = logged_in_client(None).await; + let sliding_sync = + client.sliding_sync("presence")?.set_presence(PresenceState::Offline).build().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![ From 8f54378d0c3790cb038f4673ee5b9dd0032c4bcf Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:16:49 +1000 Subject: [PATCH 2/9] doc(changelog): document sliding sync presence configuration Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- crates/matrix-sdk-ui/changelog.d/6672.added.md | 3 +++ crates/matrix-sdk/changelog.d/6672.added.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 crates/matrix-sdk-ui/changelog.d/6672.added.md create mode 100644 crates/matrix-sdk/changelog.d/6672.added.md 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..1752318c4f9 --- /dev/null +++ b/crates/matrix-sdk-ui/changelog.d/6672.added.md @@ -0,0 +1,3 @@ +Allow configuring the presence state used by `SyncService`, `RoomListService`, +and `EncryptionSyncService` sliding sync requests, so background sync flows can +avoid marking the user online. 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..84260ec82b4 --- /dev/null +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -0,0 +1,3 @@ +Allow configuring the presence state sent with sliding sync requests. This lets +clients perform sliding sync with `offline` or `unavailable` presence instead +of implicitly using the default online presence. From 01a868f5e1832100e0ddd15a950fc590f584bdbd Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:21:56 +1000 Subject: [PATCH 3/9] feat: add client-owned sync presence Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../matrix-sdk-ffi/changelog.d/6672.added.md | 1 + bindings/matrix-sdk-ffi/src/client.rs | 19 +- bindings/matrix-sdk-ffi/src/ruma.rs | 51 ++++++ .../matrix-sdk-ui/changelog.d/6672.added.md | 6 +- .../src/encryption_sync_service.rs | 18 +- .../src/room_list_service/mod.rs | 27 +-- crates/matrix-sdk-ui/src/sync_service.rs | 27 +-- .../tests/integration/notification_client.rs | 173 +++++++++++++++++- .../tests/integration/sync_service.rs | 4 +- crates/matrix-sdk/changelog.d/6672.added.md | 6 +- crates/matrix-sdk/src/client/builder/mod.rs | 8 +- crates/matrix-sdk/src/client/mod.rs | 138 +++++++++++++- crates/matrix-sdk/src/config/sync.rs | 13 +- crates/matrix-sdk/src/sliding_sync/builder.rs | 10 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 45 ++++- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 6 + 16 files changed, 447 insertions(+), 105 deletions(-) create mode 100644 bindings/matrix-sdk-ffi/changelog.d/6672.added.md 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..9b3fb7598aa --- /dev/null +++ b/bindings/matrix-sdk-ffi/changelog.d/6672.added.md @@ -0,0 +1 @@ +Expose client-level sync presence configuration and explicit presence updates. diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 22a9757bd60..f229b6a3769 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, RoomAccountDataEventType, + MediaPreviews, MediaSource, PresenceState, RoomAccountDataEvent, RoomAccountDataEventType, }, runtime::get_runtime_handle, spaces::SpaceService, @@ -1168,6 +1168,23 @@ impl Client { self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect() } + /// Set the default presence state used by future generated sync requests. + /// + /// This does not send an immediate presence update to the homeserver. + pub fn set_sync_presence(&self, presence: PresenceState) { + self.inner.set_sync_presence(presence.into()); + } + + /// Get the default presence state used by generated sync requests. + pub fn sync_presence(&self) -> PresenceState { + self.inner.sync_presence().into() + } + + /// Send an immediate presence update for the current user. + pub async fn set_presence(&self, presence: PresenceState) -> Result<(), ClientError> { + Ok(self.inner.set_presence(presence.into(), None).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 74900649104..f81490b9f18 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -79,6 +79,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, @@ -191,6 +192,56 @@ impl From<&RumaMatrixId> for MatrixId { } } +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum, Default)] +pub enum PresenceState { + #[default] + Online, + Offline, + 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(), + } + } +} + +#[cfg(test)] +mod tests { + use ruma::presence::PresenceState as RumaPresenceState; + + use super::PresenceState; + + #[test] + fn presence_state_conversions() { + let cases = [ + (PresenceState::Online, RumaPresenceState::Online), + (PresenceState::Offline, RumaPresenceState::Offline), + (PresenceState::Unavailable, RumaPresenceState::Unavailable), + ]; + + for (ffi_presence, ruma_presence) in cases { + assert_eq!(RumaPresenceState::from(ffi_presence.clone()), ruma_presence); + assert_eq!(PresenceState::from(ruma_presence), ffi_presence); + } + } +} + #[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 index 1752318c4f9..36ba620194c 100644 --- a/crates/matrix-sdk-ui/changelog.d/6672.added.md +++ b/crates/matrix-sdk-ui/changelog.d/6672.added.md @@ -1,3 +1,3 @@ -Allow configuring the presence state used by `SyncService`, `RoomListService`, -and `EncryptionSyncService` sliding sync requests, so background sync flows can -avoid marking the user online. +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/src/encryption_sync_service.rs b/crates/matrix-sdk-ui/src/encryption_sync_service.rs index bc36dc3fc66..27b33d18d15 100644 --- a/crates/matrix-sdk-ui/src/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/src/encryption_sync_service.rs @@ -33,7 +33,7 @@ use futures_core::stream::Stream; use futures_util::{StreamExt, pin_mut}; use matrix_sdk::{Client, LEASE_DURATION_MS, SlidingSync, sleep::sleep}; use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig; -use ruma::{api::client::sync::sync_events::v5 as http, assign, presence::PresenceState}; +use ruma::{api::client::sync::sync_events::v5 as http, assign}; use tokio::sync::OwnedMutexGuard; use tracing::{debug, instrument, trace}; @@ -74,16 +74,6 @@ impl EncryptionSyncService { pub async fn new( client: Client, poll_and_network_timeouts: Option<(Duration, Duration)>, - ) -> Result { - Self::new_with_presence(client, poll_and_network_timeouts, PresenceState::Online).await - } - - /// Creates a new instance of an `EncryptionSyncService` with an explicit - /// presence state for the generated sliding sync requests. - pub async fn new_with_presence( - client: Client, - poll_and_network_timeouts: Option<(Duration, Duration)>, - presence: PresenceState, ) -> Result { // Make sure to use the same `conn_id` and caching store identifier, whichever // process is running this sliding sync. There must be at most one @@ -91,7 +81,6 @@ impl EncryptionSyncService { let mut builder = client .sliding_sync("encryption") .map_err(Error::SlidingSync)? - .set_presence(presence) //.share_pos() // TODO: This is racy, needs cross-process lock :') .with_to_device_extension( assign!(http::request::ToDevice::default(), { enabled: Some(true)}), @@ -124,11 +113,6 @@ impl EncryptionSyncService { Ok(Self { client, sliding_sync }) } - /// Set the presence state to send with future sliding sync requests. - pub fn set_presence(&self, presence: PresenceState) { - self.sliding_sync.set_presence(presence); - } - /// Runs an `EncryptionSyncService` loop for a fixed number of iterations. /// /// This runs for the given number of iterations, or less than that, if it diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 9db06885818..ed882e3c256 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -68,7 +68,7 @@ use matrix_sdk::{ pub use room_list::*; use ruma::{ OwnedRoomId, RoomId, UInt, api::client::sync::sync_events::v5 as http, assign, - events::StateEventType, presence::PresenceState, + events::StateEventType, }; pub use state::*; use thiserror::Error; @@ -157,30 +157,10 @@ impl RoomListService { share_pos: bool, connection_id: &str, timeline_limit: u32, - ) -> Result { - Self::new_with_presence( - client, - share_pos, - connection_id, - timeline_limit, - PresenceState::Online, - ) - .await - } - - /// Like [`RoomListService::new_with`] but with an explicit presence state - /// for the generated sliding sync requests. - pub async fn new_with_presence( - client: Client, - share_pos: bool, - connection_id: &str, - timeline_limit: u32, - presence: PresenceState, ) -> Result { let mut builder = client .sliding_sync(connection_id) .map_err(Error::SlidingSync)? - .set_presence(presence) .with_account_data_extension( assign!(http::request::AccountData::default(), { enabled: Some(true) }), ) @@ -288,11 +268,6 @@ impl RoomListService { Ok(Self { client, sliding_sync, state_machine }) } - /// Set the presence state to send with future sliding sync requests. - pub fn set_presence(&self, presence: PresenceState) { - self.sliding_sync.set_presence(presence); - } - /// Start to sync the room list. /// /// It's the main method of this entire API. Calling `sync` allows to diff --git a/crates/matrix-sdk-ui/src/sync_service.rs b/crates/matrix-sdk-ui/src/sync_service.rs index b412da0d715..971fb6db6c9 100644 --- a/crates/matrix-sdk-ui/src/sync_service.rs +++ b/crates/matrix-sdk-ui/src/sync_service.rs @@ -38,7 +38,6 @@ use matrix_sdk::{ executor::{JoinHandle, spawn}, sleep::sleep, }; -use ruma::presence::PresenceState; use thiserror::Error; use tokio::sync::{ Mutex as AsyncMutex, OwnedMutexGuard, @@ -619,12 +618,6 @@ impl SyncService { self.state.subscribe() } - /// Set the presence state to send with future sliding sync requests. - pub async fn set_presence(&self, presence: PresenceState) { - self.room_list_service.set_presence(presence.clone()); - self.inner.lock().await.encryption_sync_service.set_presence(presence); - } - /// Start (or restart) the underlying sliding syncs. /// /// This can be called multiple times safely: @@ -792,9 +785,6 @@ pub struct SyncServiceBuilder { /// [`room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`]. room_list_timeline_limit: u32, - /// Presence state to send with the generated sliding sync requests. - presence: PresenceState, - /// The parent tracing span to use for the tasks within this service. /// /// Normally this will be [`Span::none`], but it may be useful to assign a @@ -811,7 +801,6 @@ impl SyncServiceBuilder { with_share_pos: true, room_list_conn_id: DEFAULT_CONNECTION_ID.to_owned(), room_list_timeline_limit: DEFAULT_LIST_TIMELINE_LIMIT, - presence: PresenceState::Online, parent_span: Span::none(), } } @@ -845,15 +834,6 @@ impl SyncServiceBuilder { self } - /// Set the presence state to send with the generated sliding sync requests. - /// - /// The default is [`PresenceState::Online`], matching the Matrix - /// Client-Server API default when `set_presence` is not specified. - pub fn with_presence(mut self, presence: PresenceState) -> Self { - self.presence = presence; - self - } - /// Set the parent tracing span to be used for the tasks within this /// service. pub fn with_parent_span(mut self, parent_span: Span) -> Self { @@ -873,23 +853,20 @@ impl SyncServiceBuilder { with_share_pos, room_list_conn_id, room_list_timeline_limit, - presence, parent_span, } = self; let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new())); - let room_list = RoomListService::new_with_presence( + let room_list = RoomListService::new_with( client.clone(), with_share_pos, &room_list_conn_id, room_list_timeline_limit, - presence.clone(), ) .await?; - let encryption_sync = - Arc::new(EncryptionSyncService::new_with_presence(client, None, presence).await?); + let encryption_sync = Arc::new(EncryptionSyncService::new(client, None).await?); let room_list_service = Arc::new(room_list); let state = SharedObservable::new(State::Idle); diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index 7ed49bbf87b..c41d832621e 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -21,17 +21,62 @@ use ruma::{ RoomVersionId, api::client::sync::sync_events::v5::{Response, response}, assign, event_id, - events::{Mentions, TimelineEventType, room::member::MembershipState}, - mxc_uri, owned_user_id, room_id, uint, user_id, + events::{ + Mentions, TimelineEventType, + room::{ + encrypted::{ + EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent, + }, + member::MembershipState, + }, + }, + mxc_uri, owned_device_id, owned_user_id, + presence::PresenceState, + room_id, uint, user_id, }; use serde_json::json; use wiremock::{ - Mock, Request, ResponseTemplate, + Match as _, Mock, Request, ResponseTemplate, matchers::{header, method, path}, }; use crate::sliding_sync::{PartialSlidingSyncRequest, SlidingSyncMatcher, check_requests}; +async fn assert_sliding_sync_presence_for_conn_ids( + server: &MatrixMockServer, + expected_presence: &str, + expected_conn_ids: &[&str], +) { + let expected_conn_ids = + expected_conn_ids.iter().map(|conn_id| (*conn_id).to_owned()).collect::>(); + 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 { + continue; + }; + if !expected_conn_ids.contains(conn_id) { + continue; + } + + 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(), Some(expected_presence), "conn_id: {conn_id}"); + } + + assert_eq!(seen_conn_ids, expected_conn_ids); +} + #[async_test] async fn test_notification_client_with_context() { let server = MatrixMockServer::new().await; @@ -330,7 +375,7 @@ async fn test_unsubscribed_threads_get_notifications() { } #[async_test] -async fn test_notification_client_sliding_sync() { +async fn test_notification_client_sliding_sync_uses_client_sync_presence() { let room_id = room_id!("!a98sd12bjh:example.org"); let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -431,7 +476,8 @@ async fn test_notification_client_sliding_sync() { let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); let process_setup = NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service }; - let notification_client = NotificationClient::new(client, process_setup).await.unwrap(); + let notification_client = NotificationClient::new(client.clone(), process_setup).await.unwrap(); + client.set_sync_presence(PresenceState::Unavailable); let mut result = notification_client .get_notifications_with_sliding_sync(&[NotificationItemsRequest { room_id: room_id.to_owned(), @@ -494,6 +540,7 @@ async fn test_notification_client_sliding_sync() { })], ) .await; + assert_sliding_sync_presence_for_conn_ids(&server, "unavailable", &["notifications"]).await; let Some(Ok(item)) = result.remove(event_id) else { panic!("fetching notification for {event_id} failed"); @@ -518,6 +565,122 @@ async fn test_notification_client_sliding_sync() { assert_eq!(item.is_noisy, Some(false)); } +#[async_test] +async fn test_notification_client_encryption_fallback_uses_client_sync_presence() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let event_id = event_id!("$example_event_id"); + let sender = user_id!("@user:example.org"); + let my_user_id = client.user_id().unwrap().to_owned(); + + let event_factory = EventFactory::new().room(room_id); + let own_member_event = event_factory + .member(&my_user_id) + .display_name("My self") + .membership(MembershipState::Join) + .into_raw_sync(); + let sender_member_event = + event_factory.member(sender).membership(MembershipState::Join).into_raw_sync(); + let power_levels_event = + event_factory.power_levels(&mut BTreeMap::new()).sender(sender).into_raw_sync(); + let room_encryption_event = event_factory.room_encryption().sender(sender).into_raw_sync(); + let encrypted_event = event_factory + .sender(sender) + .event(RoomEncryptedEventContent::new( + EncryptedEventScheme::MegolmV1AesSha2( + MegolmV1AesSha2ContentInit { + ciphertext: String::from( + "AwgAEpABhetEzzZzyYrxtEVUtlJnZtJcURBlQUQJ9irVeklCTs06LwgTMQj61PMUS4Vy\ + YOX+PD67+hhU40/8olOww+Ud0m2afjMjC3wFX+4fFfSkoWPVHEmRVucfcdSF1RSB4EmK\ + PIP4eo1X6x8kCIMewBvxl2sI9j4VNvDvAN7M3zkLJfFLOFHbBviI4FN7hSFHFeM739Zg\ + iwxEs3hIkUXEiAfrobzaMEM/zY7SDrTdyffZndgJo7CZOVhoV6vuaOhmAy4X2t4UnbuV\ + JGJjKfV57NAhp8W+9oT7ugwO", + ), + device_id: owned_device_id!("KIUVQQSDTM"), + sender_key: String::from("LvryVyoCjdONdBCi2vvoSbI34yTOx7YrCFACUEKoXnc"), + session_id: String::from("64H7XKokIx0ASkYDHZKlT5zd/Zccz/cQspPNdvnNULA"), + } + .into(), + ), + None, + )) + .event_id(event_id) + .into_raw_sync(); + + let notification_pos = Mutex::new(0); + let encryption_pos = Mutex::new(0); + Mock::given(SlidingSyncMatcher) + .respond_with(move |request: &Request| { + let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap(); + + match partial_request.conn_id.as_deref() { + Some("notifications") => { + let mut pos = notification_pos.lock().unwrap(); + *pos += 1; + let pos_as_str = (*pos).to_string(); + + ResponseTemplate::new(200).set_body_json(json!({ + "txn_id": partial_request.txn_id, + "pos": pos_as_str, + "rooms": { + room_id: { + "name": "The Maltese Falcon", + "initial": true, + "required_state": [ + sender_member_event.clone(), + own_member_event.clone(), + power_levels_event.clone(), + room_encryption_event.clone(), + ], + "timeline": [ + encrypted_event.clone(), + ] + } + }, + "extensions": { + "account_data": {} + } + })) + } + Some("encryption") => { + let mut pos = encryption_pos.lock().unwrap(); + *pos += 1; + let pos_as_str = (*pos).to_string(); + + ResponseTemplate::new(200).set_body_json(json!({ + "txn_id": partial_request.txn_id, + "pos": pos_as_str + })) + } + other => panic!("unexpected conn id {other:?}"), + } + }) + .mount(server.server()) + .await; + + let notification_client = + NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses) + .await + .unwrap(); + client.set_sync_presence(PresenceState::Unavailable); + + let _ = notification_client + .get_notifications_with_sliding_sync(&[NotificationItemsRequest { + room_id: room_id.to_owned(), + event_ids: vec![event_id.to_owned()], + }]) + .await; + + assert_sliding_sync_presence_for_conn_ids( + &server, + "unavailable", + &["notifications", "encryption"], + ) + .await; +} + #[async_test] async fn test_notification_client_sliding_sync_invites() { let room_id = room_id!("!a98sd12bjh:example.org"); diff --git a/crates/matrix-sdk-ui/tests/integration/sync_service.rs b/crates/matrix-sdk-ui/tests/integration/sync_service.rs index 82d27c4e073..7e0af921e22 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -208,16 +208,16 @@ async fn test_sync_service_state() -> anyhow::Result<()> { } #[async_test] -async fn test_sync_service_presence_is_used_by_both_syncs() -> anyhow::Result<()> { +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; + client.set_sync_presence(PresenceState::Unavailable); 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.set_presence(PresenceState::Unavailable).await; sync_service.start().await; tokio::time::sleep(Duration::from_millis(150)).await; diff --git a/crates/matrix-sdk/changelog.d/6672.added.md b/crates/matrix-sdk/changelog.d/6672.added.md index 84260ec82b4..d559af2a918 100644 --- a/crates/matrix-sdk/changelog.d/6672.added.md +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -1,3 +1,3 @@ -Allow configuring the presence state sent with sliding sync requests. This lets -clients perform sliding sync with `offline` or `unavailable` presence instead -of implicitly using the default online presence. +Add client-owned sync presence configuration and an explicit presence update +API. Generated sync requests use the client sync presence by default, while +classic sync settings and low-level sliding sync instances can still override it. diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 84d1fcd9c5a..db55b3236ee 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")] @@ -658,6 +663,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..dac91cb512f 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,21 @@ impl Client { *lock = version; } + /// Set the default presence state to send with generated sync requests. + /// + /// This affects future sync requests that don't configure an explicit + /// per-request or per-instance presence override. It doesn't affect + /// requests that are already in flight, and it doesn't send an immediate + /// presence update to the homeserver. + pub fn set_sync_presence(&self, presence: PresenceState) { + *self.inner.sync_presence.write().unwrap() = presence; + } + + /// Get the default presence state used by generated sync requests. + pub 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 +759,25 @@ impl Client { self.auth_ctx().access_token() } + /// Send an immediate presence update for the current user. + /// + /// This calls the Matrix presence endpoint directly. It is separate from + /// [`Self::set_sync_presence`], which only changes the presence value used + /// by future generated sync requests. + pub async fn set_presence( + &self, + presence: PresenceState, + status_msg: Option, + ) -> Result<()> { + 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 +2827,9 @@ 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 + /// sync request. If this is not set, the request uses + /// [`Client::sync_presence`]. /// /// # Examples /// @@ -2822,7 +2867,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 +2900,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 +3286,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 +3710,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 +3732,86 @@ 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_sync_presence(PresenceState::Unavailable); + + 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_sync_presence(PresenceState::Offline); + + 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; + + client.set_sync_presence(PresenceState::Unavailable); + + server.mock_sync().set_presence("unavailable").ok(|_| {}).expect(1).mount().await; + + client.sync_once(SyncSettings::new()).await.expect("sync should succeed"); + + server.mock_sync().set_presence("offline").ok(|_| {}).expect(1).mount().await; + + client + .sync_once(SyncSettings::new().set_presence(PresenceState::Offline)) + .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": "unavailable", + "status_msg": "Away" + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) + .expect(1) + .mount(server.server()) + .await; + + client + .set_presence(PresenceState::Unavailable, Some("Away".to_owned())) + .await + .expect("presence update should succeed"); + } + + #[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).await, + Err(Error::AuthenticationRequired) + ); + } + #[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..fb2d4a7737f 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,7 +182,10 @@ impl SyncSettings { self } - /// Set the presence state + /// Set the presence state for this sync request. + /// + /// If this is not set, sync requests use the client-owned sync presence + /// value. The client default is [`PresenceState::Online`]. /// /// `PresenceState::Online` - The client is marked as being online. This is /// the default preset. @@ -196,7 +199,7 @@ impl SyncSettings { /// the client. #[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/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index ba22e86af5a..6942d3cccec 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -29,7 +29,7 @@ pub struct SlidingSyncBuilder { lists: Vec, extensions: Option, room_subscriptions: BTreeMap, - presence: PresenceState, + presence: Option, poll_timeout: Duration, network_timeout: Duration, #[cfg(feature = "e2e-encryption")] @@ -52,7 +52,7 @@ impl SlidingSyncBuilder { lists: Vec::new(), extensions: None, room_subscriptions: BTreeMap::new(), - presence: PresenceState::Online, + presence: None, poll_timeout: Duration::from_secs(30), network_timeout: Duration::from_secs(30), #[cfg(feature = "e2e-encryption")] @@ -92,10 +92,10 @@ impl SlidingSyncBuilder { /// Set the presence state that will be sent with sliding sync requests. /// - /// The default is [`PresenceState::Online`], matching the Matrix - /// Client-Server API default when `set_presence` is not specified. + /// If this is not set, sliding sync requests use the client-owned sync + /// presence value. pub fn set_presence(mut self, presence: PresenceState) -> Self { - self.presence = presence; + self.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 d28a9633b82..d2ea22d2a95 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -114,8 +114,8 @@ pub(super) struct SlidingSyncInner { /// calls. extensions: http::request::Extensions, - /// The presence state sent with sliding sync requests. - presence: StdRwLock, + /// Optional presence state override sent with sliding sync requests. + presence: StdRwLock>, /// Internal channel used to pass messages between Sliding Sync and other /// types. @@ -138,14 +138,27 @@ impl SlidingSync { /// Set the presence state to send with future sliding sync requests. /// - /// The default is [`PresenceState::Online`], matching the Matrix - /// Client-Server API default when `set_presence` is not specified. + /// If this is not set, sliding sync requests use the client-owned sync + /// presence value. pub fn set_presence(&self, presence: PresenceState) { - *self.inner.presence.write().unwrap() = presence; + *self.inner.presence.write().unwrap() = Some(presence); + } + + /// Clear the explicit presence override for future sliding sync requests. + /// + /// After calling this, generated sliding sync requests use the client-owned + /// sync presence value again. + pub fn clear_presence_override(&self) { + *self.inner.presence.write().unwrap() = None; } fn presence(&self) -> PresenceState { - self.inner.presence.read().unwrap().clone() + self.inner + .presence + .read() + .unwrap() + .clone() + .unwrap_or_else(|| self.inner.client.sync_presence()) } /// Add subscriptions to many rooms. @@ -1036,6 +1049,7 @@ mod tests { #[async_test] async fn test_sliding_sync_request_uses_configured_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?; @@ -1043,7 +1057,23 @@ mod tests { assert_eq!(request.set_presence, PresenceState::Online); } - sliding_sync.set_presence(PresenceState::Unavailable); + client.set_sync_presence(PresenceState::Unavailable); + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Unavailable); + } + + sliding_sync.set_presence(PresenceState::Offline); + + { + let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; + + assert_eq!(request.set_presence, PresenceState::Offline); + } + + sliding_sync.clear_presence_override(); { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; @@ -1052,6 +1082,7 @@ mod tests { } let client = logged_in_client(None).await; + client.set_sync_presence(PresenceState::Unavailable); let sliding_sync = client.sliding_sync("presence")?.set_presence(PresenceState::Offline).build().await?; diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index dd835bb07e9..7223acb8059 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -2813,6 +2813,12 @@ 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 + } + /// Mocks the sync endpoint, using the given function to generate the /// response. pub fn ok(self, func: F) -> MatrixMock<'a> { From 37f52c285a18779cb7c59dfd20b8c41a17c93f2c Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:17:08 +1000 Subject: [PATCH 4/9] fix: use client sync presence for sliding sync Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- crates/matrix-sdk/changelog.d/6672.added.md | 2 +- crates/matrix-sdk/src/client/mod.rs | 6 +-- crates/matrix-sdk/src/sliding_sync/builder.rs | 14 +---- crates/matrix-sdk/src/sliding_sync/mod.rs | 54 ++----------------- 4 files changed, 8 insertions(+), 68 deletions(-) diff --git a/crates/matrix-sdk/changelog.d/6672.added.md b/crates/matrix-sdk/changelog.d/6672.added.md index d559af2a918..550fd2a7351 100644 --- a/crates/matrix-sdk/changelog.d/6672.added.md +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -1,3 +1,3 @@ Add client-owned sync presence configuration and an explicit presence update API. Generated sync requests use the client sync presence by default, while -classic sync settings and low-level sliding sync instances can still override it. +classic sync settings can still override it per request. diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index dac91cb512f..d59e8f820cc 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -692,9 +692,9 @@ impl Client { /// Set the default presence state to send with generated sync requests. /// /// This affects future sync requests that don't configure an explicit - /// per-request or per-instance presence override. It doesn't affect - /// requests that are already in flight, and it doesn't send an immediate - /// presence update to the homeserver. + /// per-request presence override. It doesn't affect requests that are + /// already in flight, and it doesn't send an immediate presence update to + /// the homeserver. pub fn set_sync_presence(&self, presence: PresenceState) { *self.inner.sync_presence.write().unwrap() = presence; } diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 6942d3cccec..00f237605d3 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -7,7 +7,7 @@ use std::{ use cfg_if::cfg_if; use matrix_sdk_common::timer; -use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http, presence::PresenceState}; +use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http}; use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, broadcast::channel}; use super::{ @@ -29,7 +29,6 @@ pub struct SlidingSyncBuilder { lists: Vec, extensions: Option, room_subscriptions: BTreeMap, - presence: Option, poll_timeout: Duration, network_timeout: Duration, #[cfg(feature = "e2e-encryption")] @@ -52,7 +51,6 @@ impl SlidingSyncBuilder { lists: Vec::new(), extensions: None, room_subscriptions: BTreeMap::new(), - presence: None, poll_timeout: Duration::from_secs(30), network_timeout: Duration::from_secs(30), #[cfg(feature = "e2e-encryption")] @@ -90,15 +88,6 @@ impl SlidingSyncBuilder { Ok(self.add_list(list)) } - /// Set the presence state that will be sent with sliding sync requests. - /// - /// If this is not set, sliding sync requests use the client-owned sync - /// presence value. - pub fn set_presence(mut self, presence: PresenceState) -> Self { - self.presence = Some(presence); - self - } - /// Activate e2ee, to-device-message, account data, typing and receipt /// extensions if not yet configured. /// @@ -301,7 +290,6 @@ impl SlidingSyncBuilder { room_subscriptions: StdRwLock::new(self.room_subscriptions), extensions: self.extensions.unwrap_or_default(), - presence: StdRwLock::new(self.presence), internal_channel: internal_channel_sender, diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index d2ea22d2a95..2a6be97863d 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -40,7 +40,6 @@ use ruma::{ OwnedRoomId, RoomId, api::{client::sync::sync_events::v5 as http, error::ErrorKind}, assign, - presence::PresenceState, }; use tokio::{ select, @@ -114,9 +113,6 @@ pub(super) struct SlidingSyncInner { /// calls. extensions: http::request::Extensions, - /// Optional presence state override sent with sliding sync requests. - presence: StdRwLock>, - /// Internal channel used to pass messages between Sliding Sync and other /// types. internal_channel: Sender, @@ -136,31 +132,6 @@ impl SlidingSync { SlidingSyncBuilder::new(id, client) } - /// Set the presence state to send with future sliding sync requests. - /// - /// If this is not set, sliding sync requests use the client-owned sync - /// presence value. - pub fn set_presence(&self, presence: PresenceState) { - *self.inner.presence.write().unwrap() = Some(presence); - } - - /// Clear the explicit presence override for future sliding sync requests. - /// - /// After calling this, generated sliding sync requests use the client-owned - /// sync presence value again. - pub fn clear_presence_override(&self) { - *self.inner.presence.write().unwrap() = None; - } - - fn presence(&self) -> PresenceState { - self.inner - .presence - .read() - .unwrap() - .clone() - .unwrap_or_else(|| self.inner.client.sync_presence()) - } - /// Add subscriptions to many rooms. /// /// If the associated `Room`s exist, they will be marked as members are @@ -535,7 +506,7 @@ impl SlidingSync { let mut request = assign!(http::Request::new(), { conn_id: Some(self.inner.id.clone()), pos, - set_presence: self.presence(), + set_presence: self.inner.client.sync_presence(), timeout, lists: requests_lists, }); @@ -1047,7 +1018,7 @@ mod tests { } #[async_test] - async fn test_sliding_sync_request_uses_configured_presence() -> Result<()> { + 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(); @@ -1065,26 +1036,7 @@ mod tests { assert_eq!(request.set_presence, PresenceState::Unavailable); } - sliding_sync.set_presence(PresenceState::Offline); - - { - let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - - assert_eq!(request.set_presence, PresenceState::Offline); - } - - sliding_sync.clear_presence_override(); - - { - let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - - assert_eq!(request.set_presence, PresenceState::Unavailable); - } - - let client = logged_in_client(None).await; - client.set_sync_presence(PresenceState::Unavailable); - let sliding_sync = - client.sliding_sync("presence")?.set_presence(PresenceState::Offline).build().await?; + client.set_sync_presence(PresenceState::Offline); { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; From fbf1f42b18c273702a281007232dbcd14d2729c7 Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Sat, 20 Jun 2026 00:52:16 +1000 Subject: [PATCH 5/9] refactor: add immediate presence flag Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../matrix-sdk-ffi/changelog.d/6672.added.md | 2 +- bindings/matrix-sdk-ffi/src/client.rs | 24 +++----- .../tests/integration/notification_client.rs | 4 +- .../tests/integration/sync_service.rs | 2 +- crates/matrix-sdk/changelog.d/6672.added.md | 6 +- crates/matrix-sdk/src/client/mod.rs | 61 +++++++++++++------ crates/matrix-sdk/src/sliding_sync/mod.rs | 4 +- 7 files changed, 60 insertions(+), 43 deletions(-) diff --git a/bindings/matrix-sdk-ffi/changelog.d/6672.added.md b/bindings/matrix-sdk-ffi/changelog.d/6672.added.md index 9b3fb7598aa..9cee09cafc0 100644 --- a/bindings/matrix-sdk-ffi/changelog.d/6672.added.md +++ b/bindings/matrix-sdk-ffi/changelog.d/6672.added.md @@ -1 +1 @@ -Expose client-level sync presence configuration and explicit presence updates. +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 f229b6a3769..7a8582e87be 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1168,21 +1168,17 @@ impl Client { self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect() } - /// Set the default presence state used by future generated sync requests. + /// Set the presence state for the current user. /// - /// This does not send an immediate presence update to the homeserver. - pub fn set_sync_presence(&self, presence: PresenceState) { - self.inner.set_sync_presence(presence.into()); - } - - /// Get the default presence state used by generated sync requests. - pub fn sync_presence(&self) -> PresenceState { - self.inner.sync_presence().into() - } - - /// Send an immediate presence update for the current user. - pub async fn set_presence(&self, presence: PresenceState) -> Result<(), ClientError> { - Ok(self.inner.set_presence(presence.into(), None).await?) + /// This updates the presence state used by future generated sync requests, + /// regardless of `immediate`. 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. diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index c41d832621e..0fa72336973 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -477,7 +477,7 @@ async fn test_notification_client_sliding_sync_uses_client_sync_presence() { let process_setup = NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service }; let notification_client = NotificationClient::new(client.clone(), process_setup).await.unwrap(); - client.set_sync_presence(PresenceState::Unavailable); + client.set_presence(PresenceState::Unavailable, None, false).await.unwrap(); let mut result = notification_client .get_notifications_with_sliding_sync(&[NotificationItemsRequest { room_id: room_id.to_owned(), @@ -664,7 +664,7 @@ async fn test_notification_client_encryption_fallback_uses_client_sync_presence( NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses) .await .unwrap(); - client.set_sync_presence(PresenceState::Unavailable); + client.set_presence(PresenceState::Unavailable, None, false).await.unwrap(); let _ = notification_client .get_notifications_with_sliding_sync(&[NotificationItemsRequest { diff --git a/crates/matrix-sdk-ui/tests/integration/sync_service.rs b/crates/matrix-sdk-ui/tests/integration/sync_service.rs index 7e0af921e22..84f988a9b61 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -211,7 +211,7 @@ async fn test_sync_service_state() -> anyhow::Result<()> { 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; - client.set_sync_presence(PresenceState::Unavailable); + client.set_presence(PresenceState::Unavailable, None, false).await?; let encryption_pos = Arc::new(Mutex::new(0)); let room_pos = Arc::new(Mutex::new(0)); diff --git a/crates/matrix-sdk/changelog.d/6672.added.md b/crates/matrix-sdk/changelog.d/6672.added.md index 550fd2a7351..c73a817e80b 100644 --- a/crates/matrix-sdk/changelog.d/6672.added.md +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -1,3 +1,3 @@ -Add client-owned sync presence configuration and an explicit presence update -API. Generated sync requests use the client sync presence by default, while -classic sync settings can still override it per request. +Add client-owned presence configuration that can optionally send an immediate +presence update. Generated sync requests use the client presence by default, +while classic sync settings can still override it per request. diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index d59e8f820cc..0ea5d99065d 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -689,18 +689,8 @@ impl Client { *lock = version; } - /// Set the default presence state to send with generated sync requests. - /// - /// This affects future sync requests that don't configure an explicit - /// per-request presence override. It doesn't affect requests that are - /// already in flight, and it doesn't send an immediate presence update to - /// the homeserver. - pub fn set_sync_presence(&self, presence: PresenceState) { - *self.inner.sync_presence.write().unwrap() = presence; - } - /// Get the default presence state used by generated sync requests. - pub fn sync_presence(&self) -> PresenceState { + pub(crate) fn sync_presence(&self) -> PresenceState { self.inner.sync_presence.read().unwrap().clone() } @@ -759,16 +749,24 @@ impl Client { self.auth_ctx().access_token() } - /// Send an immediate presence update for the current user. + /// Set the presence state for the current user. /// - /// This calls the Matrix presence endpoint directly. It is separate from - /// [`Self::set_sync_presence`], which only changes the presence value used - /// by future generated sync requests. + /// The presence state is stored as the default used by future generated + /// sync requests, regardless of `immediate`. 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; @@ -3743,13 +3741,19 @@ pub(crate) mod tests { assert_eq!(clone.sync_presence(), PresenceState::Online); assert_eq!(notification_client.sync_presence(), PresenceState::Online); - client.set_sync_presence(PresenceState::Unavailable); + 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_sync_presence(PresenceState::Offline); + 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); @@ -3761,7 +3765,10 @@ pub(crate) mod tests { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; - client.set_sync_presence(PresenceState::Unavailable); + client + .set_presence(PresenceState::Unavailable, None, false) + .await + .expect("presence should update"); server.mock_sync().set_presence("unavailable").ok(|_| {}).expect(1).mount().await; @@ -3797,9 +3804,11 @@ pub(crate) mod tests { .await; client - .set_presence(PresenceState::Unavailable, Some("Away".to_owned())) + .set_presence(PresenceState::Unavailable, Some("Away".to_owned()), true) .await .expect("presence update should succeed"); + + assert_eq!(client.sync_presence(), PresenceState::Unavailable); } #[async_test] @@ -3807,11 +3816,23 @@ pub(crate) mod tests { let client = MockClientBuilder::new(None).unlogged().build().await; assert_matches!( - client.set_presence(PresenceState::Unavailable, None).await, + 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::Unavailable, None, false) + .await + .expect("presence should update"); + + assert_eq!(client.sync_presence(), PresenceState::Unavailable); + } + #[async_test] async fn test_account_data() { let server = MatrixMockServer::new().await; diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 2a6be97863d..5f32b2e43e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1028,7 +1028,7 @@ mod tests { assert_eq!(request.set_presence, PresenceState::Online); } - client.set_sync_presence(PresenceState::Unavailable); + client.set_presence(PresenceState::Unavailable, None, false).await?; { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; @@ -1036,7 +1036,7 @@ mod tests { assert_eq!(request.set_presence, PresenceState::Unavailable); } - client.set_sync_presence(PresenceState::Offline); + client.set_presence(PresenceState::Offline, None, false).await?; { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; From ff7175707521f019d7ae83bc298995f9886413b6 Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:09:19 +1000 Subject: [PATCH 6/9] fix: avoid private sync presence doc link Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- crates/matrix-sdk/src/client/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 0ea5d99065d..5a03ff0b761 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2826,8 +2826,8 @@ impl Client { /// * [`full_state`] - To tell the server that we wish to receive all /// state events, regardless of our configured [`token`]. /// * [`set_presence`] - To override the presence state sent with this - /// sync request. If this is not set, the request uses - /// [`Client::sync_presence`]. + /// sync request. If this is not set, the request uses the presence + /// configured with [`Client::set_presence`]. /// /// # Examples /// From 54d9ab899e9eee733ce50fc1946ed6b2522a6b74 Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:26:06 +1000 Subject: [PATCH 7/9] refactor: address review comments Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- bindings/matrix-sdk-ffi/src/client.rs | 5 +- bindings/matrix-sdk-ffi/src/ruma.rs | 23 +-- .../integration/encryption_sync_service.rs | 23 ++- .../tests/integration/notification_client.rs | 177 +----------------- .../tests/integration/sliding_sync.rs | 41 ++++ .../tests/integration/sync_service.rs | 45 ++--- crates/matrix-sdk/changelog.d/6672.added.md | 3 +- crates/matrix-sdk/src/client/builder/mod.rs | 2 +- crates/matrix-sdk/src/client/mod.rs | 82 +++++--- crates/matrix-sdk/src/config/sync.rs | 17 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 6 +- 11 files changed, 168 insertions(+), 256 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 7a8582e87be..d7f569ada18 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1171,8 +1171,9 @@ impl Client { /// Set the presence state for the current user. /// /// This updates the presence state used by future generated sync requests, - /// regardless of `immediate`. If `immediate` is `true`, it also sends an - /// immediate presence update to the homeserver. + /// 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, diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index f81490b9f18..4aed2080560 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -194,9 +194,9 @@ impl From<&RumaMatrixId> for MatrixId { #[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum, Default)] pub enum PresenceState { - #[default] Online, Offline, + #[default] Unavailable, } @@ -221,27 +221,6 @@ impl From for PresenceState { } } -#[cfg(test)] -mod tests { - use ruma::presence::PresenceState as RumaPresenceState; - - use super::PresenceState; - - #[test] - fn presence_state_conversions() { - let cases = [ - (PresenceState::Online, RumaPresenceState::Online), - (PresenceState::Offline, RumaPresenceState::Offline), - (PresenceState::Unavailable, RumaPresenceState::Unavailable), - ]; - - for (ffi_presence, ruma_presence) in cases { - assert_eq!(RumaPresenceState::from(ffi_presence.clone()), ruma_presence); - assert_eq!(PresenceState::from(ruma_presence), ffi_presence); - } - } -} - #[matrix_sdk_ffi_macros::export] pub fn message_event_content_new( msgtype: MessageType, 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..7ba89446a77 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_unavailable() -> 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, "unavailable", &["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 0fa72336973..84207c06c2a 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -21,61 +21,19 @@ use ruma::{ RoomVersionId, api::client::sync::sync_events::v5::{Response, response}, assign, event_id, - events::{ - Mentions, TimelineEventType, - room::{ - encrypted::{ - EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent, - }, - member::MembershipState, - }, - }, - mxc_uri, owned_device_id, owned_user_id, - presence::PresenceState, - room_id, uint, user_id, + events::{Mentions, TimelineEventType, room::member::MembershipState}, + mxc_uri, owned_user_id, room_id, uint, user_id, }; use serde_json::json; use wiremock::{ - Match as _, Mock, Request, ResponseTemplate, + Mock, Request, ResponseTemplate, matchers::{header, method, path}, }; -use crate::sliding_sync::{PartialSlidingSyncRequest, SlidingSyncMatcher, check_requests}; - -async fn assert_sliding_sync_presence_for_conn_ids( - server: &MatrixMockServer, - expected_presence: &str, - expected_conn_ids: &[&str], -) { - let expected_conn_ids = - expected_conn_ids.iter().map(|conn_id| (*conn_id).to_owned()).collect::>(); - 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 { - continue; - }; - if !expected_conn_ids.contains(conn_id) { - continue; - } - - 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(), Some(expected_presence), "conn_id: {conn_id}"); - } - - assert_eq!(seen_conn_ids, expected_conn_ids); -} +use crate::sliding_sync::{ + PartialSlidingSyncRequest, SlidingSyncMatcher, assert_sliding_sync_presence_for_conn_ids, + check_requests, +}; #[async_test] async fn test_notification_client_with_context() { @@ -375,7 +333,7 @@ async fn test_unsubscribed_threads_get_notifications() { } #[async_test] -async fn test_notification_client_sliding_sync_uses_client_sync_presence() { +async fn test_notification_client_sliding_sync() { let room_id = room_id!("!a98sd12bjh:example.org"); let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -476,8 +434,7 @@ async fn test_notification_client_sliding_sync_uses_client_sync_presence() { let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); let process_setup = NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service }; - let notification_client = NotificationClient::new(client.clone(), process_setup).await.unwrap(); - client.set_presence(PresenceState::Unavailable, None, false).await.unwrap(); + let notification_client = NotificationClient::new(client, process_setup).await.unwrap(); let mut result = notification_client .get_notifications_with_sliding_sync(&[NotificationItemsRequest { room_id: room_id.to_owned(), @@ -565,122 +522,6 @@ async fn test_notification_client_sliding_sync_uses_client_sync_presence() { assert_eq!(item.is_noisy, Some(false)); } -#[async_test] -async fn test_notification_client_encryption_fallback_uses_client_sync_presence() { - let room_id = room_id!("!a98sd12bjh:example.org"); - let server = MatrixMockServer::new().await; - let client = server.client_builder().build().await; - - let event_id = event_id!("$example_event_id"); - let sender = user_id!("@user:example.org"); - let my_user_id = client.user_id().unwrap().to_owned(); - - let event_factory = EventFactory::new().room(room_id); - let own_member_event = event_factory - .member(&my_user_id) - .display_name("My self") - .membership(MembershipState::Join) - .into_raw_sync(); - let sender_member_event = - event_factory.member(sender).membership(MembershipState::Join).into_raw_sync(); - let power_levels_event = - event_factory.power_levels(&mut BTreeMap::new()).sender(sender).into_raw_sync(); - let room_encryption_event = event_factory.room_encryption().sender(sender).into_raw_sync(); - let encrypted_event = event_factory - .sender(sender) - .event(RoomEncryptedEventContent::new( - EncryptedEventScheme::MegolmV1AesSha2( - MegolmV1AesSha2ContentInit { - ciphertext: String::from( - "AwgAEpABhetEzzZzyYrxtEVUtlJnZtJcURBlQUQJ9irVeklCTs06LwgTMQj61PMUS4Vy\ - YOX+PD67+hhU40/8olOww+Ud0m2afjMjC3wFX+4fFfSkoWPVHEmRVucfcdSF1RSB4EmK\ - PIP4eo1X6x8kCIMewBvxl2sI9j4VNvDvAN7M3zkLJfFLOFHbBviI4FN7hSFHFeM739Zg\ - iwxEs3hIkUXEiAfrobzaMEM/zY7SDrTdyffZndgJo7CZOVhoV6vuaOhmAy4X2t4UnbuV\ - JGJjKfV57NAhp8W+9oT7ugwO", - ), - device_id: owned_device_id!("KIUVQQSDTM"), - sender_key: String::from("LvryVyoCjdONdBCi2vvoSbI34yTOx7YrCFACUEKoXnc"), - session_id: String::from("64H7XKokIx0ASkYDHZKlT5zd/Zccz/cQspPNdvnNULA"), - } - .into(), - ), - None, - )) - .event_id(event_id) - .into_raw_sync(); - - let notification_pos = Mutex::new(0); - let encryption_pos = Mutex::new(0); - Mock::given(SlidingSyncMatcher) - .respond_with(move |request: &Request| { - let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap(); - - match partial_request.conn_id.as_deref() { - Some("notifications") => { - let mut pos = notification_pos.lock().unwrap(); - *pos += 1; - let pos_as_str = (*pos).to_string(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": partial_request.txn_id, - "pos": pos_as_str, - "rooms": { - room_id: { - "name": "The Maltese Falcon", - "initial": true, - "required_state": [ - sender_member_event.clone(), - own_member_event.clone(), - power_levels_event.clone(), - room_encryption_event.clone(), - ], - "timeline": [ - encrypted_event.clone(), - ] - } - }, - "extensions": { - "account_data": {} - } - })) - } - Some("encryption") => { - let mut pos = encryption_pos.lock().unwrap(); - *pos += 1; - let pos_as_str = (*pos).to_string(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": partial_request.txn_id, - "pos": pos_as_str - })) - } - other => panic!("unexpected conn id {other:?}"), - } - }) - .mount(server.server()) - .await; - - let notification_client = - NotificationClient::new(client.clone(), NotificationProcessSetup::MultipleProcesses) - .await - .unwrap(); - client.set_presence(PresenceState::Unavailable, None, false).await.unwrap(); - - let _ = notification_client - .get_notifications_with_sliding_sync(&[NotificationItemsRequest { - room_id: room_id.to_owned(), - event_ids: vec![event_id.to_owned()], - }]) - .await; - - assert_sliding_sync_presence_for_conn_ids( - &server, - "unavailable", - &["notifications", "encryption"], - ) - .await; -} - #[async_test] async fn test_notification_client_sliding_sync_invites() { let room_id = room_id!("!a98sd12bjh:example.org"); diff --git a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs index 00c006a46b9..cdac74c4577 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: &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(), Some(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 84f988a9b61..4cfa0cb4d2e 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -13,7 +13,6 @@ // limitations under the License. use std::{ - collections::BTreeSet, sync::{Arc, Mutex}, time::Duration, }; @@ -27,7 +26,9 @@ 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. @@ -211,7 +212,6 @@ async fn test_sync_service_state() -> anyhow::Result<()> { 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; - client.set_presence(PresenceState::Unavailable, None, false).await?; let encryption_pos = Arc::new(Mutex::new(0)); let room_pos = Arc::new(Mutex::new(0)); @@ -224,35 +224,26 @@ async fn test_sync_service_client_sync_presence_is_used_by_both_syncs() -> anyho sync_service.stop().await; - let mut conn_ids_with_presence = BTreeSet::new(); - for request in &server.received_requests().await.expect("Request recording has been disabled") { - if !SlidingSyncMatcher.matches(request) { - continue; - } + assert_sliding_sync_presence_for_conn_ids(&server, "unavailable", &["encryption", "room-list"]) + .await; - 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 { - continue; - }; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + client.set_presence(PresenceState::Offline, None, false).await?; - if conn_id != "encryption" && conn_id != "room-list" { - panic!("unexpected conn id seen server side: {conn_id}"); - } + 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 set_presence = request - .url - .query_pairs() - .find_map(|(key, value)| (key == "set_presence").then_some(value.into_owned())); + let sync_service = SyncService::builder(client).build().await.unwrap(); + sync_service.start().await; - if set_presence.as_deref() == Some("unavailable") { - conn_ids_with_presence.insert(conn_id.to_owned()); - } - } + tokio::time::sleep(Duration::from_millis(150)).await; - assert_eq!( - conn_ids_with_presence, - BTreeSet::from(["encryption".to_owned(), "room-list".to_owned()]) - ); + sync_service.stop().await; + + assert_sliding_sync_presence_for_conn_ids(&server, "offline", &["encryption", "room-list"]) + .await; Ok(()) } diff --git a/crates/matrix-sdk/changelog.d/6672.added.md b/crates/matrix-sdk/changelog.d/6672.added.md index c73a817e80b..9a01ce26dc2 100644 --- a/crates/matrix-sdk/changelog.d/6672.added.md +++ b/crates/matrix-sdk/changelog.d/6672.added.md @@ -1,3 +1,4 @@ Add client-owned presence configuration that can optionally send an immediate presence update. Generated sync requests use the client presence by default, -while classic sync settings can still override it per request. +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 db55b3236ee..dcff68372d0 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -663,7 +663,7 @@ impl ClientBuilder { server, homeserver, sliding_sync_version, - Arc::new(StdRwLock::new(PresenceState::Online)), + Arc::new(StdRwLock::new(PresenceState::Unavailable)), 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 5a03ff0b761..d674aa7213e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -752,9 +752,10 @@ impl Client { /// 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`. If `immediate` is `true`, this - /// also calls the Matrix presence endpoint directly. `status_msg` is only - /// sent when `immediate` is `true`. + /// sync requests, regardless of `immediate`. The initial default is + /// [`PresenceState::Unavailable`]. 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, @@ -2826,8 +2827,9 @@ impl Client { /// * [`full_state`] - To tell the server that we wish to receive all /// state events, regardless of our configured [`token`]. /// * [`set_presence`] - To override the presence state sent with this - /// sync request. If this is not set, the request uses the presence - /// configured with [`Client::set_presence`]. + /// 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::Unavailable`]. /// /// # Examples /// @@ -3737,18 +3739,18 @@ pub(crate) mod tests { 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); + assert_eq!(client.sync_presence(), PresenceState::Unavailable); + assert_eq!(clone.sync_presence(), PresenceState::Unavailable); + assert_eq!(notification_client.sync_presence(), PresenceState::Unavailable); client - .set_presence(PresenceState::Unavailable, None, false) + .set_presence(PresenceState::Online, 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); + assert_eq!(client.sync_presence(), PresenceState::Online); + assert_eq!(clone.sync_presence(), PresenceState::Online); + assert_eq!(notification_client.sync_presence(), PresenceState::Online); notification_client .set_presence(PresenceState::Offline, None, false) @@ -3765,21 +3767,49 @@ pub(crate) mod tests { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; + { + let _sync_guard = server + .mock_sync() + .set_presence("unavailable") + .ok(|_| {}) + .expect(1) + .mount_as_scoped() + .await; + + client.sync_once(SyncSettings::new()).await.expect("sync should succeed"); + } + client - .set_presence(PresenceState::Unavailable, None, false) + .set_presence(PresenceState::Offline, None, false) .await .expect("presence should update"); - server.mock_sync().set_presence("unavailable").ok(|_| {}).expect(1).mount().await; + { + 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"); + client.sync_once(SyncSettings::new()).await.expect("sync should succeed"); + } - server.mock_sync().set_presence("offline").ok(|_| {}).expect(1).mount().await; + { + let _sync_guard = server + .mock_sync() + .set_presence("unavailable") + .ok(|_| {}) + .expect(1) + .mount_as_scoped() + .await; - client - .sync_once(SyncSettings::new().set_presence(PresenceState::Offline)) - .await - .expect("sync should succeed"); + client + .sync_once(SyncSettings::new().set_presence(PresenceState::Unavailable)) + .await + .expect("sync should succeed"); + } } #[async_test] @@ -3795,8 +3825,8 @@ pub(crate) mod tests { Mock::given(method("PUT")) .and(path_regex(r"^/_matrix/client/(r0|v3)/presence/.*/status$")) .and(body_partial_json(json!({ - "presence": "unavailable", - "status_msg": "Away" + "presence": "online", + "status_msg": "Here" }))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) .expect(1) @@ -3804,11 +3834,11 @@ pub(crate) mod tests { .await; client - .set_presence(PresenceState::Unavailable, Some("Away".to_owned()), true) + .set_presence(PresenceState::Online, Some("Here".to_owned()), true) .await .expect("presence update should succeed"); - assert_eq!(client.sync_presence(), PresenceState::Unavailable); + assert_eq!(client.sync_presence(), PresenceState::Online); } #[async_test] @@ -3826,11 +3856,11 @@ pub(crate) mod tests { let client = MockClientBuilder::new(None).unlogged().build().await; client - .set_presence(PresenceState::Unavailable, None, false) + .set_presence(PresenceState::Offline, None, false) .await .expect("presence should update"); - assert_eq!(client.sync_presence(), PresenceState::Unavailable); + assert_eq!(client.sync_presence(), PresenceState::Offline); } #[async_test] diff --git a/crates/matrix-sdk/src/config/sync.rs b/crates/matrix-sdk/src/config/sync.rs index fb2d4a7737f..3b1b49c7ca8 100644 --- a/crates/matrix-sdk/src/config/sync.rs +++ b/crates/matrix-sdk/src/config/sync.rs @@ -182,21 +182,28 @@ impl SyncSettings { self } - /// Set the presence state for this sync request. + /// Override the presence state for this classic `/sync` request. /// - /// If this is not set, sync requests use the client-owned sync presence - /// value. The client default is [`PresenceState::Online`]. + /// If this is not set, the request uses the client-owned sync presence + /// value configured with [`Client::set_presence`]. The client default is + /// [`PresenceState::Unavailable`]. /// /// `PresenceState::Online` - The client is marked as being online. This is - /// the default preset. + /// the active preset. /// /// `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 default 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 = Some(presence); diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 5f32b2e43e4..8d37cb88704 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1025,15 +1025,15 @@ mod tests { { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - assert_eq!(request.set_presence, PresenceState::Online); + assert_eq!(request.set_presence, PresenceState::Unavailable); } - client.set_presence(PresenceState::Unavailable, None, false).await?; + client.set_presence(PresenceState::Online, None, false).await?; { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - assert_eq!(request.set_presence, PresenceState::Unavailable); + assert_eq!(request.set_presence, PresenceState::Online); } client.set_presence(PresenceState::Offline, None, false).await?; From 8410f56e2789f8a641ae5fcc3cea0954b8bcd699 Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:35:56 +1000 Subject: [PATCH 8/9] chore: format Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- crates/matrix-sdk/src/client/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index d674aa7213e..9d1178553e4 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2828,8 +2828,9 @@ impl Client { /// state events, regardless of our configured [`token`]. /// * [`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::Unavailable`]. + /// client-owned sync presence configured with + /// [`Client::set_presence`], which defaults to + /// [`PresenceState::Unavailable`]. /// /// # Examples /// From 4caa36368c87e7a78b8a0b7b8cea8ad1978725ae Mon Sep 17 00:00:00 2001 From: Jared L <48422312+lhjt@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:30:09 +1000 Subject: [PATCH 9/9] refactor: change default presence Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../integration/encryption_sync_service.rs | 4 ++-- .../tests/integration/notification_client.rs | 2 +- .../tests/integration/sliding_sync.rs | 4 ++-- .../tests/integration/sync_service.rs | 11 ++++++---- crates/matrix-sdk/src/client/builder/mod.rs | 2 +- crates/matrix-sdk/src/client/mod.rs | 20 +++++++++---------- crates/matrix-sdk/src/config/sync.rs | 6 +++--- crates/matrix-sdk/src/sliding_sync/mod.rs | 6 +++--- crates/matrix-sdk/src/test_utils/mocks/mod.rs | 6 ++++++ 9 files changed, 35 insertions(+), 26 deletions(-) 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 7ba89446a77..c33cf4ffaab 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -174,7 +174,7 @@ async fn setup_mocking_sliding_sync_server(server: &MockServer) -> MockGuard { } #[async_test] -async fn test_encryption_sync_default_sync_presence_is_unavailable() -> anyhow::Result<()> { +async fn test_encryption_sync_default_sync_presence_is_online() -> anyhow::Result<()> { let server = MatrixMockServer::new().await; let client = server.client_builder().build().await; @@ -186,7 +186,7 @@ async fn test_encryption_sync_default_sync_presence_is_unavailable() -> anyhow:: encryption_sync.run_fixed_iterations(1, sync_permit_guard).await?; - assert_sliding_sync_presence_for_conn_ids(&server, "unavailable", &["encryption"]).await; + assert_sliding_sync_presence_for_conn_ids(&server, None, &["encryption"]).await; Ok(()) } diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index 84207c06c2a..a157884a624 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -497,7 +497,7 @@ async fn test_notification_client_sliding_sync() { })], ) .await; - assert_sliding_sync_presence_for_conn_ids(&server, "unavailable", &["notifications"]).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 cdac74c4577..20783d73725 100644 --- a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs @@ -172,7 +172,7 @@ macro_rules! sliding_sync_then_assert_request_and_fake_response { pub(crate) async fn assert_sliding_sync_presence_for_conn_ids( server: &MockServer, - expected_presence: &str, + expected_presence: Option<&str>, expected_conn_ids: &[&str], ) { let num_expected_conn_ids = expected_conn_ids.len(); @@ -203,7 +203,7 @@ pub(crate) async fn assert_sliding_sync_presence_for_conn_ids( .query_pairs() .find_map(|(key, value)| (key == "set_presence").then_some(value.into_owned())); - assert_eq!(set_presence.as_deref(), Some(expected_presence), "conn_id: {conn_id}"); + 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 4cfa0cb4d2e..f6a8fc9dd7e 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -224,8 +224,7 @@ async fn test_sync_service_client_sync_presence_is_used_by_both_syncs() -> anyho sync_service.stop().await; - assert_sliding_sync_presence_for_conn_ids(&server, "unavailable", &["encryption", "room-list"]) - .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; @@ -242,8 +241,12 @@ async fn test_sync_service_client_sync_presence_is_used_by_both_syncs() -> anyho sync_service.stop().await; - assert_sliding_sync_presence_for_conn_ids(&server, "offline", &["encryption", "room-list"]) - .await; + assert_sliding_sync_presence_for_conn_ids( + &server, + Some("offline"), + &["encryption", "room-list"], + ) + .await; Ok(()) } diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index dcff68372d0..db55b3236ee 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -663,7 +663,7 @@ impl ClientBuilder { server, homeserver, sliding_sync_version, - Arc::new(StdRwLock::new(PresenceState::Unavailable)), + 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 9d1178553e4..180eb28eb1b 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -753,7 +753,7 @@ impl Client { /// /// The presence state is stored as the default used by future generated /// sync requests, regardless of `immediate`. The initial default is - /// [`PresenceState::Unavailable`]. If `immediate` is `true`, this also + /// [`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( @@ -2830,7 +2830,7 @@ impl Client { /// 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::Unavailable`]. + /// [`PresenceState::Online`]. /// /// # Examples /// @@ -3740,18 +3740,18 @@ pub(crate) mod tests { let notification_client = client.notification_client(CrossProcessLockConfig::SingleProcess).await.unwrap(); - assert_eq!(client.sync_presence(), PresenceState::Unavailable); - assert_eq!(clone.sync_presence(), PresenceState::Unavailable); - assert_eq!(notification_client.sync_presence(), PresenceState::Unavailable); + 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::Online, None, false) + .set_presence(PresenceState::Unavailable, None, false) .await .expect("presence should update"); - assert_eq!(client.sync_presence(), PresenceState::Online); - assert_eq!(clone.sync_presence(), PresenceState::Online); - assert_eq!(notification_client.sync_presence(), PresenceState::Online); + 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) @@ -3771,7 +3771,7 @@ pub(crate) mod tests { { let _sync_guard = server .mock_sync() - .set_presence("unavailable") + .set_presence_missing() .ok(|_| {}) .expect(1) .mount_as_scoped() diff --git a/crates/matrix-sdk/src/config/sync.rs b/crates/matrix-sdk/src/config/sync.rs index 3b1b49c7ca8..4ca0e6fd402 100644 --- a/crates/matrix-sdk/src/config/sync.rs +++ b/crates/matrix-sdk/src/config/sync.rs @@ -186,15 +186,15 @@ impl SyncSettings { /// /// If this is not set, the request uses the client-owned sync presence /// value configured with [`Client::set_presence`]. The client default is - /// [`PresenceState::Unavailable`]. + /// [`PresenceState::Online`]. /// /// `PresenceState::Online` - The client is marked as being online. This is - /// the active 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. This - /// is the default preset. + /// is the idle preset. /// /// Sliding Sync requests do not use this per-request setting; they read the /// client-owned sync presence value directly. diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 8d37cb88704..5f32b2e43e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1025,15 +1025,15 @@ mod tests { { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - assert_eq!(request.set_presence, PresenceState::Unavailable); + assert_eq!(request.set_presence, PresenceState::Online); } - client.set_presence(PresenceState::Online, None, false).await?; + client.set_presence(PresenceState::Unavailable, None, false).await?; { let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?; - assert_eq!(request.set_presence, PresenceState::Online); + assert_eq!(request.set_presence, PresenceState::Unavailable); } client.set_presence(PresenceState::Offline, None, false).await?; diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 7223acb8059..8ddf8da2bea 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -2819,6 +2819,12 @@ impl<'a> MockEndpoint<'a, SyncEndpoint> { 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> {