From f5cda21d59d3a894ce9eea925fb8d21b9434660a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 12 Dec 2025 14:38:04 +0000 Subject: [PATCH 01/36] ui: `TimelineEventItem::get_shield`: stop returning `Option` The `ShieldState` enum has a `None` variant, so we don't need an `Option` on top of it. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 2 +- .../src/timeline/event_item/mod.rs | 18 +++++++++--------- .../src/timeline/tests/shields.rs | 12 ++++++------ .../tests/integration/timeline/mod.rs | 11 ++++++----- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index db1be1ee5..8cb78e724 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1278,7 +1278,7 @@ pub struct LazyTimelineItemProvider(Arc Option { - self.0.get_shield(strict).map(Into::into) + Some(self.0.get_shield(strict).into()) } /// Returns some debug information for this event timeline item. diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 97595a4fb..8b9be1ea5 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -309,30 +309,30 @@ impl EventTimelineItem { } } - /// Gets the [`ShieldState`] which can be used to decorate messages in the - /// recommended way. - pub fn get_shield(&self, strict: bool) -> Option { + /// Gets the [`ShieldState`] which can be used to decorate + /// messages in the recommended way. + pub fn get_shield(&self, strict: bool) -> ShieldState { if !self.is_room_encrypted || self.is_local_echo() { - return None; + return ShieldState::None; } // An unable-to-decrypt message has no authenticity shield. if self.content().is_unable_to_decrypt() { - return None; + return ShieldState::None; } match self.encryption_info() { Some(info) => { if strict { - Some(info.verification_state.to_shield_state_strict()) + info.verification_state.to_shield_state_strict().into() } else { - Some(info.verification_state.to_shield_state_lax()) + info.verification_state.to_shield_state_lax().into() } } - None => Some(ShieldState::Red { + None => ShieldState::Red { code: ShieldStateCode::SentInClear, message: SENT_IN_CLEAR, - }), + }, } } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index ee25822fc..e816e2bb7 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -31,7 +31,7 @@ async fn test_no_shield_in_unencrypted_room() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let shield = item.as_event().unwrap().get_shield(false); - assert!(shield.is_none()); + assert_eq!(shield, ShieldState::None); } #[async_test] @@ -46,7 +46,7 @@ async fn test_sent_in_clear_shield() { let shield = item.as_event().unwrap().get_shield(false); assert_eq!( shield, - Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) + ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." } ); } @@ -75,7 +75,7 @@ async fn test_local_sent_in_clear_shield() { // available). assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, None); + assert_eq!(shield, ShieldState::None); { // The date divider comes in late. @@ -96,7 +96,7 @@ async fn test_local_sent_in_clear_shield() { // Then the local echo still should not have a shield. assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, None); + assert_eq!(shield, ShieldState::None); // When the remote echo comes in. timeline @@ -118,7 +118,7 @@ async fn test_local_sent_in_clear_shield() { let shield = event_item.get_shield(false); assert_eq!( shield, - Some(ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }) + ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." } ); // Date divider is adjusted. @@ -168,5 +168,5 @@ async fn test_utd_shield() { // Then the message is displayed with no shield let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let shield = item.as_event().unwrap().get_shield(false); - assert!(shield.is_none()); + assert_eq!(shield, ShieldState::None); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 4a38f8eb9..5a9ed4ce8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -22,6 +22,7 @@ use matrix_sdk::{ linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update}, test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate}, }; +use matrix_sdk_common::deserialized_responses::ShieldState; use matrix_sdk_test::{ ALICE, BOB, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, async_test, event_factory::EventFactory, @@ -757,7 +758,7 @@ async fn test_timeline_without_encryption_info() { assert_eq!(items.len(), 2); assert!(items[0].as_virtual().is_some()); // No encryption, no shields. - assert!(items[1].as_event().unwrap().get_shield(false).is_none()); + assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None); } #[async_test] @@ -787,7 +788,7 @@ async fn test_timeline_without_encryption_can_update() { assert_eq!(items.len(), 2); assert!(items[0].as_virtual().is_some()); // No encryption, no shields - assert!(items[1].as_event().unwrap().get_shield(false).is_none()); + assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None); let encryption_event_content = RoomEncryptionEventContent::with_recommended_defaults(); server @@ -805,17 +806,17 @@ async fn test_timeline_without_encryption_can_update() { // Previous timeline event now has a shield. assert_let!(VectorDiff::Set { index, value } = &timeline_updates[0]); assert_eq!(*index, 1); - assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); // Room encryption event is received. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content()); assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content()); - assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); // New message event is received and has a shield. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); - assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); assert_pending!(stream); } From 7438c59acdf418acb342064eac7f422050f0c813 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Dec 2025 13:05:40 +0000 Subject: [PATCH 02/36] bindings: `get_shields`: stop returning `Option` Again, there is no need for an `Option` as well as a `None` variant --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 8cb78e724..20c3e1f38 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1277,8 +1277,8 @@ pub struct LazyTimelineItemProvider(Arc Option { - Some(self.0.get_shield(strict).into()) + fn get_shields(&self, strict: bool) -> ShieldState { + self.0.get_shield(strict).into() } /// Returns some debug information for this event timeline item. From dbefaef77723d59759301f6f95833bfa4f7c93f3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Dec 2025 13:09:54 +0000 Subject: [PATCH 03/36] bindings: remove `message` from `ShieldState` Since this can't be localised, apps shouldn't be using it. --- bindings/matrix-sdk-ffi/CHANGELOG.md | 5 +++++ bindings/matrix-sdk-ffi/src/timeline/mod.rs | 20 ++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 91497beb5..268ff38f0 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -15,6 +15,11 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `LazyTimelineItemProvider::get_shields` no longer returns an + an `Option`: the `ShieldState` type contains a `None` variant, so the + `Option` was redundant. The `message` field has also been removed: since there + was no way to localise the returned string, applications should not be using it. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) - Add `SpaceService::get_space_room` to get a space given its id from the space graph if available. [#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944) - Add `QrCodeData::to_bytes()` to allow generation of a QR code. diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 20c3e1f38..964a19dd0 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -980,12 +980,12 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState { /// authenticity properties. #[derive(uniffi::Enum, Clone)] pub enum ShieldState { - /// A red shield with a tooltip containing the associated message should be - /// presented. - Red { code: ShieldStateCode, message: String }, - /// A grey shield with a tooltip containing the associated message should be - /// presented. - Grey { code: ShieldStateCode, message: String }, + /// A red shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Red { code: TimelineEventShieldStateCode }, + /// A grey shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Grey { code: TimelineEventShieldStateCode }, /// No shield should be presented. None, } @@ -993,12 +993,8 @@ pub enum ShieldState { impl From for ShieldState { fn from(value: SdkShieldState) -> Self { match value { - SdkShieldState::Red { code, message } => { - Self::Red { code, message: message.to_owned() } - } - SdkShieldState::Grey { code, message } => { - Self::Grey { code, message: message.to_owned() } - } + SdkShieldState::Red { code, message: _ } => Self::Red { code }, + SdkShieldState::Grey { code, message: _ } => Self::Grey { code }, SdkShieldState::None => Self::None, } } From d5ce01acabe07f3a4624f3261bc4f67265e3fae0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Dec 2025 13:14:38 +0000 Subject: [PATCH 04/36] ui: new type for `EventTimelineItem::get_shield` Separate the shield types between common and UI, so that we can change common without breaking UI. The new type does not include a `message` field: since it cannot be localised, clients should not be using it. --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 6 +- .../src/deserialized_responses.rs | 1 - crates/matrix-sdk-ui/CHANGELOG.md | 4 + .../src/timeline/event_item/mod.rs | 86 +++++++++++++++++-- crates/matrix-sdk-ui/src/timeline/mod.rs | 3 +- .../src/timeline/tests/shields.rs | 15 ++-- .../tests/integration/timeline/mod.rs | 15 ++-- 7 files changed, 100 insertions(+), 30 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 964a19dd0..9ff9ebe99 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -21,7 +21,6 @@ use matrix_sdk::{ attachment::{ AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail, }, - deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode}, event_cache::RoomPaginationStatus, room::edit::EditedContent as SdkEditedContent, }; @@ -33,6 +32,7 @@ use matrix_sdk_ui::timeline::{ self, AttachmentConfig, AttachmentSource, EventItemOrigin, LatestEventValue as UiLatestEventValue, LatestEventValueLocalState, MediaUploadProgress as SdkMediaUploadProgress, Profile, TimelineDetails, + TimelineEventShieldState as SdkShieldState, TimelineEventShieldStateCode, TimelineUniqueId as SdkTimelineUniqueId, }; use mime::Mime; @@ -993,8 +993,8 @@ pub enum ShieldState { impl From for ShieldState { fn from(value: SdkShieldState) -> Self { match value { - SdkShieldState::Red { code, message: _ } => Self::Red { code }, - SdkShieldState::Grey { code, message: _ } => Self::Grey { code }, + SdkShieldState::Red { code } => Self::Red { code }, + SdkShieldState::Grey { code } => Self::Grey { code }, SdkShieldState::None => Self::None, } } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index a0211abc7..93d8a6153 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -46,7 +46,6 @@ const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device."; const MISMATCHED_SENDER: &str = "\ The sender of the event does not match the owner of the device \ that created the Megolm session."; -pub const SENT_IN_CLEAR: &str = "Not encrypted."; /// Represents the state of verification for a decrypted message sent by a /// device. diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 73ff21474..90ea430da 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -18,6 +18,10 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `EventTimelineItem::get_shield` now returns a new type, + `TimelineEventShieldState`, which extends the old `ShieldState` with a code + for `SentInClear`, now that the latter has been removed from `ShieldState`. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) - Add `SpaceService::get_space_room` to get a space given its id from the space graph if available. ([#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944)) diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 8b9be1ea5..a14c53a84 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -24,7 +24,7 @@ use matrix_sdk::{ deserialized_responses::{EncryptionInfo, ShieldState}, send_queue::{SendHandle, SendReactionHandle}, }; -use matrix_sdk_base::deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode}; +use matrix_sdk_base::deserialized_responses::ShieldStateCode; use once_cell::sync::Lazy; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId, @@ -309,16 +309,16 @@ impl EventTimelineItem { } } - /// Gets the [`ShieldState`] which can be used to decorate + /// Gets the [`TimelineEventShieldState`] which can be used to decorate /// messages in the recommended way. - pub fn get_shield(&self, strict: bool) -> ShieldState { + pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState { if !self.is_room_encrypted || self.is_local_echo() { - return ShieldState::None; + return TimelineEventShieldState::None; } // An unable-to-decrypt message has no authenticity shield. if self.content().is_unable_to_decrypt() { - return ShieldState::None; + return TimelineEventShieldState::None; } match self.encryption_info() { @@ -329,10 +329,9 @@ impl EventTimelineItem { info.verification_state.to_shield_state_lax().into() } } - None => ShieldState::Red { - code: ShieldStateCode::SentInClear, - message: SENT_IN_CLEAR, - }, + None => { + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } + } } } @@ -693,3 +692,72 @@ impl ReactionsByKeyBySender { None } } + +/// Extends [`ShieldState`] to allow for a `SentInClear` code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TimelineEventShieldState { + /// A red shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Red { + /// A machine-readable representation. + code: TimelineEventShieldStateCode, + }, + /// A grey shield with a tooltip containing a message appropriate to the + /// associated code should be presented. + Grey { + /// A machine-readable representation. + code: TimelineEventShieldStateCode, + }, + /// No shield should be presented. + None, +} + +impl From for TimelineEventShieldState { + fn from(value: ShieldState) -> Self { + match value { + ShieldState::Red { code, message: _ } => { + TimelineEventShieldState::Red { code: code.into() } + } + ShieldState::Grey { code, message: _ } => { + TimelineEventShieldState::Grey { code: code.into() } + } + ShieldState::None => TimelineEventShieldState::None, + } + } +} + +/// Extends [`ShieldStateCode`] to allow for a `SentInClear` code. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +pub enum TimelineEventShieldStateCode { + /// Not enough information available to check the authenticity. + AuthenticityNotGuaranteed, + /// The sending device isn't yet known by the Client. + UnknownDevice, + /// The sending device hasn't been verified by the sender. + UnsignedDevice, + /// The sender hasn't been verified by the Client's user. + UnverifiedIdentity, + /// The sender was previously verified but changed their identity. + VerificationViolation, + /// The `sender` field on the event does not match the owner of the device + /// that established the Megolm session. + MismatchedSender, + /// An unencrypted event in an encrypted room. + SentInClear, +} + +impl From for TimelineEventShieldStateCode { + fn from(value: ShieldStateCode) -> Self { + use TimelineEventShieldStateCode::*; + match value { + ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed, + ShieldStateCode::UnknownDevice => UnknownDevice, + ShieldStateCode::UnsignedDevice => UnsignedDevice, + ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity, + ShieldStateCode::SentInClear => SentInClear, + ShieldStateCode::VerificationViolation => VerificationViolation, + ShieldStateCode::MismatchedSender => MismatchedSender, + } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index af8bec3fc..741da8f15 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -97,7 +97,8 @@ pub use self::{ MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker, - ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineItemContent, + ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState, + TimelineEventShieldStateCode, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind, TimelineUniqueId}, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index e816e2bb7..25901d21d 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -1,6 +1,5 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; -use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode}; use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory}; use ruma::{ event_id, @@ -17,7 +16,7 @@ use ruma::{ use stream_assert::{assert_next_matches, assert_pending}; use crate::timeline::{ - EventSendState, + EventSendState, TimelineEventShieldState, TimelineEventShieldStateCode, tests::{TestTimeline, TestTimelineBuilder}, }; @@ -31,7 +30,7 @@ async fn test_no_shield_in_unencrypted_room() { let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let shield = item.as_event().unwrap().get_shield(false); - assert_eq!(shield, ShieldState::None); + assert_eq!(shield, TimelineEventShieldState::None); } #[async_test] @@ -46,7 +45,7 @@ async fn test_sent_in_clear_shield() { let shield = item.as_event().unwrap().get_shield(false); assert_eq!( shield, - ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." } + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } ); } @@ -75,7 +74,7 @@ async fn test_local_sent_in_clear_shield() { // available). assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, ShieldState::None); + assert_eq!(shield, TimelineEventShieldState::None); { // The date divider comes in late. @@ -96,7 +95,7 @@ async fn test_local_sent_in_clear_shield() { // Then the local echo still should not have a shield. assert!(event_item.is_local_echo()); let shield = event_item.get_shield(false); - assert_eq!(shield, ShieldState::None); + assert_eq!(shield, TimelineEventShieldState::None); // When the remote echo comes in. timeline @@ -118,7 +117,7 @@ async fn test_local_sent_in_clear_shield() { let shield = event_item.get_shield(false); assert_eq!( shield, - ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." } + TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear } ); // Date divider is adjusted. @@ -168,5 +167,5 @@ async fn test_utd_shield() { // Then the message is displayed with no shield let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); let shield = item.as_event().unwrap().get_shield(false); - assert_eq!(shield, ShieldState::None); + assert_eq!(shield, TimelineEventShieldState::None); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 5a9ed4ce8..0a21f5cf8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -22,7 +22,6 @@ use matrix_sdk::{ linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update}, test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate}, }; -use matrix_sdk_common::deserialized_responses::ShieldState; use matrix_sdk_test::{ ALICE, BOB, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, async_test, event_factory::EventFactory, @@ -31,8 +30,8 @@ use matrix_sdk_ui::{ Timeline, timeline::{ AnyOtherFullStateEventContent, Error, EventSendState, MsgLikeKind, OtherMessageLike, - RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineFocus, - TimelineItemContent, VirtualTimelineItem, default_event_filter, + RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineEventShieldState, + TimelineFocus, TimelineItemContent, VirtualTimelineItem, default_event_filter, }, }; use ruma::{ @@ -758,7 +757,7 @@ async fn test_timeline_without_encryption_info() { assert_eq!(items.len(), 2); assert!(items[0].as_virtual().is_some()); // No encryption, no shields. - assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None); + assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None); } #[async_test] @@ -788,7 +787,7 @@ async fn test_timeline_without_encryption_can_update() { assert_eq!(items.len(), 2); assert!(items[0].as_virtual().is_some()); // No encryption, no shields - assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None); + assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None); let encryption_event_content = RoomEncryptionEventContent::with_recommended_defaults(); server @@ -806,17 +805,17 @@ async fn test_timeline_without_encryption_can_update() { // Previous timeline event now has a shield. assert_let!(VectorDiff::Set { index, value } = &timeline_updates[0]); assert_eq!(*index, 1); - assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); // Room encryption event is received. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content()); assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content()); - assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); // New message event is received and has a shield. assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); - assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None); + assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None); assert_pending!(stream); } From b5f2128db17129cab978fd8b121a4ee89fb635cb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 12 Dec 2025 15:01:17 +0000 Subject: [PATCH 05/36] common: remove now-unused `ShieldStateCode::SentInClear` --- crates/matrix-sdk-common/CHANGELOG.md | 7 +++++++ crates/matrix-sdk-common/src/deserialized_responses.rs | 3 --- .../src/snapshots/snapshot_test_shield_codes-5.snap | 4 ++-- crates/matrix-sdk-ui/src/timeline/event_item/mod.rs | 1 - 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index 1a9cb7d7d..80e352021 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features + +- [**breaking**] `ShieldStateCode` no longer includes + `SentInClear`. `VeificationState::to_shield_state_{lax,strict}` never + returned that code, ans so having it in the enum was somewhat misleading. + ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) + ### Bug Fixes - Fix `TimelineEvent::from_bundled_latest_event` sometimes removing the `session_id` of UTDs. This broken event could later be saved to the event cache and become an unresolvable UTD. ([#5970](https://github.com/matrix-org/matrix-rust-sdk/pull/5970)). diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 93d8a6153..1d1daa031 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -282,8 +282,6 @@ pub enum ShieldStateCode { UnsignedDevice, /// The sender hasn't been verified by the Client's user. UnverifiedIdentity, - /// An unencrypted event in an encrypted room. - SentInClear, /// The sender was previously verified but changed their identity. #[serde(alias = "PreviouslyVerified")] VerificationViolation, @@ -1983,7 +1981,6 @@ mod tests { assert_json_snapshot!(ShieldStateCode::UnknownDevice); assert_json_snapshot!(ShieldStateCode::UnsignedDevice); assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity); - assert_json_snapshot!(ShieldStateCode::SentInClear); assert_json_snapshot!(ShieldStateCode::VerificationViolation); }); } diff --git a/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap b/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap index 7b294abf0..753b4b444 100644 --- a/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap +++ b/crates/matrix-sdk-common/src/snapshots/snapshot_test_shield_codes-5.snap @@ -1,5 +1,5 @@ --- source: crates/matrix-sdk-common/src/deserialized_responses.rs -expression: "ShieldStateCode::SentInClear" +expression: "ShieldStateCode::VerificationViolation" --- -"SentInClear" +"VerificationViolation" diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index a14c53a84..09f48b9d6 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -755,7 +755,6 @@ impl From for TimelineEventShieldStateCode { ShieldStateCode::UnknownDevice => UnknownDevice, ShieldStateCode::UnsignedDevice => UnsignedDevice, ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity, - ShieldStateCode::SentInClear => SentInClear, ShieldStateCode::VerificationViolation => VerificationViolation, ShieldStateCode::MismatchedSender => MismatchedSender, } From 84cd2a67d4a4cef79702e456998fbca2b7c4d01b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 25 Dec 2025 22:13:39 +0100 Subject: [PATCH 06/36] Upgrade matrix-sdk-indexeddb to Rust edition 2024 --- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 8cdfd88ec..62fad4b80 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -4,7 +4,7 @@ version = "0.16.0" repository = "https://github.com/matrix-org/matrix-rust-sdk" description = "Web's IndexedDB Storage backend for matrix-sdk" license = "Apache-2.0" -edition = "2021" +edition = "2024" rust-version.workspace = true readme = "README.md" From 06764e25421eedcb5a64083c402bbc656117f15d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 25 Dec 2025 22:13:45 +0100 Subject: [PATCH 07/36] Reformat matrix-sdk-indexeddb --- .../src/crypto_store/migrations/mod.rs | 20 ++++++----- .../src/crypto_store/migrations/v0_to_v5.rs | 5 ++- .../crypto_store/migrations/v101_to_v102.rs | 4 +-- .../src/crypto_store/migrations/v10_to_v11.rs | 4 +-- .../src/crypto_store/migrations/v11_to_v12.rs | 4 +-- .../src/crypto_store/migrations/v12_to_v13.rs | 4 +-- .../src/crypto_store/migrations/v13_to_v14.rs | 4 +-- .../crypto_store/migrations/v14_to_v101.rs | 6 ++-- .../src/crypto_store/migrations/v5_to_v7.rs | 9 +++-- .../src/crypto_store/migrations/v7_to_v8.rs | 6 ++-- .../src/crypto_store/migrations/v8_to_v10.rs | 11 +++--- .../src/crypto_store/mod.rs | 24 ++++++------- crates/matrix-sdk-indexeddb/src/error.rs | 4 +-- .../src/event_cache_store/builder.rs | 4 +-- .../event_cache_store/integration_tests.rs | 12 +++---- .../src/event_cache_store/migrations.rs | 2 +- .../src/event_cache_store/mod.rs | 8 ++--- .../serializer/indexed_types.rs | 2 +- .../src/event_cache_store/transaction.rs | 6 ++-- .../src/media_store/builder.rs | 2 +- .../src/media_store/migrations.rs | 2 +- .../src/media_store/mod.rs | 8 ++--- .../media_store/serializer/indexed_types.rs | 2 +- .../src/media_store/transaction.rs | 4 +-- .../src/media_store/types.rs | 2 +- .../src/serializer/indexed_type/mod.rs | 2 +- .../src/serializer/safe_encode/traits.rs | 11 +++--- .../src/serializer/safe_encode/types.rs | 7 ++-- .../src/state_store/migrations.rs | 23 +++++++------ .../src/state_store/mod.rs | 34 +++++++++---------- .../src/transaction/mod.rs | 8 ++--- 31 files changed, 119 insertions(+), 125 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index 6ed4f1783..58ea72f6f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -25,7 +25,7 @@ use indexed_db_futures::{ }; use tracing::info; -use crate::{crypto_store::Result, serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError}; +use crate::{IndexeddbCryptoStoreError, crypto_store::Result, serializer::SafeEncodeSerializer}; mod old_keys; mod v0_to_v5; @@ -274,13 +274,13 @@ mod tests { }; use matrix_sdk_crypto::{ olm::{InboundGroupSession, SenderData, SessionKey}, - store::{types::RoomKeyWithheldEntry, CryptoStore}, - types::{events::room_key_withheld::RoomKeyWithheldContent, EventEncryptionAlgorithm}, + store::{CryptoStore, types::RoomKeyWithheldEntry}, + types::{EventEncryptionAlgorithm, events::room_key_withheld::RoomKeyWithheldContent}, vodozemac::{Curve25519PublicKey, Curve25519SecretKey, Ed25519PublicKey, Ed25519SecretKey}, }; use matrix_sdk_store_encryption::StoreCipher; use matrix_sdk_test::async_test; - use ruma::{device_id, owned_user_id, room_id, OwnedRoomId, RoomId}; + use ruma::{OwnedRoomId, RoomId, device_id, owned_user_id, room_id}; use serde::Serialize; use tracing_subscriber::util::SubscriberInitExt; use wasm_bindgen::JsValue; @@ -288,8 +288,8 @@ mod tests { use super::{v0_to_v5, v7::InboundGroupSessionIndexedDbObject2}; use crate::{ - crypto_store::{keys, migrations::*, InboundGroupSessionIndexedDbObject}, IndexeddbCryptoStore, + crypto_store::{InboundGroupSessionIndexedDbObject, keys, migrations::*}, }; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -658,10 +658,12 @@ mod tests { )) ); assert_eq!(idb_object.sender_data_type, Some(session.sender_data_type() as u8)); - assert!(raw_store - .index_names() - .find(|idx| idx == "inbound_group_session_sender_key_sender_data_type_idx") - .is_some()); + assert!( + raw_store + .index_names() + .find(|idx| idx == "inbound_group_session_sender_key_sender_data_type_idx") + .is_some() + ); transaction.commit().await.unwrap(); db.close(); diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs index 444d7e952..e30772542 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v0_to_v5.rs @@ -16,15 +16,14 @@ //! the first version of `inbound_group_sessions`. use indexed_db_futures::{ + Build, database::Database, error::{Error, OpenDbError}, - Build, }; use crate::crypto_store::{ - keys, + Result, keys, migrations::{add_nonunique_index, add_unique_index, do_schema_upgrade, old_keys}, - Result, }; /// Perform schema migrations as needed, up to schema version 5. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs index 9ee0d9be0..04b6865d9 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v101 to v102, add the `lease_locks` table. /// diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs index 8ae0cd5b9..0a5039a7a 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs @@ -16,14 +16,14 @@ //! `backup_keys.backup_version_v1`, switching to a new serialization format. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use wasm_bindgen::JsValue; use crate::{ crypto_store::{ keys, - migrations::{do_schema_upgrade, old_keys, MigrationDb}, + migrations::{MigrationDb, do_schema_upgrade, old_keys}, }, serializer::SafeEncodeSerializer, }; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs index 256947904..ec5f6ded0 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v11 to v12, adding an index on /// `(curve_key, sender_data_type, session_id)` to `inbound_group_sessions3`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs index e17a3a4f6..fafa3f538 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v12_to_v13.rs @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -use indexed_db_futures::{error::OpenDbError, Build}; +use indexed_db_futures::{Build, error::OpenDbError}; -use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; +use crate::crypto_store::{Result, keys, migrations::do_schema_upgrade}; /// Perform the schema upgrade v12 to v13, adding the /// `received_room_key_bundles` store. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs index ac9084fcf..3fba7d514 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -use indexed_db_futures::{error::OpenDbError, transaction::TransactionMode, Build}; +use indexed_db_futures::{Build, error::OpenDbError, transaction::TransactionMode}; use super::MigrationDb; use crate::{ - crypto_store::{keys, migrations::do_schema_upgrade, Result}, + crypto_store::{Result, keys, migrations::do_schema_upgrade}, serializer::SafeEncodeSerializer, }; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs index b17c98aaf..883a13e63 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v14_to_v101.rs @@ -19,15 +19,15 @@ limitations under the License. //! the key around; finally, we drop the old table. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::store::types::RoomKeyWithheldEntry; use tracing::{debug, info, warn}; use wasm_bindgen::JsValue; -use super::{old_keys, MigrationDb}; +use super::{MigrationDb, old_keys}; use crate::{ - crypto_store::{keys, migrations::do_schema_upgrade, Result}, + crypto_store::{Result, keys, migrations::do_schema_upgrade}, serializer::SafeEncodeSerializer, }; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs index fd3e803f4..df33f846f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs @@ -20,20 +20,19 @@ //! The migration 6->7 deletes the old store inbound_group_sessions. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - keys, - migrations::{add_nonunique_index, do_schema_upgrade, old_keys, v7, MigrationDb}, - Result, + Result, keys, + migrations::{MigrationDb, add_nonunique_index, do_schema_upgrade, old_keys, v7}, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// Perform the schema upgrade v5 to v6, creating `inbound_group_sessions2`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs index e07f68ce8..3ccb87a97 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs @@ -16,19 +16,19 @@ //! ensuring that the keys are correctly encoded for this new store name. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - migrations::{do_schema_upgrade, old_keys, v7, MigrationDb}, Result, + migrations::{MigrationDb, do_schema_upgrade, old_keys, v7}, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// In the migration v5 to v7, we incorrectly copied the keys in diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs index a7ff3ff0f..613d6e7f0 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs @@ -16,23 +16,22 @@ //! inbound_group_sessions3, shrinking the values stored in each record. use indexed_db_futures::{ - error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, Build, + Build, error::OpenDbError, query_source::QuerySource, transaction::TransactionMode, }; use matrix_sdk_crypto::olm::InboundGroupSession; use tracing::{debug, info}; use wasm_bindgen::JsValue; use crate::{ + IndexeddbCryptoStoreError, crypto_store::{ - keys, + InboundGroupSessionIndexedDbObject, Result, keys, migrations::{ - add_nonunique_index, do_schema_upgrade, old_keys, - v7::InboundGroupSessionIndexedDbObject2, MigrationDb, + MigrationDb, add_nonunique_index, do_schema_upgrade, old_keys, + v7::InboundGroupSessionIndexedDbObject2, }, - InboundGroupSessionIndexedDbObject, Result, }, serializer::SafeEncodeSerializer, - IndexeddbCryptoStoreError, }; /// Perform the schema upgrade v8 to v9, creating `inbound_group_sessions3`. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 2fbe6f33f..879817b8e 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -21,38 +21,38 @@ use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use hkdf::Hkdf; use indexed_db_futures::{ + KeyRange, cursor::Cursor, database::Database, internals::SystemRepr, object_store::ObjectStore, prelude::*, transaction::{Transaction, TransactionMode}, - KeyRange, }; use js_sys::Array; use matrix_sdk_base::cross_process_lock::{ CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, }; use matrix_sdk_crypto::{ + Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, olm::{ Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession, PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session, StaticAccountData, }, store::{ + CryptoStore, CryptoStoreError, types::{ BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, RoomKeyWithheldEntry, RoomSettings, StoredRoomKeyBundleData, }, - CryptoStore, CryptoStoreError, }, vodozemac::base64_encode, - Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ - events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, - RoomId, TransactionId, UserId, + DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId, + events::secret::request::SecretName, }; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -313,11 +313,7 @@ impl PendingIndexeddbChanges { .iter() .filter_map( |(store, pending_operations)| { - if !pending_operations.is_empty() { - Some(*store) - } else { - None - } + if !pending_operations.is_empty() { Some(*store) } else { None } }, ) .collect() @@ -2004,9 +2000,9 @@ mod unit_tests { // Testing the exact JSON here is theoretically flaky in the face of // serialization changes in serde_json but it seems unlikely, and it's // simple enough to fix if we need to. - assert!(serde_json::to_string(&session_needs_backup) - .unwrap() - .contains(r#""needs_backup":1"#),); + assert!( + serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#), + ); } #[test] @@ -2178,7 +2174,7 @@ mod encrypted_tests { use matrix_sdk_crypto::{ cryptostore_integration_tests, olm::Account, - store::{types::PendingChanges, CryptoStore}, + store::{CryptoStore, types::PendingChanges}, vodozemac::base64_encode, }; use matrix_sdk_test::async_test; diff --git a/crates/matrix-sdk-indexeddb/src/error.rs b/crates/matrix-sdk-indexeddb/src/error.rs index 0bd42147a..4384736b4 100644 --- a/crates/matrix-sdk-indexeddb/src/error.rs +++ b/crates/matrix-sdk-indexeddb/src/error.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License +#[cfg(feature = "state-store")] +use matrix_sdk_base::StoreError; #[cfg(feature = "event-cache-store")] use matrix_sdk_base::event_cache::store::EventCacheStoreError; #[cfg(feature = "media-store")] use matrix_sdk_base::media::store::MediaStoreError; -#[cfg(feature = "state-store")] -use matrix_sdk_base::StoreError; #[cfg(any(feature = "event-cache-store", feature = "media-store"))] use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm}; #[cfg(feature = "e2e-encryption")] diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs index 6d8bcd862..16703d494 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs @@ -23,8 +23,8 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ event_cache_store::{ - error::IndexeddbEventCacheStoreError, migrations::open_and_upgrade_db, - IndexeddbEventCacheStore, + IndexeddbEventCacheStore, error::IndexeddbEventCacheStoreError, + migrations::open_and_upgrade_db, }, serializer::{indexed_type::IndexedTypeSerializer, safe_encode::types::SafeEncodeSerializer}, }; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs index 02781b0e5..09dba1150 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs @@ -15,11 +15,11 @@ use assert_matches::assert_matches; use matrix_sdk_base::{ event_cache::{ - store::{ - integration_tests::{check_test_event, make_test_event}, - EventCacheStore, - }, Gap, + store::{ + EventCacheStore, + integration_tests::{check_test_event, make_test_event}, + }, }, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, Position, Update}, }; @@ -573,8 +573,8 @@ pub async fn test_load_previous_chunk(store: IndexeddbEventCacheStore) { /// mod tests { /// use super::{EventCacheStore, EventCacheStoreResult, MyStore}; /// -/// async fn get_event_cache_store( -/// ) -> Result { +/// async fn get_event_cache_store() +/// -> Result { /// Ok(MyStore::new()) /// } /// diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs index c82a24472..91140762c 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -21,7 +21,7 @@ use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v2, Version}; + use super::{Version, v2}; pub const VERSION: Version = Version::V2; pub use v2::keys; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs index 6b9696ef5..69014d906 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -16,13 +16,13 @@ use std::{rc::Rc, time::Duration}; -use indexed_db_futures::{database::Database, Build}; +use indexed_db_futures::{Build, database::Database}; #[cfg(target_family = "wasm")] use matrix_sdk_base::cross_process_lock::{ CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, }; use matrix_sdk_base::{ - event_cache::{store::EventCacheStore, Event, Gap}, + event_cache::{Event, Gap, store::EventCacheStore}, linked_chunk::{ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position, RawChunk, Update, @@ -30,7 +30,7 @@ use matrix_sdk_base::{ timer, }; use ruma::{ - events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, + EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType, }; use tracing::{error, instrument, trace}; use web_sys::IdbTransactionMode; @@ -41,7 +41,7 @@ use crate::{ transaction::IndexeddbEventCacheStoreTransaction, types::{ChunkType, InBandEvent, Lease, OutOfBandEvent}, }, - serializer::indexed_type::{traits::Indexed, IndexedTypeSerializer}, + serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed}, transaction::TransactionError, }; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs index d3454669f..f8455b0ea 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs @@ -29,7 +29,7 @@ use matrix_sdk_base::linked_chunk::{ChunkIdentifier, LinkedChunkId}; use matrix_sdk_crypto::CryptoStoreError; -use ruma::{events::relation::RelationType, EventId, RoomId}; +use ruma::{EventId, RoomId, events::relation::RelationType}; use serde::{Deserialize, Serialize}; use thiserror::Error; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs index 19b8e1411..74dd55eca 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs @@ -19,8 +19,8 @@ use matrix_sdk_base::{ event_cache::{Event as RawEvent, Gap as RawGap}, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, RawChunk}, }; -use ruma::{events::relation::RelationType, EventId, RoomId}; -use serde::{de::DeserializeOwned, Serialize}; +use ruma::{EventId, RoomId, events::relation::RelationType}; +use serde::{Serialize, de::DeserializeOwned}; use crate::{ error::AsyncErrorDeps, @@ -33,9 +33,9 @@ use crate::{ types::{Chunk, ChunkType, Event, Gap, Lease, Position}, }, serializer::indexed_type::{ + IndexedTypeSerializer, range::IndexedKeyRange, traits::{Indexed, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds}, - IndexedTypeSerializer, }, transaction::{Transaction, TransactionError}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs index 1dede46d0..1d1526b96 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs @@ -19,7 +19,7 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ media_store::{ - error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, IndexeddbMediaStore, + IndexeddbMediaStore, error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, }, serializer::{indexed_type::IndexedTypeSerializer, safe_encode::types::SafeEncodeSerializer}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs index 5d115498e..3bcc2fbe4 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs @@ -21,7 +21,7 @@ use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v2, Version}; + use super::{Version, v2}; pub const VERSION: Version = Version::V2; pub use v2::keys; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs index 92b8dc778..67d568ea8 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs @@ -29,7 +29,7 @@ use std::{rc::Rc, time::Duration}; pub use builder::IndexeddbMediaStoreBuilder; pub use error::IndexeddbMediaStoreError; use indexed_db_futures::{ - cursor::CursorDirection, database::Database, transaction::TransactionMode, Build, + Build, cursor::CursorDirection, database::Database, transaction::TransactionMode, }; #[cfg(target_family = "wasm")] use matrix_sdk_base::cross_process_lock::{ @@ -37,15 +37,15 @@ use matrix_sdk_base::cross_process_lock::{ }; use matrix_sdk_base::{ media::{ + MediaRequestParameters, store::{ IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore, MediaStoreInner, }, - MediaRequestParameters, }, timer, }; -use ruma::{time::SystemTime, MilliSecondsSinceUnixEpoch, MxcUri}; +use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, time::SystemTime}; use tracing::instrument; use crate::{ @@ -53,7 +53,7 @@ use crate::{ transaction::IndexeddbMediaStoreTransaction, types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime}, }, - serializer::indexed_type::{traits::Indexed, IndexedTypeSerializer}, + serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed}, transaction::TransactionError, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs index fc6ed3681..13d25364d 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs @@ -30,8 +30,8 @@ use std::ops::Deref; use matrix_sdk_base::media::{ - store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, MediaRequestParameters, UniqueKey, + store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, }; use matrix_sdk_crypto::CryptoStoreError; use ruma::MxcUri; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs index e492d4aaf..d7dc9abdd 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs @@ -16,8 +16,8 @@ use std::ops::Deref; use indexed_db_futures::{cursor::CursorDirection, transaction as inner}; use matrix_sdk_base::media::{ - store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, MediaRequestParameters, + store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, }; use ruma::MxcUri; use uuid::Uuid; @@ -34,7 +34,7 @@ use crate::{ types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime}, }, serializer::indexed_type::{ - range::IndexedKeyRange, traits::IndexedPrefixKeyComponentBounds, IndexedTypeSerializer, + IndexedTypeSerializer, range::IndexedKeyRange, traits::IndexedPrefixKeyComponentBounds, }, transaction::{Transaction, TransactionError}, }; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/types.rs b/crates/matrix-sdk-indexeddb/src/media_store/types.rs index 204afba9e..06986fed4 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/types.rs @@ -19,7 +19,7 @@ use std::{ use matrix_sdk_base::{ cross_process_lock::CrossProcessLockGeneration, - media::{store::IgnoreMediaRetentionPolicy, MediaRequestParameters}, + media::{MediaRequestParameters, store::IgnoreMediaRetentionPolicy}, }; use ruma::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs index 66182ee3d..1423152bd 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs @@ -25,7 +25,7 @@ pub mod traits; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::KeyRange; use range::IndexedKeyRange; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; use traits::{Indexed, IndexedKey}; use wasm_bindgen::JsValue; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs index aa4bf32c9..a5c276e50 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs @@ -1,18 +1,17 @@ //! Helpers for wasm32/browser environments use base64::{ - alphabet, - engine::{general_purpose, GeneralPurpose}, - Engine, + Engine, alphabet, + engine::{GeneralPurpose, general_purpose}, }; use indexed_db_futures::KeyRange; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ - events::{ - receipt::ReceiptType, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, - }, DeviceId, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, TransactionId, UserId, + events::{ + GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, receipt::ReceiptType, + }, }; use wasm_bindgen::JsValue; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs index 348c899c4..c17e97707 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs @@ -15,15 +15,14 @@ use std::sync::Arc; use base64::{ - alphabet, - engine::{general_purpose, GeneralPurpose}, - Engine, + Engine, alphabet, + engine::{GeneralPurpose, general_purpose}, }; use gloo_utils::format::JsValueSerdeExt; use indexed_db_futures::KeyRange; use matrix_sdk_crypto::CryptoStoreError; use matrix_sdk_store_encryption::{EncryptedValueBase64, StoreCipher}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use wasm_bindgen::JsValue; use zeroize::Zeroizing; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index d066d12d2..a413c4a37 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -28,17 +28,17 @@ use indexed_db_futures::{ }; use js_sys::Date as JsDate; use matrix_sdk_base::{ - deserialized_responses::SyncOrStrippedState, store::migration_helpers::RoomInfoV1, - StateStoreDataKey, + StateStoreDataKey, deserialized_responses::SyncOrStrippedState, + store::migration_helpers::RoomInfoV1, }; use matrix_sdk_store_encryption::StoreCipher; use ruma::{ events::{ + StateEventType, room::{ create::RoomCreateEventContent, member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, }, - StateEventType, }, serde::Raw, }; @@ -47,8 +47,8 @@ use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; use wasm_bindgen::JsValue; use super::{ - deserialize_value, encode_key, encode_to_range, keys, serialize_value, Result, RoomMember, - ALL_STORES, + ALL_STORES, Result, RoomMember, deserialize_value, encode_key, encode_to_range, keys, + serialize_value, }; use crate::IndexeddbStateStoreError; @@ -854,33 +854,34 @@ mod tests { transaction::{Transaction, TransactionMode}, }; use matrix_sdk_base::{ + RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError, deserialized_responses::RawMemberEvent, store::{RoomLoadSettings, StateStoreExt}, sync::UnreadNotificationsCount, - RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError, }; use matrix_sdk_test::{async_test, test_json}; use ruma::{ + EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId, events::{ + AnySyncStateEvent, StateEventType, room::{ create::RoomCreateEventContent, member::{StrippedRoomMemberEvent, SyncRoomMemberEvent}, }, - AnySyncStateEvent, StateEventType, }, owned_user_id, room_id, serde::Raw, - server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId, + server_name, user_id, }; use serde_json::json; use uuid::Uuid; use wasm_bindgen::JsValue; - use super::{old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION}; + use super::{CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, MigrationConflictStrategy, old_keys}; use crate::{ - serializer::safe_encode::traits::SafeEncode, - state_store::{encode_key, keys, serialize_value, Result}, IndexeddbStateStore, IndexeddbStateStoreError, + serializer::safe_encode::traits::SafeEncode, + state_store::{Result, encode_key, keys, serialize_value}, }; const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key"; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index a90a3cc8b..23b9e3896 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -22,38 +22,38 @@ use async_trait::async_trait; use gloo_utils::format::JsValueSerdeExt; use growable_bloom_filter::GrowableBloom; use indexed_db_futures::{ - cursor::CursorDirection, database::Database, error::OpenDbError, prelude::*, - transaction::TransactionMode, KeyRange, + KeyRange, cursor::CursorDirection, database::Database, error::OpenDbError, prelude::*, + transaction::TransactionMode, }; use matrix_sdk_base::{ + MinimalRoomMemberEvent, ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, RoomInfo, + RoomMemberships, StateStoreDataKey, StateStoreDataValue, ThreadSubscriptionCatchupToken, deserialized_responses::{DisplayName, RawAnySyncOrStrippedState}, store::{ - compare_thread_subscription_bump_stamps, ChildTransactionId, ComposerDraft, - DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequest, QueuedRequestKind, - RoomLoadSettings, SentRequestKey, SerializableEventContent, StateChanges, StateStore, - StoreError, StoredThreadSubscription, SupportedVersionsResponse, ThreadSubscriptionStatus, - TtlStoreValue, WellKnownResponse, + ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind, + QueuedRequest, QueuedRequestKind, RoomLoadSettings, SentRequestKey, + SerializableEventContent, StateChanges, StateStore, StoreError, StoredThreadSubscription, + SupportedVersionsResponse, ThreadSubscriptionStatus, TtlStoreValue, WellKnownResponse, + compare_thread_subscription_bump_stamps, }, - MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue, - ThreadSubscriptionCatchupToken, ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, }; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ - canonical_json::{redact, RedactedBecause}, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, + OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + canonical_json::{RedactedBecause, redact}, events::{ + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, + GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, SyncStateEvent, presence::PresenceEvent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::member::{ MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent, SyncRoomMemberEvent, }, - AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, - GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, SyncStateEvent, }, serde::Raw, - CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, - OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, }; -use serde::{de::DeserializeOwned, ser::Error, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned, ser::Error}; use tracing::{debug, warn}; use wasm_bindgen::JsValue; @@ -2103,8 +2103,8 @@ mod migration_tests { use assert_matches2::assert_matches; use matrix_sdk_base::store::{QueuedRequestKind, SerializableEventContent}; use ruma::{ - events::room::message::RoomMessageEventContent, room_id, OwnedRoomId, OwnedTransactionId, - TransactionId, + OwnedRoomId, OwnedTransactionId, TransactionId, + events::room::message::RoomMessageEventContent, room_id, }; use serde::{Deserialize, Serialize}; diff --git a/crates/matrix-sdk-indexeddb/src/transaction/mod.rs b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs index 03074190e..ac1836a7d 100644 --- a/crates/matrix-sdk-indexeddb/src/transaction/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs @@ -20,12 +20,12 @@ use futures_util::TryStreamExt; use indexed_db_futures::{ - cursor::CursorDirection, internals::SystemRepr, query_source::QuerySource, - transaction as inner, BuildSerde, + BuildSerde, cursor::CursorDirection, internals::SystemRepr, query_source::QuerySource, + transaction as inner, }; use serde::{ - de::{DeserializeOwned, Error}, Serialize, + de::{DeserializeOwned, Error}, }; use thiserror::Error; use wasm_bindgen::JsValue; @@ -33,9 +33,9 @@ use wasm_bindgen::JsValue; use crate::{ error::{AsyncErrorDeps, GenericError}, serializer::indexed_type::{ + IndexedTypeSerializer, range::IndexedKeyRange, traits::{Indexed, IndexedKey}, - IndexedTypeSerializer, }, }; From 0a9994c5299a159f633f8c899916f822448a76e8 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 25 Dec 2025 22:15:38 +0100 Subject: [PATCH 08/36] Fix new clippy lints --- .../src/state_store/migrations.rs | 10 ++-- .../src/state_store/mod.rs | 58 +++++++++---------- .../tests/integration/timeline/media.rs | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index a413c4a37..664d32561 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -408,11 +408,11 @@ async fn v3_fix_store(store: &ObjectStore<'_>, store_cipher: Option<&StoreCipher if json.contains(r#""content":null"#) { let mut value: JsonValue = serde_json::from_str(json)?; - if let Some(content) = value.get_mut("content") { - if matches!(content, JsonValue::Null) { - *content = JsonValue::Object(Default::default()); - return Ok(Some(value)); - } + if let Some(content) = value.get_mut("content") + && matches!(content, JsonValue::Null) + { + *content = JsonValue::Object(Default::default()); + return Ok(Some(value)); } } diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 23b9e3896..ee6eefbd6 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -372,12 +372,12 @@ impl IndexeddbStateStore { .open_cursor() .with_direction(CursorDirection::Prev) .await? + && let Some(record) = cursor.next_record::().await? { - if let Some(record) = cursor.next_record::().await? { - return Ok(record.as_string()); - } + Ok(record.as_string()) + } else { + Ok(None) } - Ok(None) } /// Encrypt (if needs be) then JSON-serialize a value. @@ -1044,32 +1044,32 @@ impl_state_store!({ }; let raw_evt = self.deserialize_value::>(&value)?; - if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") { - if let Some(redaction) = redactions.get(&event_id) { - let redaction_rules = { - if redaction_rules.is_none() { - redaction_rules.replace(room_info - .get(&self.encode_key(keys::ROOM_INFOS, room_id)) - .await? - .and_then(|f| self.deserialize_value::(&f).ok()) - .map(|info| info.room_version_rules_or_default()) - .unwrap_or_else(|| { - warn!(?room_id, "Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}"); - ROOM_VERSION_RULES_FALLBACK - }).redaction - ); - } - redaction_rules.as_ref().unwrap() - }; + if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") + && let Some(redaction) = redactions.get(&event_id) + { + let redaction_rules = { + if redaction_rules.is_none() { + redaction_rules.replace(room_info + .get(&self.encode_key(keys::ROOM_INFOS, room_id)) + .await? + .and_then(|f| self.deserialize_value::(&f).ok()) + .map(|info| info.room_version_rules_or_default()) + .unwrap_or_else(|| { + warn!(?room_id, "Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}"); + ROOM_VERSION_RULES_FALLBACK + }).redaction + ); + } + redaction_rules.as_ref().unwrap() + }; - let redacted = redact( - raw_evt.deserialize_as::()?, - redaction_rules, - Some(RedactedBecause::from_raw_event(redaction)?), - ) - .map_err(StoreError::Redaction)?; - state.put(&self.serialize_value(&redacted)?).with_key(key).build()?; - } + let redacted = redact( + raw_evt.deserialize_as::()?, + redaction_rules, + Some(RedactedBecause::from_raw_event(redaction)?), + ) + .map_err(StoreError::Redaction)?; + state.put(&self.serialize_value(&redacted)?).with_key(key).build()?; } } } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs index 9c408b7a8..aeada71c8 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/media.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/media.rs @@ -483,7 +483,7 @@ async fn test_send_gallery_from_bytes() -> TestResult { assert_let!(MediaSource::Plain(uri) = &file.source); assert!(uri.to_string().contains("localhost")); - (*index, progress.clone()) + (*index, *progress) }; // Eventually, the media is updated with the final MXC IDs… From e4aff871de8d1efe6b0ae8ad57795667a9754e43 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 25 Dec 2025 22:25:19 +0100 Subject: [PATCH 09/36] Refactor IndexeddbStateStore::save_changes --- .../src/state_store/mod.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index ee6eefbd6..ce3a3f30c 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -1047,20 +1047,26 @@ impl_state_store!({ if let Ok(Some(event_id)) = raw_evt.get_field::("event_id") && let Some(redaction) = redactions.get(&event_id) { - let redaction_rules = { - if redaction_rules.is_none() { - redaction_rules.replace(room_info + let redaction_rules = match &redaction_rules { + Some(r) => r, + None => { + let value = room_info .get(&self.encode_key(keys::ROOM_INFOS, room_id)) .await? .and_then(|f| self.deserialize_value::(&f).ok()) .map(|info| info.room_version_rules_or_default()) .unwrap_or_else(|| { - warn!(?room_id, "Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}"); + warn!( + ?room_id, + "Unable to get the room version rules, \ + defaulting to rules for room version \ + {ROOM_VERSION_FALLBACK}" + ); ROOM_VERSION_RULES_FALLBACK - }).redaction - ); + }) + .redaction; + redaction_rules.get_or_insert(value) } - redaction_rules.as_ref().unwrap() }; let redacted = redact( From ebbf34e924a2d4c21849ef97ae7773b2a330ee9f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 25 Dec 2025 22:33:33 +0100 Subject: [PATCH 10/36] Fix more new clippy lints --- .../src/event_cache_store/mod.rs | 12 +++++------- crates/matrix-sdk-indexeddb/src/media_store/mod.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs index 69014d906..efa87d9f0 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -406,15 +406,13 @@ impl EventCacheStore for IndexeddbEventCacheStore { )?; if let Some(chunk) = transaction.get_chunk_by_id(linked_chunk_id, before_chunk_identifier).await? + && let Some(previous_identifier) = chunk.previous { - if let Some(previous_identifier) = chunk.previous { - let previous_identifier = ChunkIdentifier::new(previous_identifier); - return Ok(transaction - .load_chunk_by_id(linked_chunk_id, previous_identifier) - .await?); - } + let previous_identifier = ChunkIdentifier::new(previous_identifier); + Ok(transaction.load_chunk_by_id(linked_chunk_id, previous_identifier).await?) + } else { + Ok(None) } - Ok(None) } #[instrument(skip(self))] diff --git a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs index 67d568ea8..c9d51a597 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs @@ -345,12 +345,12 @@ impl MediaStoreInner for IndexeddbMediaStore { let transaction = self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?; - if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await? { - if metadata.ignore_policy != ignore_policy { - metadata.ignore_policy = ignore_policy; - transaction.put_media_metadata(&metadata).await?; - transaction.commit().await?; - } + if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await? + && metadata.ignore_policy != ignore_policy + { + metadata.ignore_policy = ignore_policy; + transaction.put_media_metadata(&metadata).await?; + transaction.commit().await?; } Ok(()) } From 5ee379d58821f500da7011766b7224cdbbd9d714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 1 Jan 2026 12:24:41 +0100 Subject: [PATCH 11/36] fix(ui): Deduplicate aggregation local and remote echo We can have 3 different states for the same aggregation in related_events, in chronological order: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. The local echo with a transaction ID. 2. The local echo with the event ID returned by the server after sending the event. 3. The remote echo received via sync. The transition from states 1 to 2 was already handled in `mark_aggregation_as_sent()`. But the transition from states 2 to 3 was never handled and we ended up with both the local echo and the remote echo in the related events. This resulted in the local echo being chosen over the remote echo when computing the latest edit only because it was first in the list, even though it didn't contain the raw JSON of the edit. Signed-off-by: Kévin Commaille --- .../src/timeline/controller/aggregations.rs | 19 +++++++++- .../tests/integration/timeline/edit.rs | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs b/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs index 47d3d9df5..a44210ea3 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/aggregations.rs @@ -387,7 +387,24 @@ impl Aggregations { } self.inverted_map.insert(aggregation.own_id.clone(), related_to.clone()); - self.related_events.entry(related_to).or_default().push(aggregation); + + // We can have 3 different states for the same aggregation in related_events, in + // chronological order: + // + // 1. The local echo with a transaction ID. + // 2. The local echo with the event ID returned by the server after sending the + // event. + // 3. The remote echo received via sync. + // + // The transition from states 1 to 2 is handled in `mark_aggregation_as_sent()`. + // So here we need to handle the transition from states 2 to 3. We need to + // replace the local echo by the remote echo, which might have more data, like + // the raw JSON. + let related_events = self.related_events.entry(related_to).or_default(); + if let Some(pos) = related_events.iter().position(|agg| agg.own_id == aggregation.own_id) { + related_events.remove(pos); + } + related_events.push(aggregation); } /// Is the given id one for a known aggregation to another event? diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 55b634ea0..bf271f2db 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -312,16 +312,14 @@ async fn test_send_edit() { let hello_world_message = hello_world_item.content().as_message().unwrap(); assert!(!hello_world_message.is_edited()); assert!(hello_world_item.is_editable()); + assert_matches!(hello_world_item.original_json(), Some(_)); + assert_matches!(hello_world_item.latest_edit_json(), None); server.mock_room_send().ok(event_id!("$edit_event")).mock_once().mount().await; + let edit = RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"); timeline - .edit( - &hello_world_item.identifier(), - EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( - "Hello, Room!", - )), - ) + .edit(&hello_world_item.identifier(), EditedContent::RoomMessage(edit.clone())) .await .unwrap(); @@ -337,6 +335,31 @@ async fn test_send_edit() { let edit_message = edit_item.content().as_message().unwrap(); assert_eq!(edit_message.body(), "Hello, Room!"); assert!(edit_message.is_edited()); + assert_matches!(edit_item.original_json(), Some(_)); + // The local echo doesn't have the edit's JSON yet. + assert_matches!(edit_item.latest_edit_json(), None); + + // We receive the remote echo for the edit. + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.text_msg("*Hello, Room!") + .sender(client.user_id().unwrap()) + .event_id(event_id!("$edit_event")) + .edit(hello_world_item.event_id().unwrap(), edit), + ), + ) + .await; + + let edit_item = + assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => value); + let edit_message = edit_item.content().as_message().unwrap(); + assert_eq!(edit_message.body(), "Hello, Room!"); + assert!(edit_message.is_edited()); + assert_matches!(edit_item.original_json(), Some(_)); + // The remote echo populated the edit's JSON. + assert_matches!(edit_item.latest_edit_json(), Some(_)); // The response to the mocked endpoint does not generate further timeline // updates, so just wait for a bit before verifying that the endpoint was From b0a536aaeb06c9ed5c8b6ed14380d33f17ae7c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Mon, 5 Jan 2026 12:08:10 +0100 Subject: [PATCH 12/36] Upgrade Ruma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the newly released version. Signed-off-by: Kévin Commaille --- Cargo.lock | 40 +++++++++++++++++++++++++--------------- Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c961fb3ab..64b998ba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4809,8 +4809,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.14.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f620a2116d0d3082f9256e61dcdf67f2ec266d3f6bb9d2f9c8a20ec5a1fabb" dependencies = [ "assign", "js_int", @@ -4827,8 +4828,9 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.22.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc977d1a91ea15dcf896cbd7005ed4a253784468833638998109ffceaee53e7" dependencies = [ "as_variant", "assign", @@ -4850,8 +4852,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a01993f22d291320b7c9267675e7395775e95269ff526e2c8c3ed5e13175b" dependencies = [ "as_variant", "base64", @@ -4883,8 +4886,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.32.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dbdeccb62cb4ffe3282325de8ba28cbc0fdce7c78a3f11b7241fbfdb9cb9907" dependencies = [ "as_variant", "indexmap", @@ -4910,8 +4914,9 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb45c15badbf4299c6113a6b90df3e7cb64edbe756bbd8e0224144b56b38305" dependencies = [ "headers", "http", @@ -4932,7 +4937,8 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.6.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6dcd6e9823e177d15460d3cd3a413f38a2beea381f26aca1001c05cd6954ff" dependencies = [ "as_variant", "html5ever", @@ -4943,7 +4949,8 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c6b5643060beec0fc9d7acfb41d2c5d91e1591db440ff62361d178e77c35fe" dependencies = [ "js_int", "thiserror 2.0.17", @@ -4951,9 +4958,11 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0753312ad577ac462de1742bf2e326b6ba9856ff6f13343aeb17d423fd5426" dependencies = [ + "as_variant", "cfg-if", "proc-macro-crate", "proc-macro2", @@ -4967,7 +4976,8 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.19.0" -source = "git+https://github.com/ruma/ruma?rev=a67081e402dce14365089b34f50489dacc9c53b5#a67081e402dce14365089b34f50489dacc9c53b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146ace2cd59b60ec80d3e801a84e7e6a91e3e01d18a9f5d896ea7ca16a6b8e08" dependencies = [ "base64", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index cf4b8b3e8..f8441bd15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ rand = "0.8.5" regex = "1.12.2" reqwest = { version = "0.12.24", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "a67081e402dce14365089b34f50489dacc9c53b5", features = [ +ruma = { version = "0.14.1", features = [ "client-api-c", "compat-upload-signatures", "compat-arbitrary-length-ids", From 21cad562134d953113dc870750d9f389a651df5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:11:17 +0000 Subject: [PATCH 13/36] chore(deps): bump CodSpeedHQ/action from 4.4.1 to 4.5.2 Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.4.1 to 4.5.2. - [Release notes](https://github.com/codspeedhq/action/releases) - [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codspeedhq/action/compare/346a2d8a8d9d38909abd0bc3d23f773110f076ad...dbda7111f8ac363564b0c51b992d4ce76bb89f2f) --- updated-dependencies: - dependency-name: CodSpeedHQ/action dependency-version: 4.5.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 29d0fd691..f24e5d7fd 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -92,7 +92,7 @@ jobs: run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed - name: Run the benchmarks - uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad + uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f with: run: cargo codspeed run mode: "instrumentation" From 0a5a22ec6fcf0546cd57b61b400d95b00da60e07 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 11:49:11 +0000 Subject: [PATCH 14/36] feat(ui): Expose information about room key bundle forwarder. Signed-off-by: Skye Elliot --- bindings/matrix-sdk-ffi/src/timeline/mod.rs | 19 +++++++++ .../controller/decryption_retry_task.rs | 6 +++ .../timeline/controller/observable_items.rs | 4 ++ .../src/timeline/controller/state.rs | 2 + .../timeline/controller/state_transaction.rs | 28 +++++++++++++ .../src/timeline/date_dividers.rs | 2 + .../src/timeline/event_handler.rs | 20 +++++++++ .../src/timeline/event_item/mod.rs | 42 ++++++++++++++++++- 8 files changed, 122 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 9ff9ebe99..cc36c0e2e 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -1007,6 +1007,8 @@ pub struct EventTimelineItem { event_or_transaction_id: EventOrTransactionId, sender: String, sender_profile: ProfileDetails, + forwarder: Option, + forwarder_profile: Option, is_own: bool, is_editable: bool, content: TimelineItemContent, @@ -1030,6 +1032,8 @@ impl From for EventTimelineItem { event_or_transaction_id: item.identifier().into(), sender: item.sender().to_string(), sender_profile: item.sender_profile().clone().into(), + forwarder: item.forwarder().map(ToString::to_string), + forwarder_profile: item.forwarder_profile().map(Into::into), is_own: item.is_own(), is_editable: item.is_editable(), content: item.content().clone().into(), @@ -1085,6 +1089,21 @@ impl From> for ProfileDetails { } } +impl From<&TimelineDetails> for ProfileDetails { + fn from(details: &TimelineDetails) -> Self { + match details { + TimelineDetails::Unavailable => Self::Unavailable, + TimelineDetails::Pending => Self::Pending, + TimelineDetails::Ready(profile) => Self::Ready { + display_name: profile.display_name.clone(), + display_name_ambiguous: profile.display_name_ambiguous, + avatar_url: profile.avatar_url.as_ref().map(ToString::to_string), + }, + TimelineDetails::Error(e) => Self::Error { message: e.to_string() }, + } + } +} + #[derive(Clone, uniffi::Record)] pub struct PollData { question: String, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs index 68fa53bb0..c88c82956 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs @@ -236,6 +236,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, @@ -262,6 +264,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::MsgLike(MsgLikeContent::unable_to_decrypt( EncryptedMessage::from_content( @@ -315,6 +319,8 @@ mod tests { TimelineItemKind::Event(EventTimelineItem::new( owned_user_id!("@u:s.to"), TimelineDetails::Pending, + None, + None, timestamp(), TimelineItemContent::message( content.msgtype, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 409679fdb..76735ffb4 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -734,6 +734,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { @@ -768,6 +770,8 @@ mod observable_items_tests { EventTimelineItem::new( owned_user_id!("@ivan:mnt.io"), TimelineDetails::Unavailable, + None, + None, MilliSecondsSinceUnixEpoch(0u32.into()), TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message { diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index ba0e813b3..47838fb5a 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -180,6 +180,8 @@ impl TimelineState

{ let ctx = TimelineEventContext { sender: own_user_id, sender_profile: own_profile, + forwarder: None, + forwarder_profile: None, timestamp: MilliSecondsSinceUnixEpoch::now(), read_receipts: Default::default(), // An event sent by ourselves is never matched against push rules. diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs index 5c257d69e..14251e44e 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -226,9 +226,23 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { | Some(action @ TimelineAction::HandleAggregation { .. }) => { let encryption_info = event.kind.encryption_info().cloned(); let sender_profile = room_data_provider.profile_from_user_id(&sender).await; + + let forwarder = encryption_info + .as_ref() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + let mut ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile: forwarder_profile.flatten(), timestamp, // These are not used when handling an aggregation. read_receipts: Default::default(), @@ -700,6 +714,18 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { map.get(&UnsignedEventLocation::RelationsReplace)?.encryption_info().cloned() }); + let forwarder = event + .kind + .encryption_info() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + let (raw, utd_info) = match event.kind { TimelineEventKind::UnableToDecrypt { utd_info, event } => (event, Some(utd_info)), _ => (event.kind.into_raw(), None), @@ -794,6 +820,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let ctx = TimelineEventContext { sender, sender_profile, + forwarder, + forwarder_profile: forwarder_profile.flatten(), timestamp, read_receipts: if settings.track_read_receipts.is_enabled() && should_add diff --git a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs index 54d766220..6d7b4d842 100644 --- a/crates/matrix-sdk-ui/src/timeline/date_dividers.rs +++ b/crates/matrix-sdk-ui/src/timeline/date_dividers.rs @@ -683,6 +683,8 @@ mod tests { EventTimelineItem::new( owned_user_id!("@alice:example.org"), crate::timeline::TimelineDetails::Pending, + None, + None, timestamp, TimelineItemContent::MsgLike(MsgLikeContent::redacted()), event_kind, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 454fe9660..48f93dcc7 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -108,6 +108,16 @@ impl Flow { pub(super) struct TimelineEventContext { pub(super) sender: OwnedUserId, pub(super) sender_profile: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option, /// The event's `origin_server_ts` field (or creation time for local echo). pub(super) timestamp: MilliSecondsSinceUnixEpoch, pub(super) read_receipts: IndexMap, @@ -762,6 +772,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { fn add_item(&mut self, content: TimelineItemContent) { let sender = self.ctx.sender.to_owned(); let sender_profile = TimelineDetails::from_initial_value(self.ctx.sender_profile.clone()); + + let forwarder = self.ctx.forwarder.to_owned(); + let forwarder_profile = self + .ctx + .forwarder + .as_ref() + .map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone())); + let timestamp = self.ctx.timestamp; let kind: EventTimelineItemKind = match &self.ctx.flow { @@ -808,6 +826,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let item = EventTimelineItem::new( sender, sender_profile, + forwarder, + forwarder_profile, timestamp, content, kind, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 09f48b9d6..e2afdb31b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -67,6 +67,16 @@ pub struct EventTimelineItem { pub(super) sender: OwnedUserId, /// The sender's profile of the event. pub(super) sender_profile: TimelineDetails, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder: Option, + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, the forwarder's profile, if present. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub(super) forwarder_profile: Option>, /// The timestamp of the event. pub(super) timestamp: MilliSecondsSinceUnixEpoch, /// The content of the event. @@ -108,15 +118,27 @@ pub(crate) enum TimelineItemHandle<'a> { } impl EventTimelineItem { + #[allow(clippy::too_many_arguments)] pub(super) fn new( sender: OwnedUserId, sender_profile: TimelineDetails, + forwarder: Option, + forwarder_profile: Option>, timestamp: MilliSecondsSinceUnixEpoch, content: TimelineItemContent, kind: EventTimelineItemKind, is_room_encrypted: bool, ) -> Self { - Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted } + Self { + sender, + sender_profile, + forwarder, + forwarder_profile, + timestamp, + content, + kind, + is_room_encrypted, + } } /// Check whether this item is a local echo. @@ -216,6 +238,22 @@ impl EventTimelineItem { &self.sender_profile } + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the user ID of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder(&self) -> Option<&UserId> { + self.forwarder.as_deref() + } + + /// If the keys used to decrypt this event were shared-on-invite as part of + /// an [MSC4268] key bundle, returns the profile of the forwarder. + /// + /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268 + pub fn forwarder_profile(&self) -> Option<&TimelineDetails> { + self.forwarder_profile.as_ref() + } + /// Get the content of this item. pub fn content(&self) -> &TimelineItemContent { &self.content @@ -449,6 +487,8 @@ impl EventTimelineItem { Self { sender: self.sender.clone(), sender_profile: self.sender_profile.clone(), + forwarder: self.forwarder.clone(), + forwarder_profile: self.forwarder_profile.clone(), timestamp: self.timestamp, content, kind, From 6fdd83478a4572d5bdb7420a49a9628529002313 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 12:55:19 +0000 Subject: [PATCH 15/36] tests: Ensure forwarder info accessible via high-level API. --- .../src/tests/e2ee/shared_history.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs index 89e3a670e..ce60589a8 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs @@ -32,7 +32,8 @@ use matrix_sdk_ui::{ Timeline, sync_service::SyncService, timeline::{ - EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineItem, TimelineItemContent, + EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineDetails, TimelineItem, + TimelineItemContent, }, }; use similar_asserts::assert_eq; @@ -178,6 +179,35 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> "The decrypted event should match the message Alice has sent" ); + // We should be able to find the event using the high level timeline API, and + // inspect who forwarded us the keys to decrypt. + + let alice_id = alice.user_id().unwrap(); + let alice_display_name = + alice.account().get_display_name().await?.expect("Alice should have a display name"); + + let bob_timeline = bob_room.timeline().await?; + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await; + let event = item.as_event().expect("The timeline item should be an event"); + + assert_eq!( + event.forwarder().expect("We should be able to access the forwarder's ID"), + alice_id.as_str() + ); + assert_let!( + Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(), + "We should be able to access the forwarder's profile" + ); + assert_eq!( + profile + .display_name + .as_ref() + .expect("We should be able to access the forwarder's display name"), + &alice_display_name + ); + Ok(()) } From 503234976fa14ef1eaefedc8f48f083ad80e2810 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 14:28:44 +0000 Subject: [PATCH 16/36] refactor: Extract forwarder data fetcing to a helper function. --- .../timeline/controller/state_transaction.rs | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs index 14251e44e..f836a4dee 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -227,22 +227,14 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let encryption_info = event.kind.encryption_info().cloned(); let sender_profile = room_data_provider.profile_from_user_id(&sender).await; - let forwarder = encryption_info - .as_ref() - .and_then(|info| info.forwarder.as_ref()) - .map(|info| info.user_id.clone()); - - let forwarder_profile = if let Some(ref forwarder_id) = forwarder { - Some(room_data_provider.profile_from_user_id(forwarder_id).await) - } else { - None - }; + let (forwarder, forwarder_profile) = + get_forwarder_info(&event, room_data_provider).await; let mut ctx = TimelineEventContext { sender, sender_profile, forwarder, - forwarder_profile: forwarder_profile.flatten(), + forwarder_profile, timestamp, // These are not used when handling an aggregation. read_receipts: Default::default(), @@ -694,9 +686,9 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { let is_highlighted = event.push_actions().is_some_and(|actions| actions.iter().any(Action::is_highlight)); - let thread_summary = if let ThreadSummaryStatus::Some(summary) = event.thread_summary { - let latest_reply_item = if let Some(latest_reply) = summary.latest_reply { - self.fetch_latest_thread_reply(&latest_reply, room_data_provider).await + let thread_summary = if let ThreadSummaryStatus::Some(ref summary) = event.thread_summary { + let latest_reply_item = if let Some(ref latest_reply) = summary.latest_reply { + self.fetch_latest_thread_reply(latest_reply, room_data_provider).await } else { None }; @@ -714,17 +706,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { map.get(&UnsignedEventLocation::RelationsReplace)?.encryption_info().cloned() }); - let forwarder = event - .kind - .encryption_info() - .and_then(|info| info.forwarder.as_ref()) - .map(|info| info.user_id.clone()); - - let forwarder_profile = if let Some(ref forwarder_id) = forwarder { - Some(room_data_provider.profile_from_user_id(forwarder_id).await) - } else { - None - }; + let (forwarder, forwarder_profile) = get_forwarder_info(&event, room_data_provider).await; let (raw, utd_info) = match event.kind { TimelineEventKind::UnableToDecrypt { utd_info, event } => (event, Some(utd_info)), @@ -821,7 +803,7 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { sender, sender_profile, forwarder, - forwarder_profile: forwarder_profile.flatten(), + forwarder_profile, timestamp, read_receipts: if settings.track_read_receipts.is_enabled() && should_add @@ -1057,3 +1039,34 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> { } } } + +/// Retrieves the forwarder information for a given timeline event. +/// +/// # Parameters +/// +/// - `event`: The timeline event to extract forwarder information from. +/// - `room_data_provider`: A reference to the room data provider. +/// +/// # Returns +/// +/// A tuple containing: +/// - `Option`: The user ID of the forwarder, if available. +/// - `Option`: The profile of the forwarder, if available. +async fn get_forwarder_info( + event: &TimelineEvent, + room_data_provider: &P, +) -> (Option, Option) { + let forwarder = event + .kind + .encryption_info() + .and_then(|info| info.forwarder.as_ref()) + .map(|info| info.user_id.clone()); + + let forwarder_profile = if let Some(ref forwarder_id) = forwarder { + Some(room_data_provider.profile_from_user_id(forwarder_id).await) + } else { + None + }; + + (forwarder, forwarder_profile.flatten()) +} From bfdd3ccc070fa07e6cc79cab312d44f9708441b7 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 15:00:33 +0000 Subject: [PATCH 17/36] test: Ensure forwarder info is not available on unshared events. --- .../src/tests/e2ee/shared_history.rs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs index ce60589a8..e411256c5 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs @@ -730,6 +730,175 @@ async fn test_history_sharing_session_merging() -> Result<()> { Ok(()) } +/// This is a very similar test to [`test_history_share_on_invite`], but we send +/// a second message once Bob has fully joined. +/// +/// We can't combine this with the above since: +/// +/// - We want to test that history sharing works when Alice's device is deleted, +/// which prevents Alice from sending; +/// - Sending a message after we invite Bob but before they join causes the +/// sessions to be merged, so we lose the forwarder info on the first event as +/// intended. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> Result<()> { + let alice_span = tracing::info_span!("alice"); + let bob_span = tracing::info_span!("bob"); + + let encryption_settings = + EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; + + let alice = TestClientBuilder::new("alice") + .use_sqlite() + .encryption_settings(encryption_settings) + .enable_share_history_on_invite(true) + .build() + .await?; + + let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service"); + let alice_sync_service = SyncService::builder(alice.clone()) + .with_parent_span(sync_service_span) + .build() + .await + .expect("Could not build alice sync service"); + + alice.encryption().wait_for_e2ee_initialization_tasks().await; + alice_sync_service.start().await; + + let bob = SyncTokenAwareClient::new( + TestClientBuilder::new("bob") + .encryption_settings(encryption_settings) + .enable_share_history_on_invite(true) + .build() + .await?, + ); + + // Alice creates a room ... + let alice_room = alice + .create_room(assign!(CreateRoomRequest::new(), { + preset: Some(RoomPreset::PublicChat), + })) + .await?; + alice_room.enable_encryption().await?; + + info!(room_id = ?alice_room.room_id(), "Alice has created and enabled encryption in the room"); + + // ... and sends a message + let event_id = alice_room + .send(RoomMessageEventContent::text_plain("Hello Bob")) + .await + .expect("We should be able to send a message to the room") + .response + .event_id; + + let bundle_stream = bob + .encryption() + .historic_room_key_stream() + .await + .expect("We should be able to get the bundle stream"); + + // Alice invites Bob to the room + alice_room.invite_user_by_id(bob.user_id().unwrap()).await?; + + // Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Bob needs a copy of + // Alice's identity. + bob.encryption() + .request_user_identity(alice.user_id().unwrap()) + .instrument(bob_span.clone()) + .await?; + + let bob_response = bob.sync_once().instrument(bob_span.clone()).await?; + + // Bob should have received a to-device event with the payload + assert_eq!(bob_response.to_device.len(), 1); + let to_device_event = &bob_response.to_device[0]; + assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event); + assert_eq!( + raw.get_field::("type").unwrap().unwrap(), + "io.element.msc4268.room_key_bundle" + ); + + bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); + + pin_mut!(bundle_stream); + + let info = bundle_stream + .next() + .now_or_never() + .flatten() + .expect("We should be notified about the received bundle"); + + assert_eq!(Some(info.sender.deref()), alice.user_id()); + assert_eq!(info.room_id, alice_room.room_id()); + + let bob_room = bob + .join_room_by_id(alice_room.room_id()) + .instrument(bob_span.clone()) + .await + .expect("Bob should be able to accept the invitation from Alice"); + + let event = bob_room + .event(&event_id, None) + .instrument(bob_span.clone()) + .await + .expect("Bob should be able to fetch the historic event"); + + assert_decrypted_message_eq!( + event, + "Hello Bob", + "The decrypted event should match the message Alice has sent" + ); + + // We should be able to find the event using the high level timeline API, and + // inspect who forwarded us the keys to decrypt. + + let alice_id = alice.user_id().unwrap(); + let alice_display_name = + alice.account().get_display_name().await?.expect("Alice should have a display name"); + + let bob_timeline = bob_room.timeline().await?; + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await; + let event = item.as_event().expect("The timeline item should be an event"); + + assert_eq!( + event.forwarder().expect("We should be able to access the forwarder's ID"), + alice_id.as_str() + ); + assert_let!( + Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(), + "We should be able to access the forwarder's profile" + ); + assert_eq!( + profile + .display_name + .as_ref() + .expect("We should be able to access the forwarder's display name"), + &alice_display_name + ); + + // Alice sends a second message, which Bob should receive, but have no forwarder + // info for as it was sent as part of a session they already have. + + let event_id = alice_room + .send(RoomMessageEventContent::text_plain("I said Hello, Bob")) + .await + .expect("We should be able to send a message to the room") + .response + .event_id; + + bob.sync_once().instrument(bob_span.clone()).await?; + + let item = assert_event_received(&bob_timeline, &event_id, "I said Hello, Bob").await; + assert!( + item.as_event().expect("The timeline item should be an event").forwarder().is_none(), + "There should be no forwarder for the second message" + ); + + Ok(()) +} + async fn create_encryption_enabled_client(username: &str) -> Result { let encryption_settings = EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; From a8c9257dfea3202d3d10906342fa304f3d2562c3 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 16:16:20 +0000 Subject: [PATCH 18/36] docs: Update CHANGELOGs. --- bindings/matrix-sdk-ffi/CHANGELOG.md | 4 ++++ crates/matrix-sdk-ui/CHANGELOG.md | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 80e2fc035..eb7199e6f 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -30,6 +30,10 @@ All notable changes to this project will be documented in this file. [#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/) - Created `RoomPowerLevels::events` function which returns a `HashMap` with all the power levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937)) +- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of + the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + key bundle. + ([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000)) ### Refactor diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 90ea430da..668b79f97 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -32,7 +32,11 @@ All notable changes to this project will be documented in this file. ([#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)) - `Room::load_event_with_relations` now also calls `/relations` to fetch related events when falling back to network mode after a cache miss. - ([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930)) + ([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930)) +- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of + the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + key bundle. + ([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000)) ### Refactor From a5b1231f8cbf20be30c17a97dfeed14cfbccca66 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Tue, 6 Jan 2026 16:18:33 +0000 Subject: [PATCH 19/36] refactor: Deduplicate shared history test code to helper methods. --- .../src/tests/e2ee/shared_history.rs | 142 +++++++++--------- 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs index e411256c5..5f23935b0 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs @@ -6,7 +6,7 @@ use assign::assign; use eyeball_im::VectorDiff; use futures::{FutureExt, StreamExt, future, pin_mut}; use matrix_sdk::{ - assert_decrypted_message_eq, assert_next_with_timeout, + Client, assert_decrypted_message_eq, assert_next_with_timeout, deserialized_responses::TimelineEventKind, encryption::EncryptionSettings, room::power_levels::RoomPowerLevelChanges, @@ -66,35 +66,15 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> let alice_span = tracing::info_span!("alice"); let bob_span = tracing::info_span!("bob"); - let encryption_settings = - EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; - - let alice = TestClientBuilder::new("alice") - .use_sqlite() - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .exclude_insecure_devices(exclude_insecure_devices) - .build() + let alice = create_encryption_enabled_client("alice", exclude_insecure_devices) + .instrument(alice_span.clone()) .await?; - let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service"); - let alice_sync_service = SyncService::builder(alice.clone()) - .with_parent_span(sync_service_span) - .build() - .await - .expect("Could not build alice sync service"); + let alice_sync_service = start_client_sync_service(&alice_span, &alice).await; - alice.encryption().wait_for_e2ee_initialization_tasks().await; - alice_sync_service.start().await; - - let bob = SyncTokenAwareClient::new( - TestClientBuilder::new("bob") - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .exclude_insecure_devices(exclude_insecure_devices) - .build() - .await?, - ); + let bob = create_encryption_enabled_client("bob", exclude_insecure_devices) + .instrument(bob_span.clone()) + .await?; // Alice creates a room ... let alice_room = alice @@ -140,13 +120,7 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) -> let bob_response = bob.sync_once().instrument(bob_span.clone()).await?; // Bob should have received a to-device event with the payload - assert_eq!(bob_response.to_device.len(), 1); - let to_device_event = &bob_response.to_device[0]; - assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event); - assert_eq!( - raw.get_field::("type").unwrap().unwrap(), - "io.element.msc4268.room_key_bundle" - ); + assert_received_room_key_bundle(bob_response); bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); @@ -437,11 +411,13 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> { let charlie_span = tracing::info_span!("charlie"); let derek_span = tracing::info_span!("derek"); - let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?; - let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?; + let alice = + create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?; + let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?; let charlie = - create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?; - let derek = create_encryption_enabled_client("derek").instrument(derek_span.clone()).await?; + create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?; + let derek = + create_encryption_enabled_client("derek", false).instrument(derek_span.clone()).await?; // 1. Alice creates a room, and enables encryption let alice_room = alice @@ -602,10 +578,11 @@ async fn test_history_sharing_session_merging() -> Result<()> { let bob_span = tracing::info_span!("bob"); let charlie_span = tracing::info_span!("charlie"); - let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?; - let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?; + let alice = + create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?; + let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?; let charlie = - create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?; + create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?; // 1. Alice creates a room, and enables encryption let alice_room = alice @@ -745,33 +722,13 @@ async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> R let alice_span = tracing::info_span!("alice"); let bob_span = tracing::info_span!("bob"); - let encryption_settings = - EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; - - let alice = TestClientBuilder::new("alice") - .use_sqlite() - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .build() - .await?; - - let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service"); - let alice_sync_service = SyncService::builder(alice.clone()) - .with_parent_span(sync_service_span) - .build() - .await - .expect("Could not build alice sync service"); + let alice = create_encryption_enabled_client("alice", false).await?; + let alice_sync_service = start_client_sync_service(&alice_span, &alice).await; alice.encryption().wait_for_e2ee_initialization_tasks().await; alice_sync_service.start().await; - let bob = SyncTokenAwareClient::new( - TestClientBuilder::new("bob") - .encryption_settings(encryption_settings) - .enable_share_history_on_invite(true) - .build() - .await?, - ); + let bob = create_encryption_enabled_client("bob", false).await?; // Alice creates a room ... let alice_room = alice @@ -807,16 +764,8 @@ async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> R .instrument(bob_span.clone()) .await?; - let bob_response = bob.sync_once().instrument(bob_span.clone()).await?; - // Bob should have received a to-device event with the payload - assert_eq!(bob_response.to_device.len(), 1); - let to_device_event = &bob_response.to_device[0]; - assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event); - assert_eq!( - raw.get_field::("type").unwrap().unwrap(), - "io.element.msc4268.room_key_bundle" - ); + assert_received_room_key_bundle(bob.sync_once().instrument(bob_span.clone()).await?); bob.get_room(alice_room.room_id()).expect("Bob should have received the invite"); @@ -899,7 +848,18 @@ async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> R Ok(()) } -async fn create_encryption_enabled_client(username: &str) -> Result { +/// Creates a new encryption-enabled client with the given username and +/// settings. +/// +/// # Arguments +/// +/// * `username` - The username for the client. +/// * `exclude_insecure_devices` - A boolean indicating whether to exclude +/// insecure devices. +async fn create_encryption_enabled_client( + username: &str, + exclude_insecure_devices: bool, +) -> Result { let encryption_settings = EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() }; @@ -908,6 +868,7 @@ async fn create_encryption_enabled_client(username: &str) -> Result("type").unwrap().unwrap(), + "io.element.msc4268.room_key_bundle", + "Expected the event type to be 'io.element.msc4268.room_key_bundle'" + ); +} + +/// Start the given client's sync service and attach a new span to track logs. +async fn start_client_sync_service( + span: &tracing::Span, + client: &impl Deref, +) -> SyncService { + let sync_service_span = tracing::info_span!(parent: span, "sync_service"); + let sync_service = SyncService::builder(client.deref().clone()) + .with_parent_span(sync_service_span) + .build() + .await + .expect("Could not build sync service"); + + client.encryption().wait_for_e2ee_initialization_tasks().await; + sync_service.start().await; + sync_service +} From b081654c5164ac0e772cdadce1bf2d5f3fee55cf Mon Sep 17 00:00:00 2001 From: ragebreaker <125530737+mlm-games@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:37:36 +0530 Subject: [PATCH 20/36] fix(search): Create key dirs if they don't exist. (#5992) The issue is related to the encrypt_store_dir fn where, when creating the key file, it doesn't ensure that the parent directory exists first. It might be not optimal for the user of the crate to ensure in an non hacky manner, as the sdk iterates through most of its directories internally. Have a test to verify it, which can be removed later (if being merged) Needs a review, might not be the optimal solution as this is my first pr with the crate and am not that familiar with it (although do use it in one of my apps). --- .../src/encrypted/encrypted_dir.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs b/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs index a00ea91d1..ce3afa541 100644 --- a/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs +++ b/crates/matrix-sdk-search/src/encrypted/encrypted_dir.rs @@ -13,7 +13,7 @@ // limitations under the License. use std::{ - fs::File, + fs::{File, create_dir_all}, io::{BufWriter, Cursor, Error as IoError, ErrorKind, Read, Write}, path::Path, sync::Arc, @@ -413,10 +413,13 @@ impl EncryptedMmapDirectory { passphrase: &str, pbkdf_count: u32, ) -> Result { + let dir_path = key_path.parent().unwrap_or(key_path); + + create_dir_all(dir_path).map_err(|err| err.into_tv_err(dir_path))?; // Derive a AES key from our passphrase using a randomly generated salt // to prevent bruteforce attempts using rainbow tables. let (key, hmac_key, salt) = EncryptedMmapDirectory::derive_key(passphrase, pbkdf_count) - .map_err(|err| err.into_tv_err(key_path))?; + .map_err(|err| err.into_tv_err(dir_path))?; // Generate a new random store key. This key will encrypt our Tantivy // indexing files. The key itself is stored encrypted using the derived // key. @@ -696,4 +699,13 @@ mod tests { let _ = EncryptedMmapDirectory::open(tmpdir.path(), "password") .expect("Can't open the store with the new passphrase"); } + + #[test] + fn create_store_in_nonexistent_directory() { + let tmpdir = tempdir().unwrap(); + let nested_path = tmpdir.path().join("nested").join("directory"); + let dir = EncryptedMmapDirectory::open_or_create(&nested_path, "password", PBKDF_COUNT) + .expect("Should create store in non-existent nested directory"); + drop(dir); + } } From fe46a0cce08710a61ddbcca587bf0388e0647c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 5 Jan 2026 12:54:30 +0100 Subject: [PATCH 21/36] fix(sdk): When using `fetch_client_well_known_with_url`, use the server name from the `Client::user_id` as a possible fallback too --- crates/matrix-sdk/src/client/mod.rs | 81 +++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index f48102a55..330e660dc 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2046,22 +2046,46 @@ impl Client { /// Fetches client well_known from network; no caching. pub async fn fetch_client_well_known(&self) -> Option { - let server_url_string = self - .server() - .unwrap_or( - // Sometimes people configure their well-known directly on the homeserver so use - // this as a fallback when the server name is unknown. - &self.homeserver(), - ) - .to_string(); + let homeserver = self.homeserver(); + let scheme = homeserver.scheme(); + // Use the server name, either an explicit one or an implicit one taken from + // the user id + let server_url = self + .server() + .map(|server| server.to_string()) + .or_else(|| self.user_id().map(|id| format!("{}://{}", scheme, id.server_name()))); + + let response = if let Some(server_url) = server_url { + // First try using the server name + self.fetch_client_well_known_with_url(server_url).await + } else { + None + }; + + if response.is_none() { + // Sometimes people configure their well-known directly on the homeserver so use + // this as a fallback when the server name is unknown. + warn!( + "Fetching the well-known from the server name didn't work, using the homeserver url instead" + ); + self.fetch_client_well_known_with_url(homeserver.to_string()).await + } else { + response + } + } + + async fn fetch_client_well_known_with_url( + &self, + url: String, + ) -> Option { let well_known = self .inner .http_client .send( discover_homeserver::Request::new(), Some(RequestConfig::short_retry()), - server_url_string, + url, None, (), Default::default(), @@ -4524,4 +4548,43 @@ pub(crate) mod tests { assert_matches!(client.device_exists(owned_device_id!("ABCDEF")).await, Err(_)); } + + #[async_test] + async fn test_fetching_well_known_with_homeserver_url() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + server.mock_well_known().ok().mount().await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } + + #[async_test] + async fn test_fetching_well_known_with_server_name() { + let server = MatrixMockServer::new().await; + let server_name = ServerName::parse(server.server().address().to_string()).unwrap(); + + server.mock_well_known().ok().mount().await; + + let client = MockClientBuilder::new(None) + .on_builder(|builder| builder.insecure_server_name_no_tls(&server_name)) + .build() + .await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } + + #[async_test] + async fn test_fetching_well_known_with_domain_part_of_user_id() { + let server = MatrixMockServer::new().await; + server.mock_well_known().ok().mount().await; + + let user_id = + UserId::parse(format!("@user:{}", server.server().address())).expect("Invalid user id"); + let client = MockClientBuilder::new(None) + .logged_in_with_token("A_TOKEN".to_owned(), user_id, owned_device_id!("ABCDEF")) + .build() + .await; + + assert_matches!(client.fetch_client_well_known().await, Some(_)); + } } From d52e5cfb50d93572dbd1113b07c7bafd636c0256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 10:15:47 +0100 Subject: [PATCH 22/36] doc: Add more doc comments to `Client::fetch_client_well_known` --- crates/matrix-sdk/src/client/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 330e660dc..dd18c8002 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2045,17 +2045,30 @@ impl Client { } /// Fetches client well_known from network; no caching. + /// + /// 1. If the [`Client::server`] value is available, we use it to fetch the + /// well-known contents. + /// 2. If it's not, we try extracting the server name from the + /// [`Client::user_id`] and building the server URL from it. + /// 3. If we couldn't get the well-known contents with either the explicit + /// server name or the implicit extracted one, we try the homeserver URL + /// as a last resort. pub async fn fetch_client_well_known(&self) -> Option { let homeserver = self.homeserver(); let scheme = homeserver.scheme(); // Use the server name, either an explicit one or an implicit one taken from - // the user id + // the user id: sometimes we'll have only the homeserver url available and no + // server name, but the server name can be extracted from the current user id. let server_url = self .server() .map(|server| server.to_string()) + // If the server name wasn't available, extract it from the user id and build a URL: + // Reuse the same scheme as the homeserver url does, assuming if it's `http` there it + // will be the same for the public server url, lacking a better candidate. .or_else(|| self.user_id().map(|id| format!("{}://{}", scheme, id.server_name()))); + // If the server name is available, first try using it let response = if let Some(server_url) = server_url { // First try using the server name self.fetch_client_well_known_with_url(server_url).await @@ -2063,6 +2076,7 @@ impl Client { None }; + // If we didn't get a well-known value yet, try with the homeserver url instead: if response.is_none() { // Sometimes people configure their well-known directly on the homeserver so use // this as a fallback when the server name is unknown. From 48d1d1f80f5688bb586f544ef7db34d9c54ae436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 10:17:34 +0100 Subject: [PATCH 23/36] doc: Add changelog entry --- crates/matrix-sdk/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 8d126d565..e8523138e 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. ### Bugfix +- Use the server name extracted from the user id in `Client::fetch_client_well_known` as a fallback value. Otherwise, sometimes the server name is not available and we can't reload the well-known contents. ([#5996](https://github.com/matrix-org/matrix-rust-sdk/pull/5996)) - Latest Event is lazier: a `RoomLatestEvents` can be registered even if its associated `RoomEventCache` isn't created yet. ([#5947](https://github.com/matrix-org/matrix-rust-sdk/pull/5947)) From 42de8307bff9cf0c2cfddc559a0e48d77a766e70 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 6 Jan 2026 13:58:23 +0100 Subject: [PATCH 24/36] chore(sdk): Sliding Sync `extensions` is no longer sticky. This patch extracts `SlidingSyncStickyParameters::extensions` to no longer make it sticky. We are dropping sticky parameters as it's not part of the last MSC. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 6 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 327 +----------------- 2 files changed, 17 insertions(+), 316 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 0d25319c7..6c2a5732e 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -289,11 +289,9 @@ impl SlidingSyncBuilder { position: Arc::new(AsyncMutex::new(SlidingSyncPositionMarkers { pos })), sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncStickyParameters::new( - self.subscriptions, - self.extensions.unwrap_or_default(), - ), + SlidingSyncStickyParameters::new(self.subscriptions), )), + extensions: self.extensions.unwrap_or_default(), 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 92c8db59b..daf779b66 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -113,6 +113,10 @@ pub(super) struct SlidingSyncInner { /// Request parameters that are sticky. sticky: StdRwLock>, + /// The intended state of the extensions being supplied to sliding /sync + /// calls. + extensions: http::request::Extensions, + /// Internal channel used to pass messages between Sliding Sync and other /// types. internal_channel: Sender, @@ -421,8 +425,7 @@ impl SlidingSync { debug!(pos = ?position_guard.pos, "Got a position"); - let to_device_enabled = - self.inner.sticky.read().unwrap().data().extensions.to_device.enabled == Some(true); + let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true); let restored_fields = if self.inner.share_pos || to_device_enabled { restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await? @@ -486,6 +489,9 @@ impl SlidingSync { // Apply sticky parameters, if needs be. self.inner.sticky.write().unwrap().maybe_apply(&mut request, txn_id); + // Add extensions. + request.extensions = self.inner.extensions.clone(); + // Extensions are now applied (via sticky parameters). // // Override the to-device token if the extension is enabled. @@ -630,14 +636,13 @@ impl SlidingSync { /// Is the e2ee extension enabled for this sliding sync instance? #[cfg(feature = "e2e-encryption")] fn is_e2ee_enabled(&self) -> bool { - self.inner.sticky.read().unwrap().data().extensions.e2ee.enabled == Some(true) + self.inner.extensions.e2ee.enabled == Some(true) } /// Is the thread subscriptions extension enabled for this sliding sync /// instance? fn is_thread_subscriptions_enabled(&self) -> bool { - self.inner.sticky.read().unwrap().data().extensions.thread_subscriptions.enabled - == Some(true) + self.inner.extensions.thread_subscriptions.enabled == Some(true) } #[cfg(not(feature = "e2e-encryption"))] @@ -832,16 +837,6 @@ impl SlidingSync { let mut position_lock = self.inner.position.lock().await; position_lock.pos = Some(new_pos); } - - /// Read the static extension configuration for this Sliding Sync. - /// - /// Note: this is not the next content of the sticky parameters, but rightly - /// the static configuration that was set during creation of this - /// Sliding Sync. - pub fn extensions_config(&self) -> http::request::Extensions { - let sticky = self.inner.sticky.read().unwrap(); - sticky.data().extensions.clone() - } } #[derive(Clone, Debug)] @@ -885,18 +880,11 @@ pub(super) struct SlidingSyncStickyParameters { /// but one wants to receive updates. room_subscriptions: BTreeMap, - - /// The intended state of the extensions being supplied to sliding /sync - /// calls. - extensions: http::request::Extensions, } impl SlidingSyncStickyParameters { /// Create a new set of sticky parameters. - pub fn new( - room_subscriptions: BTreeMap, - extensions: http::request::Extensions, - ) -> Self { + pub fn new(room_subscriptions: BTreeMap) -> Self { Self { room_subscriptions: room_subscriptions .into_iter() @@ -904,7 +892,6 @@ impl SlidingSyncStickyParameters { (room_id, (RoomSubscriptionState::Pending, room_subscription)) }) .collect(), - extensions, } } } @@ -919,7 +906,6 @@ impl StickyData for SlidingSyncStickyParameters { .filter(|(_, (state, _))| matches!(state, RoomSubscriptionState::Pending)) .map(|(room_id, (_, room_subscription))| (room_id.clone(), room_subscription.clone())) .collect(); - request.extensions = self.extensions.clone(); } fn on_commit(&mut self) { @@ -1222,10 +1208,8 @@ mod tests { room_subscriptions.insert(r0.to_owned(), Default::default()); // At first it's invalidated. - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - room_subscriptions, - Default::default(), - )); + let mut sticky = + SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new(room_subscriptions)); assert!(sticky.is_invalidated()); // Then when we create a request, the sticky parameters are applied. @@ -1295,10 +1279,8 @@ mod tests { let r0 = room_id!("!r0.matrix.org"); let r1 = room_id!("!r1:matrix.org"); - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - BTreeMap::new(), - Default::default(), - )); + let mut sticky = + SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new(BTreeMap::new())); // A room subscription is added, applied, and committed. { @@ -1385,150 +1367,6 @@ mod tests { } } - #[test] - fn test_extensions_are_sticky() { - let mut extensions = http::request::Extensions::default(); - extensions.account_data.enabled = Some(true); - - // At first it's invalidated. - let mut sticky = SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new( - Default::default(), - extensions, - )); - - assert!(sticky.is_invalidated(), "invalidated because of non default parameters"); - - // `StickyParameters::new` follows its caller's intent when it comes to e2ee and - // to-device. - let extensions = &sticky.data().extensions; - assert_eq!(extensions.e2ee.enabled, None); - assert_eq!(extensions.to_device.enabled, None); - assert_eq!(extensions.to_device.since, None); - - // What the user explicitly enabled is… enabled. - assert_eq!(extensions.account_data.enabled, Some(true)); - - let txn_id: &TransactionId = "tid123".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - assert!(sticky.is_invalidated()); - assert_eq!(request.extensions.to_device.enabled, None); - assert_eq!(request.extensions.to_device.since, None); - assert_eq!(request.extensions.e2ee.enabled, None); - assert_eq!(request.extensions.account_data.enabled, Some(true)); - } - - #[async_test] - async fn test_sticky_extensions_plus_since() -> Result<()> { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; - - let sync = client - .sliding_sync("test-slidingsync")? - .add_list(SlidingSyncList::builder("new_list")) - .build() - .await?; - - // No extensions have been explicitly enabled here. - assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.to_device.enabled, None); - assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.e2ee.enabled, None); - assert_eq!(sync.inner.sticky.read().unwrap().data().extensions.account_data.enabled, None); - - // Now enable e2ee and to-device. - let sync = client - .sliding_sync("test-slidingsync")? - .add_list(SlidingSyncList::builder("new_list")) - .with_to_device_extension( - assign!(http::request::ToDevice::default(), { enabled: Some(true)}), - ) - .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)})) - .build() - .await?; - - // Even without a since token, the first request will contain the extensions - // configuration, at least. - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; - - assert_eq!(request.extensions.e2ee.enabled, Some(true)); - assert_eq!(request.extensions.to_device.enabled, Some(true)); - assert!(request.extensions.to_device.since.is_none()); - - { - // Committing with another transaction id doesn't validate anything. - let mut sticky = sync.inner.sticky.write().unwrap(); - assert!(sticky.is_invalidated()); - sticky.maybe_commit( - "hopefully the rng won't generate this very specific transaction id".into(), - ); - assert!(sticky.is_invalidated()); - } - - // Regenerating a request will yield the same one. - let txn_id2 = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id2.to_owned())) - .await?; - - assert_eq!(request.extensions.e2ee.enabled, Some(true)); - assert_eq!(request.extensions.to_device.enabled, Some(true)); - assert!(request.extensions.to_device.since.is_none()); - - assert!(txn_id != txn_id2, "the two requests must not share the same transaction id"); - - { - // Committing with the expected transaction id will validate it. - let mut sticky = sync.inner.sticky.write().unwrap(); - assert!(sticky.is_invalidated()); - sticky.maybe_commit(txn_id2.as_str().into()); - assert!(!sticky.is_invalidated()); - } - - // The next request should contain no sticky parameters. - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; - assert!(request.extensions.e2ee.enabled.is_none()); - assert!(request.extensions.to_device.enabled.is_none()); - assert!(request.extensions.to_device.since.is_none()); - - // If there's a to-device `since` token, we make sure we put the token - // into the extension config. The rest doesn't need to be re-enabled due to - // stickiness. - let _since_token = "since"; - - #[cfg(feature = "e2e-encryption")] - { - use matrix_sdk_base::crypto::store::types::Changes; - if let Some(olm_machine) = &*client.olm_machine().await { - olm_machine - .store() - .save_changes(Changes { - next_batch_token: Some(_since_token.to_owned()), - ..Default::default() - }) - .await?; - } - } - - let txn_id = TransactionId::new(); - let (request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; - - assert!(request.extensions.e2ee.enabled.is_none()); - assert!(request.extensions.to_device.enabled.is_none()); - - #[cfg(feature = "e2e-encryption")] - assert_eq!(request.extensions.to_device.since.as_deref(), Some(_since_token)); - - Ok(()) - } - // With MSC4186, with the `e2ee` extension enabled, if a request has no `pos`, // all the tracked users by the `OlmMachine` must be marked as dirty, i.e. // `/key/query` requests must be sent. See the code to see the details. @@ -1676,141 +1514,6 @@ mod tests { Ok(()) } - #[async_test] - async fn test_unknown_pos_resets_pos_and_sticky_parameters() -> Result<()> { - let server = MockServer::start().await; - let client = logged_in_client(Some(server.uri())).await; - - let sliding_sync = client - .sliding_sync("test-slidingsync")? - .with_to_device_extension( - assign!(http::request::ToDevice::default(), { enabled: Some(true) }), - ) - .build() - .await?; - - // First request asks to enable the extension. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - assert!(request.extensions.to_device.enabled.is_some()); - - let sync = sliding_sync.sync(); - pin_mut!(sync); - - // `pos` is `None` to start with. - assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - - #[derive(Deserialize)] - struct PartialRequest { - txn_id: Option, - } - - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "0", - })) - }) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned())); - } - - // Next request doesn't ask to enable the extension. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - assert!(request.extensions.to_device.enabled.is_none()); - - // Next request is successful. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "1", - })) - }) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("1".to_owned())); - } - - // Next request is successful despite it receives an already - // received `pos` from the server. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(|request: &Request| { - // Repeat the txn_id in the response, if set. - let request: PartialRequest = request.body_json().unwrap(); - - ResponseTemplate::new(200).set_body_json(json!({ - "txn_id": request.txn_id, - "pos": "0", // <- already received! - })) - }) - .up_to_n_times(1) // run this mock only once. - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - assert_matches!(next, Some(Ok(_update_summary))); - - // `pos` has been updated. - assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned())); - } - - // Stop responding with successful requests! - // - // When responding with `M_UNKNOWN_POS`, that regenerates the sticky parameters, - // so they're reset. It also resets the `pos`. - { - let _mock_guard = Mock::given(SlidingSyncMatcher) - .respond_with(ResponseTemplate::new(400).set_body_json(json!({ - "error": "foo", - "errcode": "M_UNKNOWN_POS", - }))) - .mount_as_scoped(&server) - .await; - - let next = sync.next().await; - - // The expected error is returned. - assert_matches!(next, Some(Err(err)) if err.client_api_error_kind() == Some(&ErrorKind::UnknownPos)); - - // `pos` has been reset. - assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - - // Next request asks to enable the extension again. - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; - - assert!(request.extensions.to_device.enabled.is_some()); - - // `sync` has been stopped. - assert!(sync.next().await.is_none()); - } - - Ok(()) - } - #[cfg(feature = "e2e-encryption")] #[async_test] async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> { From b28999e40c5b004e75d0c1c136f93a35352840a1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 6 Jan 2026 15:45:20 +0100 Subject: [PATCH 25/36] chore(sdk): Inline and always apply `SlidingSyncStickyManager`. This patch removes `SlidingSyncStickyManager` as it only contains the logic for a single request field: `room_subscriptions`. Also, previously, `room_subscriptions` was considered sent based on the transaction ID. This is useless as it's always sticky per MSC4186. The logic from `SlidingSyncStickyManager` is sent inlined, allowing to effectively remove this type. --- crates/matrix-sdk/src/sliding_sync/builder.rs | 21 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 417 ++++++++---------- 2 files changed, 207 insertions(+), 231 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/builder.rs b/crates/matrix-sdk/src/sliding_sync/builder.rs index 6c2a5732e..888af82f3 100644 --- a/crates/matrix-sdk/src/sliding_sync/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/builder.rs @@ -11,10 +11,10 @@ use ruma::{OwnedRoomId, api::client::sync::sync_events::v5 as http}; use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock, broadcast::channel}; use super::{ - Error, SlidingSync, SlidingSyncInner, SlidingSyncListBuilder, SlidingSyncPositionMarkers, - Version, cache::format_storage_key_prefix, sticky_parameters::SlidingSyncStickyManager, + Error, RoomSubscriptionState, SlidingSync, SlidingSyncInner, SlidingSyncListBuilder, + SlidingSyncPositionMarkers, Version, cache::format_storage_key_prefix, }; -use crate::{Client, Result, sliding_sync::SlidingSyncStickyParameters}; +use crate::{Client, Result}; /// Configuration for a Sliding Sync instance. /// @@ -28,7 +28,7 @@ pub struct SlidingSyncBuilder { client: Client, lists: Vec, extensions: Option, - subscriptions: BTreeMap, + room_subscriptions: BTreeMap, poll_timeout: Duration, network_timeout: Duration, #[cfg(feature = "e2e-encryption")] @@ -50,7 +50,7 @@ impl SlidingSyncBuilder { client, lists: Vec::new(), extensions: None, - subscriptions: BTreeMap::new(), + room_subscriptions: BTreeMap::new(), poll_timeout: Duration::from_secs(30), network_timeout: Duration::from_secs(30), #[cfg(feature = "e2e-encryption")] @@ -288,9 +288,14 @@ impl SlidingSyncBuilder { position: Arc::new(AsyncMutex::new(SlidingSyncPositionMarkers { pos })), - sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncStickyParameters::new(self.subscriptions), - )), + room_subscriptions: StdRwLock::new( + self.room_subscriptions + .into_iter() + .map(|(room_id, room_subscription)| { + (room_id, (RoomSubscriptionState::Pending, room_subscription)) + }) + .collect(), + ), extensions: self.extensions.unwrap_or_default(), 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 daf779b66..8aec0d883 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -50,9 +50,8 @@ use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; pub use self::{builder::*, client::VersionBuilderError, error::*, list::*}; use self::{ - cache::restore_sliding_sync_state, - client::SlidingSyncResponseProcessor, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager, StickyData}, + cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor, + sticky_parameters::LazyTransactionId, }; use crate::{Client, Result, config::RequestConfig}; @@ -110,8 +109,10 @@ pub(super) struct SlidingSyncInner { /// The lists of this Sliding Sync instance. lists: AsyncRwLock>, - /// Request parameters that are sticky. - sticky: StdRwLock>, + /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists + /// but one wants to receive updates. + room_subscriptions: + StdRwLock>, /// The intended state of the extensions being supplied to sliding /sync /// calls. @@ -149,8 +150,7 @@ impl SlidingSync { cancel_in_flight_request: bool, ) { let settings = settings.unwrap_or_default(); - let mut sticky = self.inner.sticky.write().unwrap(); - let room_subscriptions = &mut sticky.data_mut().room_subscriptions; + let room_subscriptions = &mut self.inner.room_subscriptions.write().unwrap(); let mut skip_over_current_sync_loop_iteration = false; @@ -310,11 +310,24 @@ impl SlidingSync { trace!(?sync_response); // Commit sticky parameters, if needed. - if let Some(ref txn_id) = sliding_sync_response.txn_id { - let txn_id = txn_id.as_str().into(); - self.inner.sticky.write().unwrap().maybe_commit(txn_id); - let mut lists = self.inner.lists.write().await; - lists.values_mut().for_each(|list| list.maybe_commit_sticky(txn_id)); + { + // All room subscriptions are marked as `Applied`. + { + let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap(); + + for (state, _room_subscription) in room_subscriptions.values_mut() { + if matches!(state, RoomSubscriptionState::Pending) { + *state = RoomSubscriptionState::Applied; + } + } + } + + // Commit lists. + if let Some(ref txn_id) = sliding_sync_response.txn_id { + let txn_id = txn_id.as_str().into(); + let mut lists = self.inner.lists.write().await; + lists.values_mut().for_each(|list| list.maybe_commit_sticky(txn_id)); + } } let update_summary = { @@ -486,8 +499,16 @@ impl SlidingSync { lists: requests_lists, }); - // Apply sticky parameters, if needs be. - self.inner.sticky.write().unwrap().maybe_apply(&mut request, txn_id); + // Add room subscriptions. + request.room_subscriptions = self + .inner + .room_subscriptions + .read() + .unwrap() + .iter() + .filter(|(_, (state, _))| matches!(state, RoomSubscriptionState::Pending)) + .map(|(room_id, (_, room_subscription))| (room_id.clone(), room_subscription.clone())) + .collect(); // Add extensions. request.extensions = self.inner.extensions.clone(); @@ -654,7 +675,7 @@ impl SlidingSync { async fn must_process_rooms_response(&self) -> bool { // We consider that we must, if there's any room subscription or there's any // list. - !self.inner.sticky.read().unwrap().data().room_subscriptions.is_empty() + !self.inner.room_subscriptions.read().unwrap().is_empty() || !self.inner.lists.read().await.is_empty() } @@ -795,11 +816,9 @@ impl SlidingSync { } { - let mut sticky = self.inner.sticky.write().unwrap(); - // Clear all room subscriptions: we don't want to resend all room subscriptions // when the session will restart. - sticky.data_mut().room_subscriptions.clear(); + self.inner.room_subscriptions.write().unwrap().clear(); } } } @@ -857,67 +876,29 @@ pub struct UpdateSummary { } /// A very basic bool-ish enum to represent the state of a -/// [`http::request::RoomSubscription`]. A `RoomSubscription` that has been sent -/// once should ideally not being sent again, to mostly save bandwidth. +/// [`RoomSubscription`]. +/// +/// Once a [`RoomSubscription`] has beent sent, it's not removed from the list +/// of room subscriptions, but instead is marked as [`Self::Applied`], so that +/// it cannot be sent again, mostly to save bandwidth. +/// +/// [`RoomSubscription`]: http::request::RoomSubscription #[derive(Debug, Default)] enum RoomSubscriptionState { - /// The `RoomSubscription` has not been sent or received correctly from the - /// server, i.e. the `RoomSubscription` —which is part of the sticky - /// parameters— has not been committed. + /// The [`RoomSubscription`] has not been sent to or received correctly by + /// the server. + /// + /// [`RoomSubscription`]: http::request::RoomSubscription #[default] Pending, - /// The `RoomSubscription` has been sent and received correctly by the + /// The [`RoomSubscription`] has been sent and received correctly by the /// server. + /// + /// [`RoomSubscription`]: http::request::RoomSubscription Applied, } -/// The set of sticky parameters owned by the `SlidingSyncInner` instance, and -/// sent in the request. -#[derive(Debug)] -pub(super) struct SlidingSyncStickyParameters { - /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists - /// but one wants to receive updates. - room_subscriptions: - BTreeMap, -} - -impl SlidingSyncStickyParameters { - /// Create a new set of sticky parameters. - pub fn new(room_subscriptions: BTreeMap) -> Self { - Self { - room_subscriptions: room_subscriptions - .into_iter() - .map(|(room_id, room_subscription)| { - (room_id, (RoomSubscriptionState::Pending, room_subscription)) - }) - .collect(), - } - } -} - -impl StickyData for SlidingSyncStickyParameters { - type Request = http::Request; - - fn apply(&self, request: &mut Self::Request) { - request.room_subscriptions = self - .room_subscriptions - .iter() - .filter(|(_, (state, _))| matches!(state, RoomSubscriptionState::Pending)) - .map(|(room_id, (_, room_subscription))| (room_id.clone(), room_subscription.clone())) - .collect(); - } - - fn on_commit(&mut self) { - // All room subscriptions are marked as `Applied`. - for (state, _room_subscription) in self.room_subscriptions.values_mut() { - if matches!(state, RoomSubscriptionState::Pending) { - *state = RoomSubscriptionState::Applied; - } - } - } -} - #[cfg(all(test, not(target_family = "wasm")))] #[allow(clippy::dbg_macro)] mod tests { @@ -936,9 +917,7 @@ mod tests { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory}; use ruma::{ - OwnedRoomId, TransactionId, - api::client::error::ErrorKind, - assign, + OwnedRoomId, TransactionId, assign, events::{direct::DirectEvent, room::member::MembershipState}, owned_room_id, room_id, serde::Raw, @@ -951,13 +930,12 @@ mod tests { }; use super::{ - SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, - SlidingSyncStickyParameters, http, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager}, + RoomSubscriptionState, SlidingSync, SlidingSyncBuilder, SlidingSyncList, + SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http, + sticky_parameters::LazyTransactionId, }; use crate::{ Client, Result, - sliding_sync::cache::restore_sliding_sync_state, test_utils::{logged_in_client, mocks::MatrixMockServer}, }; @@ -1067,8 +1045,7 @@ mod tests { assert!(room0.are_members_synced().not()); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1126,8 +1103,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1138,8 +1114,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0)); assert!(room_subscriptions.contains_key(room_id_1)); @@ -1150,8 +1125,7 @@ mod tests { sliding_sync.expire_session().await; { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.is_empty()); } @@ -1160,8 +1134,7 @@ mod tests { sliding_sync.subscribe_to_rooms(&[room_id_2], None, false); { - let sticky = sliding_sync.inner.sticky.read().unwrap(); - let room_subscriptions = &sticky.data().room_subscriptions; + let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap(); assert!(room_subscriptions.contains_key(room_id_0).not()); assert!(room_subscriptions.contains_key(room_id_1).not()); @@ -1199,171 +1172,169 @@ mod tests { Ok(()) } - #[test] - fn test_sticky_parameters_api_invalidated_flow() { + #[async_test] + async fn test_room_subscriptions_are_sticky() { let r0 = room_id!("!r0.matrix.org"); let r1 = room_id!("!r1:matrix.org"); - let mut room_subscriptions = BTreeMap::new(); - room_subscriptions.insert(r0.to_owned(), Default::default()); + let client = logged_in_client(None).await; + let sliding_sync = + SlidingSyncBuilder::new("foo".to_owned(), client).unwrap().build().await.unwrap(); - // At first it's invalidated. - let mut sticky = - SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new(room_subscriptions)); - assert!(sticky.is_invalidated()); - - // Then when we create a request, the sticky parameters are applied. - let txn_id: &TransactionId = "tid123".into(); - - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - assert!(request.room_subscriptions.contains_key(r0)); - - let tid = request.txn_id.unwrap(); - - sticky.maybe_commit(tid.as_str().into()); - assert!(!sticky.is_invalidated()); - - // Applying new parameters will invalidate again. - sticky - .data_mut() - .room_subscriptions - .insert(r1.to_owned(), (Default::default(), Default::default())); - assert!(sticky.is_invalidated()); - - // Committing with the wrong transaction id will keep it invalidated. - sticky.maybe_commit("wrong tid today, my love has gone away 🎵".into()); - assert!(sticky.is_invalidated()); - - // Restarting a request will only remember the last generated transaction id. - let txn_id1: &TransactionId = "tid456".into(); - let mut request1 = http::Request::default(); - request1.txn_id = Some(txn_id1.to_string()); - sticky.maybe_apply(&mut request1, &mut LazyTransactionId::from_owned(txn_id1.to_owned())); - - assert!(sticky.is_invalidated()); - // The first room subscription has been applied to `request`, so it's not - // reapplied here. It's a particular logic of `room_subscriptions`, it's not - // related to the sticky design. - assert_eq!(request1.room_subscriptions.len(), 1); - assert!(request1.room_subscriptions.contains_key(r1)); - - let txn_id2: &TransactionId = "tid789".into(); - let mut request2 = http::Request::default(); - request2.txn_id = Some(txn_id2.to_string()); - - sticky.maybe_apply(&mut request2, &mut LazyTransactionId::from_owned(txn_id2.to_owned())); - assert!(sticky.is_invalidated()); - // `request2` contains `r1` because the sticky parameters have not been - // committed, so it's still marked as pending. - assert_eq!(request2.room_subscriptions.len(), 1); - assert!(request2.room_subscriptions.contains_key(r1)); - - // Here we commit with the not most-recent TID, so it keeps the invalidated - // status. - sticky.maybe_commit(txn_id1); - assert!(sticky.is_invalidated()); - - // But here we use the latest TID, so the commit is effective. - sticky.maybe_commit(txn_id2); - assert!(!sticky.is_invalidated()); - } - - #[test] - fn test_room_subscriptions_are_sticky() { - let r0 = room_id!("!r0.matrix.org"); - let r1 = room_id!("!r1:matrix.org"); - - let mut sticky = - SlidingSyncStickyManager::new(SlidingSyncStickyParameters::new(BTreeMap::new())); - - // A room subscription is added, applied, and committed. + // A room subscription is added. The request is sent and received. { // Insert `r0`. - sticky - .data_mut() - .room_subscriptions - .insert(r0.to_owned(), (Default::default(), Default::default())); + sliding_sync.subscribe_to_rooms(&[r0], None, false); - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid0".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); + // The room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Pending, _)) + ); - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); + // Generate the request. + let mut transaction_id = LazyTransactionId::new(); + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); - assert!(request.txn_id.is_some()); assert_eq!(request.room_subscriptions.len(), 1); assert!(request.room_subscriptions.contains_key(r0)); - // Then the sticky parameters are committed. - let tid = request.txn_id.unwrap(); + // The room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Pending, _)) + ); - sticky.maybe_commit(tid.as_str().into()); + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos0".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // The room subscription is marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); } - // A room subscription is added, applied, but NOT committed. + // A room subscription is re-added. + { + // Insert `r0`. + sliding_sync.subscribe_to_rooms(&[r0], None, false); + + // The room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + } + + // A new room subscription is added. { // Insert `r1`. - sticky - .data_mut() - .room_subscriptions - .insert(r1.to_owned(), (Default::default(), Default::default())); + sliding_sync.subscribe_to_rooms(&[r1], None, false); - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid1".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); + // This room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + // This room subscription is marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Pending, _)) + ); - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); + // Generate the request. + let mut transaction_id = LazyTransactionId::new(); + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); - assert!(request.txn_id.is_some()); assert_eq!(request.room_subscriptions.len(), 1); - // `r0` is not present, it's only `r1`. assert!(request.room_subscriptions.contains_key(r1)); - // Then the sticky parameters are NOT committed. - // It can happen if the request has failed to be sent for example, - // or if the response didn't match. + // This room subscription is still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + // This room subscription is still marked as `Pending`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Pending, _)) + ); + + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos1".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // This room subscription is marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); } - // A previously added room subscription is re-added, applied, and committed. + // No new room subscription is added. { - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid2".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); + // Generate the request. + let mut transaction_id = LazyTransactionId::new(); + let (request, _, mut position_markers) = + sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); - assert!(request.txn_id.is_some()); - assert_eq!(request.room_subscriptions.len(), 1); - // `r0` is not present, it's only `r1`. - assert!(request.room_subscriptions.contains_key(r1)); - - // Then the sticky parameters are committed. - let tid = request.txn_id.unwrap(); - - sticky.maybe_commit(tid.as_str().into()); - } - - // All room subscriptions have been committed. - { - // Then the sticky parameters are applied. - let txn_id: &TransactionId = "tid3".into(); - let mut request = http::Request::default(); - request.txn_id = Some(txn_id.to_string()); - - sticky.maybe_apply(&mut request, &mut LazyTransactionId::from_owned(txn_id.to_owned())); - - assert!(request.txn_id.is_some()); - // All room subscriptions have been sent. assert!(request.room_subscriptions.is_empty()); + + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); + + // Receive a response (simulate the request is sent and a response is received). + sliding_sync + .handle_response( + http::Response::new("pos1".to_owned()), + &mut position_markers, + Default::default(), + ) + .await + .unwrap(); + + // Room subscriptions are still marked as `Applied`. + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r0), + Some((RoomSubscriptionState::Applied, _)) + ); + assert_matches!( + sliding_sync.inner.room_subscriptions.read().unwrap().get(r1), + Some((RoomSubscriptionState::Applied, _)) + ); } } From 43657d83025eb82ad85f2e974a29dd1c9f35d290 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 6 Jan 2026 15:52:31 +0100 Subject: [PATCH 26/36] chore(sdk): Sliding Sync list `required_state` is no longer sticky. THis patch extracts `SlidingSyncListStickyParameters::required_state` to no longer make it sticky. We are dropping sticky parameters as it's not part of the last MSC. --- crates/matrix-sdk/src/sliding_sync/list/builder.rs | 3 ++- crates/matrix-sdk/src/sliding_sync/list/mod.rs | 8 +++++++- crates/matrix-sdk/src/sliding_sync/list/sticky.rs | 11 ++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index 8f2ad1567..bbba96914 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -214,8 +214,9 @@ impl SlidingSyncListBuilder { // From the builder sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncListStickyParameters::new(self.required_state, self.filters), + SlidingSyncListStickyParameters::new(self.filters), )), + required_state: self.required_state, timeline_limit: StdRwLock::new(self.timeline_limit), name: self.name, cache_policy: self.cache_policy, diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index 654307569..4dbc0f0d4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -11,7 +11,9 @@ use std::{ use eyeball::{SharedObservable, Subscriber}; use futures_core::Stream; -use ruma::{TransactionId, api::client::sync::sync_events::v5 as http, assign}; +use ruma::{ + TransactionId, api::client::sync::sync_events::v5 as http, assign, events::StateEventType, +}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::Sender; use tracing::{instrument, warn}; @@ -222,6 +224,9 @@ pub(super) struct SlidingSyncListInner { /// knows). sticky: StdRwLock>, + /// Required states to return per room. + required_state: Vec<(StateEventType, String)>, + /// The maximum number of timeline events to query for. timeline_limit: StdRwLock, @@ -308,6 +313,7 @@ impl SlidingSyncListInner { let mut request = assign!(http::request::List::default(), { ranges }); request.room_details.timeline_limit = (*self.timeline_limit.read().unwrap()).into(); + request.room_details.required_state = self.required_state.clone(); { let mut sticky = self.sticky.write().unwrap(); diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs index 960afa400..1dc661787 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs @@ -6,21 +6,15 @@ use crate::sliding_sync::sticky_parameters::StickyData; /// defined by the [Sliding Sync MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). #[derive(Debug)] pub(super) struct SlidingSyncListStickyParameters { - /// Required states to return per room. - required_state: Vec<(StateEventType, String)>, - /// Any filters to apply to the query. filters: Option, } impl SlidingSyncListStickyParameters { - pub fn new( - required_state: Vec<(StateEventType, String)>, - filters: Option, - ) -> Self { + pub fn new(filters: Option) -> Self { // Consider that each list will have at least one parameter set, so invalidate // it by default. - Self { required_state, filters } + Self { filters } } } @@ -28,7 +22,6 @@ impl StickyData for SlidingSyncListStickyParameters { type Request = http::request::List; fn apply(&self, request: &mut Self::Request) { - request.room_details.required_state = self.required_state.to_vec(); request.filters = self.filters.clone(); } } From 742b6e52002059e3675b5c1b4876a04a5db81268 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 6 Jan 2026 15:54:43 +0100 Subject: [PATCH 27/36] chore(sdk): Sliding Sync list `filters` is no longer sticky. This patch extracts `SlidingSyncListStickyParameters::filters` to no longer make it sticky. We are dropping sticky parameters as it's not part of the last MSC. --- .../matrix-sdk/src/sliding_sync/list/builder.rs | 3 ++- crates/matrix-sdk/src/sliding_sync/list/mod.rs | 4 ++++ crates/matrix-sdk/src/sliding_sync/list/sticky.rs | 15 ++++----------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index bbba96914..8446c8f34 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -214,8 +214,9 @@ impl SlidingSyncListBuilder { // From the builder sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncListStickyParameters::new(self.filters), + SlidingSyncListStickyParameters::new(), )), + filters: self.filters, required_state: self.required_state, timeline_limit: StdRwLock::new(self.timeline_limit), name: self.name, diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index 4dbc0f0d4..d38b6e57c 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -224,6 +224,9 @@ pub(super) struct SlidingSyncListInner { /// knows). sticky: StdRwLock>, + /// Any filters to apply to the query. + filters: Option, + /// Required states to return per room. required_state: Vec<(StateEventType, String)>, @@ -313,6 +316,7 @@ impl SlidingSyncListInner { let mut request = assign!(http::request::List::default(), { ranges }); request.room_details.timeline_limit = (*self.timeline_limit.read().unwrap()).into(); + request.filters = self.filters.clone(); request.room_details.required_state = self.required_state.clone(); { diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs index 1dc661787..153011e08 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs @@ -5,23 +5,16 @@ use crate::sliding_sync::sticky_parameters::StickyData; /// The set of `SlidingSyncList` request parameters that are *sticky*, as /// defined by the [Sliding Sync MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). #[derive(Debug)] -pub(super) struct SlidingSyncListStickyParameters { - /// Any filters to apply to the query. - filters: Option, -} +pub(super) struct SlidingSyncListStickyParameters {} impl SlidingSyncListStickyParameters { - pub fn new(filters: Option) -> Self { - // Consider that each list will have at least one parameter set, so invalidate - // it by default. - Self { filters } + pub fn new() -> Self { + Self } } impl StickyData for SlidingSyncListStickyParameters { type Request = http::request::List; - fn apply(&self, request: &mut Self::Request) { - request.filters = self.filters.clone(); - } + fn apply(&self, request: &mut Self::Request) {} } From 4ac4ad3440567a28ab3ed31690fc536c5040fc8d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Jan 2026 09:39:25 +0100 Subject: [PATCH 28/36] chore(sdk): Remove the `sticky_parameters` module. This patch removes the `sticky_parameters` module, which is now unused. --- .../src/sliding_sync/list/builder.rs | 14 +- .../matrix-sdk/src/sliding_sync/list/mod.rs | 53 +---- .../src/sliding_sync/list/sticky.rs | 20 -- crates/matrix-sdk/src/sliding_sync/mod.rs | 102 +++------ .../src/sliding_sync/sticky_parameters.rs | 194 ------------------ 5 files changed, 43 insertions(+), 340 deletions(-) delete mode 100644 crates/matrix-sdk/src/sliding_sync/list/sticky.rs delete mode 100644 crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs diff --git a/crates/matrix-sdk/src/sliding_sync/list/builder.rs b/crates/matrix-sdk/src/sliding_sync/list/builder.rs index 8446c8f34..3ba5895f4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/builder.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/builder.rs @@ -11,14 +11,11 @@ use ruma::{api::client::sync::sync_events::v5 as http, events::StateEventType}; use tokio::sync::broadcast::Sender; use super::{ - super::SlidingSyncInternalMessage, Bound, SlidingSyncList, SlidingSyncListCachePolicy, - SlidingSyncListInner, SlidingSyncListLoadingState, SlidingSyncListRequestGenerator, - SlidingSyncListStickyParameters, SlidingSyncMode, -}; -use crate::{ - Client, - sliding_sync::{cache::restore_sliding_sync_list, sticky_parameters::SlidingSyncStickyManager}, + super::{SlidingSyncInternalMessage, cache::restore_sliding_sync_list}, + Bound, SlidingSyncList, SlidingSyncListCachePolicy, SlidingSyncListInner, + SlidingSyncListLoadingState, SlidingSyncListRequestGenerator, SlidingSyncMode, }; +use crate::Client; /// Data that might have been read from the cache. #[derive(Clone)] @@ -213,9 +210,6 @@ impl SlidingSyncListBuilder { sync_mode: StdRwLock::new(self.sync_mode.clone()), // From the builder - sticky: StdRwLock::new(SlidingSyncStickyManager::new( - SlidingSyncListStickyParameters::new(), - )), filters: self.filters, required_state: self.required_state, timeline_limit: StdRwLock::new(self.timeline_limit), diff --git a/crates/matrix-sdk/src/sliding_sync/list/mod.rs b/crates/matrix-sdk/src/sliding_sync/list/mod.rs index d38b6e57c..2fe7441e4 100644 --- a/crates/matrix-sdk/src/sliding_sync/list/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/list/mod.rs @@ -1,7 +1,6 @@ mod builder; mod frozen; mod request_generator; -mod sticky; use std::{ fmt, @@ -11,20 +10,14 @@ use std::{ use eyeball::{SharedObservable, Subscriber}; use futures_core::Stream; -use ruma::{ - TransactionId, api::client::sync::sync_events::v5 as http, assign, events::StateEventType, -}; +use ruma::{api::client::sync::sync_events::v5 as http, assign, events::StateEventType}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::Sender; use tracing::{instrument, warn}; pub use self::builder::*; -use self::sticky::SlidingSyncListStickyParameters; pub(super) use self::{frozen::FrozenSlidingSyncList, request_generator::*}; -use super::{ - Error, SlidingSyncInternalMessage, - sticky_parameters::{LazyTransactionId, SlidingSyncStickyManager}, -}; +use super::{Error, SlidingSyncInternalMessage}; use crate::Result; /// Should this [`SlidingSyncList`] be stored in the cache, and automatically @@ -151,11 +144,8 @@ impl SlidingSyncList { /// /// The next request is entirely calculated based on the request generator /// ([`SlidingSyncListRequestGenerator`]). - pub(super) fn next_request( - &self, - txn_id: &mut LazyTransactionId, - ) -> Result { - self.inner.next_request(txn_id) + pub(super) fn next_request(&self) -> Result { + self.inner.next_request() } /// Returns the current cache policy for this list. @@ -183,17 +173,6 @@ impl SlidingSyncList { Ok(new_changes) } - /// Commit the set of sticky parameters for this list. - pub fn maybe_commit_sticky(&mut self, txn_id: &TransactionId) { - self.inner.sticky.write().unwrap().maybe_commit(txn_id); - } - - /// Manually invalidate the sticky data, so the sticky parameters are - /// re-sent next time. - pub(super) fn invalidate_sticky_data(&self) { - let _ = self.inner.sticky.write().unwrap().data_mut(); - } - /// Get the sync-mode. #[cfg(feature = "testing")] pub fn sync_mode(&self) -> SlidingSyncMode { @@ -219,11 +198,6 @@ pub(super) struct SlidingSyncListInner { #[cfg(target_family = "wasm")] requires_timeout: Arc bool>, - /// Parameters that are sticky, and can be sent only once per session (until - /// the connection is dropped or the server invalidates what the client - /// knows). - sticky: StdRwLock>, - /// Any filters to apply to the query. filters: Option, @@ -297,7 +271,7 @@ impl SlidingSyncListInner { } /// Update the state to the next request, and return it. - fn next_request(&self, txn_id: &mut LazyTransactionId) -> Result { + fn next_request(&self) -> Result { let ranges = { // Use a dedicated scope to ensure the lock is released before continuing. let mut request_generator = self.request_generator.write().unwrap(); @@ -305,13 +279,13 @@ impl SlidingSyncListInner { }; // Here we go. - Ok(self.request(ranges, txn_id)) + Ok(self.request(ranges)) } /// Build a [`http::request::List`] based on the current state of the /// request generator. #[instrument(skip(self), fields(name = self.name))] - fn request(&self, ranges: Ranges, txn_id: &mut LazyTransactionId) -> http::request::List { + fn request(&self, ranges: Ranges) -> http::request::List { let ranges = ranges.into_iter().map(|r| ((*r.start()).into(), (*r.end()).into())).collect(); let mut request = assign!(http::request::List::default(), { ranges }); @@ -319,11 +293,6 @@ impl SlidingSyncListInner { request.filters = self.filters.clone(); request.room_details.required_state = self.required_state.clone(); - { - let mut sticky = self.sticky.write().unwrap(); - sticky.maybe_apply(&mut request, txn_id); - } - request } @@ -544,7 +513,7 @@ mod tests { use tokio::sync::broadcast::{channel, error::TryRecvError}; use super::{SlidingSyncList, SlidingSyncListLoadingState, SlidingSyncMode}; - use crate::sliding_sync::{SlidingSyncInternalMessage, sticky_parameters::LazyTransactionId}; + use crate::sliding_sync::SlidingSyncInternalMessage; macro_rules! assert_json_roundtrip { (from $type:ty: $rust_value:expr => $json_value:expr) => { @@ -635,7 +604,7 @@ mod tests { $( { // Generate a new request. - let request = $list.next_request(&mut LazyTransactionId::new()).unwrap(); + let request = $list.next_request().unwrap(); assert_eq!( request.ranges, @@ -1146,7 +1115,7 @@ mod tests { assert!(list.maximum_number_of_rooms().is_none()); // Simulate a request. - let _ = list.next_request(&mut LazyTransactionId::new()); + let _ = list.next_request(); let new_changes = list.update(Some(5)).unwrap(); assert!(new_changes); @@ -1154,7 +1123,7 @@ mod tests { assert_eq!(list.maximum_number_of_rooms(), Some(5)); // Simulate another request. - let _ = list.next_request(&mut LazyTransactionId::new()); + let _ = list.next_request(); let new_changes = list.update(Some(5)).unwrap(); assert!(!new_changes); diff --git a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs b/crates/matrix-sdk/src/sliding_sync/list/sticky.rs deleted file mode 100644 index 153011e08..000000000 --- a/crates/matrix-sdk/src/sliding_sync/list/sticky.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ruma::{api::client::sync::sync_events::v5 as http, events::StateEventType}; - -use crate::sliding_sync::sticky_parameters::StickyData; - -/// The set of `SlidingSyncList` request parameters that are *sticky*, as -/// defined by the [Sliding Sync MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). -#[derive(Debug)] -pub(super) struct SlidingSyncListStickyParameters {} - -impl SlidingSyncListStickyParameters { - pub fn new() -> Self { - Self - } -} - -impl StickyData for SlidingSyncListStickyParameters { - type Request = http::request::List; - - fn apply(&self, request: &mut Self::Request) {} -} diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 8aec0d883..3e1d064aa 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -20,7 +20,6 @@ mod cache; mod client; mod error; mod list; -mod sticky_parameters; use std::{ collections::{BTreeMap, btree_map::Entry}, @@ -49,10 +48,7 @@ use tokio::{ use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; pub use self::{builder::*, client::VersionBuilderError, error::*, list::*}; -use self::{ - cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor, - sticky_parameters::LazyTransactionId, -}; +use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor}; use crate::{Client, Result, config::RequestConfig}; /// The Sliding Sync instance. @@ -309,25 +305,15 @@ impl SlidingSync { debug!("Sliding Sync response has been handled by the client"); trace!(?sync_response); - // Commit sticky parameters, if needed. + // All room subscriptions are marked as `Applied`. { - // All room subscriptions are marked as `Applied`. - { - let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap(); + let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap(); - for (state, _room_subscription) in room_subscriptions.values_mut() { - if matches!(state, RoomSubscriptionState::Pending) { - *state = RoomSubscriptionState::Applied; - } + for (state, _room_subscription) in room_subscriptions.values_mut() { + if matches!(state, RoomSubscriptionState::Pending) { + *state = RoomSubscriptionState::Applied; } } - - // Commit lists. - if let Some(ref txn_id) = sliding_sync_response.txn_id { - let txn_id = txn_id.as_str().into(); - let mut lists = self.inner.lists.write().await; - lists.values_mut().for_each(|list| list.maybe_commit_sticky(txn_id)); - } } let update_summary = { @@ -402,7 +388,6 @@ impl SlidingSync { #[instrument(skip_all)] async fn generate_sync_request( &self, - txn_id: &mut LazyTransactionId, ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard)> { // Collect requests for lists. let mut requests_lists = BTreeMap::new(); @@ -414,7 +399,7 @@ impl SlidingSync { let mut require_timeout = true; for (name, list) in lists.iter() { - requests_lists.insert(name.clone(), list.next_request(txn_id)?); + requests_lists.insert(name.clone(), list.next_request()?); require_timeout = require_timeout && list.requires_timeout(); } @@ -521,11 +506,6 @@ impl SlidingSync { restored_fields.and_then(|fields| fields.to_device_token); } - // Apply the transaction id if one was generated. - if let Some(txn_id) = txn_id.get() { - request.txn_id = Some(txn_id.to_string()); - } - Ok(( // The request itself. request, @@ -685,8 +665,7 @@ impl SlidingSync { #[doc(hidden)] #[instrument(skip_all, fields(pos, conn_id = self.inner.id))] pub async fn sync_once(&self) -> Result { - let (request, request_config, position_guard) = - self.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, request_config, position_guard) = self.generate_sync_request().await?; // Send the request. let summaries = self.send_sync_request(request, request_config, position_guard).await?; @@ -791,12 +770,10 @@ impl SlidingSync { { let lists = self.inner.lists.read().await; + for list in lists.values() { // Invalidate in-memory data that would be persisted on disk. list.set_maximum_number_of_rooms(None); - - // Invalidate the sticky data for this list. - list.invalidate_sticky_data(); } } @@ -917,7 +894,7 @@ mod tests { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory}; use ruma::{ - OwnedRoomId, TransactionId, assign, + OwnedRoomId, assign, events::{direct::DirectEvent, room::member::MembershipState}, owned_room_id, room_id, serde::Raw, @@ -932,7 +909,6 @@ mod tests { use super::{ RoomSubscriptionState, SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http, - sticky_parameters::LazyTransactionId, }; use crate::{ Client, Result, @@ -1193,9 +1169,8 @@ mod tests { ); // Generate the request. - let mut transaction_id = LazyTransactionId::new(); let (request, _, mut position_markers) = - sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); + sliding_sync.generate_sync_request().await.unwrap(); assert_eq!(request.room_subscriptions.len(), 1); assert!(request.room_subscriptions.contains_key(r0)); @@ -1252,9 +1227,8 @@ mod tests { ); // Generate the request. - let mut transaction_id = LazyTransactionId::new(); let (request, _, mut position_markers) = - sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); + sliding_sync.generate_sync_request().await.unwrap(); assert_eq!(request.room_subscriptions.len(), 1); assert!(request.room_subscriptions.contains_key(r1)); @@ -1300,9 +1274,8 @@ mod tests { ); // Generate the request. - let mut transaction_id = LazyTransactionId::new(); let (request, _, mut position_markers) = - sliding_sync.generate_sync_request(&mut transaction_id).await.unwrap(); + sliding_sync.generate_sync_request().await.unwrap(); assert!(request.room_subscriptions.is_empty()); @@ -1425,10 +1398,7 @@ mod tests { .await?; // First request: no `pos`. - let txn_id = TransactionId::new(); - let (_request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; + let (_request, _, _) = sync.generate_sync_request().await?; // Now, tracked users must be dirty. { @@ -1466,10 +1436,7 @@ mod tests { // Second request: with a `pos` this time. sync.set_pos("chocolat".to_owned()).await; - let txn_id = TransactionId::new(); - let (_request, _, _) = sync - .generate_sync_request(&mut LazyTransactionId::from_owned(txn_id.to_owned())) - .await?; + let (_request, _, _) = sync.generate_sync_request().await?; // Tracked users are not marked as dirty. { @@ -1523,8 +1490,7 @@ mod tests { { assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -1561,8 +1527,7 @@ mod tests { // It's still 0, not "yolo". { assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0")); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("0")); } @@ -1612,8 +1577,7 @@ mod tests { // `pos` is `None` to start with. { - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); assert!(sliding_sync.inner.position.lock().await.pos.is_none()); @@ -1649,8 +1613,7 @@ mod tests { // It's alright, the next request will load it from the database. { - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("42")); assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42")); } @@ -1660,8 +1623,7 @@ mod tests { let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?; assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42")); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert_eq!(request.pos.as_deref(), Some("42")); } @@ -1672,8 +1634,7 @@ mod tests { { assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -1682,8 +1643,7 @@ mod tests { let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?; assert!(sliding_sync.inner.position.lock().await.pos.is_none()); - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; assert!(request.pos.is_none()); } @@ -2312,8 +2272,7 @@ mod tests { async fn test_timeout_zero_list() -> Result<()> { let (_server, sliding_sync) = new_sliding_sync(vec![]).await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // Zero list means sliding sync is fully loaded, so there is a timeout to wait // on new update to pop. @@ -2329,8 +2288,7 @@ mod tests { ]) .await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // The list does not require a timeout. assert!(request.timeout.is_none()); @@ -2358,8 +2316,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // The list is now fully loaded, so it requires a timeout. assert!(request.timeout.is_some()); @@ -2377,8 +2334,7 @@ mod tests { ]) .await?; - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // Two lists don't require a timeout. assert!(request.timeout.is_none()); @@ -2406,8 +2362,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // One don't require a timeout. assert!(request.timeout.is_none()); @@ -2435,8 +2390,7 @@ mod tests { }; } - let (request, _, _) = - sliding_sync.generate_sync_request(&mut LazyTransactionId::new()).await?; + let (request, _, _) = sliding_sync.generate_sync_request().await?; // All lists require a timeout. assert!(request.timeout.is_some()); diff --git a/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs b/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs deleted file mode 100644 index acacbc0ee..000000000 --- a/crates/matrix-sdk/src/sliding_sync/sticky_parameters.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Sticky parameters are a way to spare bandwidth on the network, by sending -//! request parameters once and have the server remember them. -//! -//! The set of sticky parameters have to be agreed upon by the server and the -//! client; this is defined in the -//! [MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md). - -use ruma::{OwnedTransactionId, TransactionId}; - -/// An `OwnedTransactionId` that is either initialized at creation, or -/// lazily-generated once. -#[derive(Debug)] -pub struct LazyTransactionId { - txn_id: Option, -} - -impl LazyTransactionId { - /// Create a new `LazyTransactionId`, not set. - pub fn new() -> Self { - Self { txn_id: None } - } - - /// Get (or create it, if never set) a `TransactionId`. - pub fn get_or_create(&mut self) -> &TransactionId { - self.txn_id.get_or_insert_with(TransactionId::new) - } - - /// Attempt to get the underlying `TransactionId` without creating it, if - /// missing. - pub fn get(&self) -> Option<&TransactionId> { - self.txn_id.as_deref() - } -} - -#[cfg(test)] -impl LazyTransactionId { - /// Create a `LazyTransactionId` for a given known transaction id. For - /// testing only. - pub fn from_owned(owned: OwnedTransactionId) -> Self { - Self { txn_id: Some(owned) } - } -} - -/// A trait to implement for data that can be sticky, given a context. -pub trait StickyData { - /// Request type that will be applied to, if the sticky parameters have been - /// invalidated before. - type Request; - - /// Apply the current data onto the request. - fn apply(&self, request: &mut Self::Request); - - /// When the current are committed, i.e. when the request has been validated - /// by a response. - fn on_commit(&mut self) { - // noop - } -} - -/// Helper data structure to manage sticky parameters, for any kind of data. -/// -/// Initially, the provided data is considered to be invalidated, so it's -/// applied onto the request the first time it's sent. Any changes to the -/// wrapped data happen via [`Self::data_mut`], which invalidates the sticky -/// parameters; they will be applied automatically to the next request. -/// -/// When applying sticky parameters, we will also remember the transaction id -/// that was generated for us, stash it, so we can match the response against -/// the transaction id later, and only consider the data isn't invalidated -/// anymore (we say it's "committed" in that case) if the response's transaction -/// id match what we expect. -#[derive(Debug)] -pub struct SlidingSyncStickyManager { - /// The data managed by this sticky manager. - data: D, - - /// Was any of the parameters invalidated? If yes, reinitialize them. - invalidated: bool, - - /// If the sticky parameters were applied to a given request, this is - /// the transaction id generated for that request, that must be matched - /// upon in the next call to `commit()`. - txn_id: Option, -} - -impl SlidingSyncStickyManager { - /// Create a new `StickyManager` for the given data. - /// - /// Always assume the initial data invalidates the request, at first. - pub fn new(data: D) -> Self { - Self { data, txn_id: None, invalidated: true } - } - - /// Get a mutable reference to the managed data. - /// - /// Will invalidate the sticky set by default. If you don't need to modify - /// the data, use `Self::data()`; if you're not sure you're going to modify - /// the data, it's best to first use `Self::data()` then `Self::data_mut()` - /// when you're sure. - pub fn data_mut(&mut self) -> &mut D { - self.invalidated = true; - &mut self.data - } - - /// Returns a non-invalidating reference to the managed data. - pub fn data(&self) -> &D { - &self.data - } - - /// May apply some the managed sticky parameters to the given request. - /// - /// After receiving the response from this sliding sync, the caller MUST - /// also call [`Self::maybe_commit`] with the transaction id from the - /// server's response. - /// - /// If no `txn_id` is provided, it will generate one that can be reused - /// later. - pub fn maybe_apply(&mut self, req: &mut D::Request, txn_id: &mut LazyTransactionId) { - if self.invalidated { - let txn_id = txn_id.get_or_create(); - self.txn_id = Some(txn_id.to_owned()); - self.data.apply(req); - } - } - - /// May mark the managed data as not invalidated anymore, if the transaction - /// id received from the response matches the one received from the request. - pub fn maybe_commit(&mut self, txn_id: &TransactionId) { - if self.invalidated && self.txn_id.as_deref() == Some(txn_id) { - self.invalidated = false; - self.data.on_commit(); - } - } - - #[cfg(test)] - pub fn is_invalidated(&self) -> bool { - self.invalidated - } -} - -#[cfg(test)] -mod tests { - use super::{LazyTransactionId, SlidingSyncStickyManager, StickyData}; - - struct EmptyStickyData(u8); - - impl StickyData for EmptyStickyData { - type Request = bool; - - fn apply(&self, req: &mut Self::Request) { - // Mark that applied has had an effect. - *req = true; - } - - fn on_commit(&mut self) { - self.0 += 1; - } - } - - #[test] - fn test_sticky_parameters_api_non_invalidated_no_effect() { - let mut sticky = SlidingSyncStickyManager::new(EmptyStickyData(0)); - - // At first, it's always invalidated. - assert!(sticky.is_invalidated()); - - let mut applied = false; - let mut txn_id = LazyTransactionId::new(); - sticky.maybe_apply(&mut applied, &mut txn_id); - assert!(applied); - assert!(sticky.is_invalidated()); - assert!(txn_id.get().is_some(), "a transaction id was lazily generated"); - - // Committing with the wrong transaction id won't commit. - sticky.maybe_commit("tid456".into()); - assert_eq!(sticky.data.0, 0); - assert!(sticky.is_invalidated()); - - // Providing the correct transaction id will commit. - sticky.maybe_commit(txn_id.get().unwrap()); - assert_eq!(sticky.data.0, 1); - assert!(!sticky.is_invalidated()); - - // Applying without being invalidated won't do anything, and not generate a - // transaction id. - let mut txn_id = LazyTransactionId::new(); - let mut applied = false; - sticky.maybe_apply(&mut applied, &mut txn_id); - - assert!(!applied); - assert!(!sticky.is_invalidated()); - assert!(txn_id.get().is_none()); - } -} From 87962d10e23c118b13e2837ffdfaa4ff6b3e0360 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Jan 2026 10:22:06 +0100 Subject: [PATCH 29/36] test(sdk): Test `Request::extensions::to_device::since` is set. This patch restores a couple of assertions from a recently removed test where it is asserted that `Request::extensions::to_device::since` is set from the Olm machine. --- crates/matrix-sdk/src/sliding_sync/mod.rs | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 3e1d064aa..44b90925b 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -1148,6 +1148,71 @@ mod tests { Ok(()) } + #[cfg(feature = "e2e-encryption")] + #[async_test] + async fn test_extensions_to_device_since_is_set() { + use matrix_sdk_base::crypto::store::types::Changes; + + let client = logged_in_client(None).await; + let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone()) + .unwrap() + .with_to_device_extension(assign!( + http::request::ToDevice::default(), + { + enabled: Some(true), + } + )) + .build() + .await + .unwrap(); + + // Test `SlidingSyncInner::extensions`. + { + let to_device = &sliding_sync.inner.extensions.to_device; + + assert_eq!(to_device.enabled, Some(true)); + assert!(to_device.since.is_none()); + } + + // Test `Request::extensions`. + { + let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap(); + + let to_device = &request.extensions.to_device; + + assert_eq!(to_device.enabled, Some(true)); + assert!(to_device.since.is_none()); + } + + // Define a `since` token. + let since_token = "depuis".to_owned(); + + { + if let Some(olm_machine) = &*client.olm_machine().await { + olm_machine + .store() + .save_changes(Changes { + next_batch_token: Some(since_token.clone()), + ..Default::default() + }) + .await + .unwrap(); + } else { + panic!("Where is the Olm machine?"); + } + } + + // Test `Request::extensions` again. + { + let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap(); + + let to_device = &request.extensions.to_device; + + assert_eq!(to_device.enabled, Some(true)); + assert_eq!(to_device.since, Some(since_token)); + } + } + #[async_test] async fn test_room_subscriptions_are_sticky() { let r0 = room_id!("!r0.matrix.org"); From ed1c847e7bcc1edd8b89bf53cbbfb7713d674f46 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Jan 2026 10:32:30 +0100 Subject: [PATCH 30/36] doc(sdk): Update the `CHANGELOG.md. --- crates/matrix-sdk/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index e8523138e..c18effa4f 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -26,6 +26,8 @@ All notable changes to this project will be documented in this file. - Replace in-memory stores with IndexedDB implementations when initializing `Client` with `BuilderStoreConfig::IndexedDb`. [#5946](https://github.com/matrix-org/matrix-rust-sdk/pull/5946) +- Sliding Sync room subscriptions are sent once to save bandwidth. + ([#6002](https://github.com/matrix-org/matrix-rust-sdk/pull/6002)) ### Bugfix From 5dcd877dcdf60bd48ee45a8c73e3adf4130d407d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Wed, 7 Jan 2026 10:56:29 +0100 Subject: [PATCH 31/36] test(ui): Sticky parameters have been removed. --- .../integration/encryption_sync_service.rs | 71 +++++++++++-------- .../tests/integration/room_list_service.rs | 27 +++++-- .../tests/integration/sliding_sync.rs | 5 +- crates/matrix-sdk/src/sliding_sync/mod.rs | 9 +-- 4 files changed, 72 insertions(+), 40 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 c0a698245..30af36bc7 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -47,14 +47,14 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { // Requests enable the e2ee and to_device extensions on the first run. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "e2ee": { - "enabled": true + "enabled": true, }, "to_device": { - "enabled": true + "enabled": true, } } }, @@ -64,33 +64,39 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { }; // The request then passes the `pos`ition marker to the next request, as usual - // in sliding sync. The extensions haven't changed, so they're not updated - // (sticky parameters ftw). + // in sliding sync. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", + "extensions": { + "e2ee": { + "enabled": true, + }, + "to_device": { + "enabled": true, + } + } }, respond with = { "pos": "1", "extensions": { "to_device": { - "next_batch": "nb0" + "next_batch": "nb0", } } }, }; // The to-device since token is passed from the previous request. - // The extensions haven't changed, so they're not updated (sticky parameters - // ftw). sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { - "since": "nb0" + "enabled": true, + "since": "nb0", } } }, @@ -105,18 +111,15 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { }; // The to-device since token is passed from the previous request. - // The extensions haven't changed, so they're not updated (sticky parameters - // ftw)... in the first request. Then, the sliding sync instance will retry - // those requests, so it will include them again; as a matter of fact, the - // last request that we assert against will contain those. sliding_sync_then_assert_request_and_fake_response! { [server, stream] sync matches Some(Err(_)), - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { - "since": "nb1" + "enabled": true, + "since": "nb1", } } }, @@ -134,15 +137,12 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> { let stream = encryption_sync.sync(sync_permit_guard); pin_mut!(stream); - // The next request will contain sticky parameters again. + // The next request will contain extensions again. sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { - "e2ee": { - "enabled": true - }, "to_device": { "enabled": true, "since": "nb1" @@ -219,8 +219,6 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> { encryption_sync.run_fixed_iterations(2, sync_permit_guard).await?; - // First iteration fills the whole request. - // Second iteration only sends non-sticky parameters. let expected_requests = [ json!({ "conn_id": "encryption", @@ -235,6 +233,14 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> { }), json!({ "conn_id": "encryption", + "extensions": { + "e2ee": { + "enabled": true + }, + "to_device": { + "enabled": true + } + } }), ]; @@ -280,14 +286,17 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< }, }; - // Second iteration only sends non-sticky parameters, plus the to-device token - // from the previous request. + // Second iteration contains the to-device token from the previous request. sliding_sync_then_assert_request_and_fake_response! { [server, stream] assert request = { "conn_id": "encryption", "extensions": { + "e2ee": { + "enabled": true + }, "to_device": { + "enabled": true, "since": "nb0", }, } @@ -321,7 +330,11 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result< assert request = { "conn_id": "encryption", "extensions": { + "e2ee": { + "enabled": true + }, "to_device": { + "enabled": true, "since": "nb2", }, } @@ -453,7 +466,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { @@ -504,7 +517,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { @@ -529,7 +542,7 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a sliding_sync_then_assert_request_and_fake_response! { [server, stream] - assert request = { + assert request >= { "conn_id": "encryption", "extensions": { "to_device": { diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 82d0eeb95..7dddc47e7 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -417,7 +417,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("0"), // Still no long-polling because the list isn't fully-loaded. assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -445,7 +445,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("1"), // Still no long-polling because the list isn't fully-loaded. assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -474,7 +474,7 @@ async fn test_sync_all_states() -> Result<(), Error> { // Still no long-polling because the list isn't fully-loaded, // but it's about to be! assert timeout None, - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -502,7 +502,7 @@ async fn test_sync_all_states() -> Result<(), Error> { assert pos Some("3"), // The list is fully-loaded, we can start long-polling. assert timeout Some(30000), - assert request = { + assert request >= { "conn_id": "room-list", "lists": { ALL_ROOMS: { @@ -2414,6 +2414,25 @@ async fn test_room_subscription() -> Result<(), Error> { "lists": { ALL_ROOMS: { "ranges": [[0, 2]], + "required_state": [ + ["m.room.name", ""], + ["m.room.encryption", ""], + ["m.room.member", "$LAZY"], + ["m.room.member", "$ME"], + ["m.room.topic", ""], + ["m.room.avatar", ""], + ["m.room.canonical_alias", ""], + ["m.room.power_levels", ""], + ["org.matrix.msc3401.call.member", "*"], + ["m.room.join_rules", ""], + ["m.room.tombstone", ""], + ["m.room.create", ""], + ["m.room.history_visibility", ""], + ["io.element.functional_members", ""], + ["m.space.parent", "*"], + ["m.space.child", "*"], + ], + "filters": {}, "timeline_limit": 1, }, }, diff --git a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs index e31ccf3d7..81a10f989 100644 --- a/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/sliding_sync.rs @@ -27,7 +27,10 @@ pub(crate) async fn check_requests(server: MockServer, expected_requests: &[serd &expected_requests[num_requests], assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict), ) { - panic!("{error}\n\njson_value = {json_value:?}"); + panic!( + "{error}\n\nexpected_requests[{num_requests}] = {expected_request}\n\njson_value = {json_value:?}", + expected_request = expected_requests[num_requests], + ); } num_requests += 1; diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index 44b90925b..866eeb6ca 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -498,8 +498,6 @@ impl SlidingSync { // Add extensions. request.extensions = self.inner.extensions.clone(); - // Extensions are now applied (via sticky parameters). - // // Override the to-device token if the extension is enabled. if to_device_enabled { request.extensions.to_device.since = @@ -723,7 +721,7 @@ impl SlidingSync { // Here, errors we **cannot** ignore, and that must stop the sync loop. Err(error) => { if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) { - // The Sliding Sync session has expired. Let's reset `pos` and sticky parameters. + // The Sliding Sync session has expired. Let's reset `pos`. self.expire_session().await; } @@ -755,8 +753,7 @@ impl SlidingSync { /// Expire the current Sliding Sync session on the client-side. /// - /// Expiring a Sliding Sync session means: resetting `pos`. It also resets - /// sticky parameters. + /// Expiring a Sliding Sync session means: resetting `pos`. /// /// This should only be used when it's clear that this session was about to /// expire anyways, and should be used only in very specific cases (e.g. @@ -766,7 +763,7 @@ impl SlidingSync { /// This method **MUST** be called when the sync loop is stopped. #[doc(hidden)] pub async fn expire_session(&self) { - info!("Session expired; resetting `pos` and sticky parameters"); + info!("Session expired; resetting `pos`"); { let lists = self.inner.lists.read().await; From 3be6fb1a809013f9915b6fc2fca015b46c4a2ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 12:14:00 +0100 Subject: [PATCH 32/36] fix(sqlite): Add WAL checkpoints when vacuuming For some reason, the automatic WAL checkpoints don't seem to be working as expected. Since we should periodically run VACUUM operations, we might as well add checkpoints before vacuuming (so the WAL size is reset and can grow to fit the whole DB) and after (so we clean up after that). --- crates/matrix-sdk-sqlite/src/utils.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index d9265a77c..ce10de06e 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -187,6 +187,7 @@ pub(crate) trait SqliteAsyncConnExt { /// /// Only returns an error in tests, otherwise the error is only logged. async fn vacuum(&self) -> Result<()> { + self.wal_checkpoint().await; if let Err(error) = self.execute_batch("VACUUM").await { // Since this is an optimisation step, do not propagate the error // but log it. @@ -198,11 +199,19 @@ pub(crate) trait SqliteAsyncConnExt { return Err(error.into()); } else { trace!("VACUUM complete"); + self.wal_checkpoint().await; } Ok(()) } + async fn wal_checkpoint(&self) { + match self.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);").await { + Ok(_) => trace!("WAL checkpoint completed"), + Err(error) => error!(?error, "WAL checkpoint error"), + } + } + async fn get_db_size(&self) -> Result { let page_size = self.query_row("PRAGMA page_size;", (), |row| row.get::<_, usize>(0)).await?; From b5a0042e14bacdeeb1973f927097ca9514ccf6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 13:17:14 +0100 Subject: [PATCH 33/36] fix(sqlite): Add WAL checkpoints to the DBs when they're opened too --- crates/matrix-sdk-sqlite/src/crypto_store.rs | 2 ++ crates/matrix-sdk-sqlite/src/event_cache_store.rs | 3 +++ crates/matrix-sdk-sqlite/src/media_store.rs | 2 ++ crates/matrix-sdk-sqlite/src/state_store.rs | 2 ++ 4 files changed, 9 insertions(+) diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 28fecd934..ef24833fb 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -125,6 +125,8 @@ impl SqliteCryptoStore { debug!("Opened sqlite store with version {}", version); run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index bad1489a5..73eee0a9e 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -149,8 +149,11 @@ impl SqliteEventCacheStore { let conn = pool.get().await?; let version = conn.db_version().await?; + run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/media_store.rs b/crates/matrix-sdk-sqlite/src/media_store.rs index 9584b0365..ae6c229bd 100644 --- a/crates/matrix-sdk-sqlite/src/media_store.rs +++ b/crates/matrix-sdk-sqlite/src/media_store.rs @@ -143,6 +143,8 @@ impl SqliteMediaStore { let version = conn.db_version().await?; run_migrations(&conn, version).await?; + conn.wal_checkpoint().await; + let store_cipher = match secret { Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s).await?)), None => None, diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 5dbc235b9..a760781d6 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -160,6 +160,8 @@ impl SqliteStateStore { }; this.run_migrations(version, None).await?; + this.read().await?.wal_checkpoint().await; + Ok(this) } From c65026b70a4a28064525895fd68e93c421634c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 13:22:01 +0100 Subject: [PATCH 34/36] doc: Add changelog entry --- crates/matrix-sdk/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index c18effa4f..473460f76 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to this project will be documented in this file. ### Bugfix +- Add manual WAL checkpoints when opening Sqlite DBs and when vacuuming them, since the WAL files aren't automatically shrinking. ([#6004](https://github.com/matrix-org/matrix-rust-sdk/pull/6004)) - Use the server name extracted from the user id in `Client::fetch_client_well_known` as a fallback value. Otherwise, sometimes the server name is not available and we can't reload the well-known contents. ([#5996](https://github.com/matrix-org/matrix-rust-sdk/pull/5996)) - Latest Event is lazier: a `RoomLatestEvents` can be registered even if its associated `RoomEventCache` isn't created yet. From 6b0c1e2992c42ed6de8bcb78a7c04b56c92177e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 7 Jan 2026 15:10:34 +0100 Subject: [PATCH 35/36] doc: Add doc and inline comments --- crates/matrix-sdk-sqlite/src/utils.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-sqlite/src/utils.rs b/crates/matrix-sdk-sqlite/src/utils.rs index ce10de06e..06a33a562 100644 --- a/crates/matrix-sdk-sqlite/src/utils.rs +++ b/crates/matrix-sdk-sqlite/src/utils.rs @@ -187,6 +187,7 @@ pub(crate) trait SqliteAsyncConnExt { /// /// Only returns an error in tests, otherwise the error is only logged. async fn vacuum(&self) -> Result<()> { + // Truncate the WAL file before vacuuming so it has room to grow. self.wal_checkpoint().await; if let Err(error) = self.execute_batch("VACUUM").await { // Since this is an optimisation step, do not propagate the error @@ -199,12 +200,17 @@ pub(crate) trait SqliteAsyncConnExt { return Err(error.into()); } else { trace!("VACUUM complete"); + // Once vacuumed, truncate the WAL file again to purge the copied DB contents. self.wal_checkpoint().await; } Ok(()) } + /// Adds a manual [WAL checkpoint] to copy back the contents of the WAL + /// files into the actual database, resetting the write-ahead log. + /// + /// [WAL checkpoint]: https://sqlite.org/c3ref/wal_checkpoint.html async fn wal_checkpoint(&self) { match self.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);").await { Ok(_) => trace!("WAL checkpoint completed"), From fbc0981e8dfcc5b8ad92401cfdafad85b41386d2 Mon Sep 17 00:00:00 2001 From: Skye Elliot Date: Wed, 7 Jan 2026 16:06:42 +0000 Subject: [PATCH 36/36] docs(common): Correct `ForwarderInfo` changelog entry. Signed-off-by: Skye Elliot --- crates/matrix-sdk-common/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index cb51603a4..6361ec993 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -12,6 +12,11 @@ All notable changes to this project will be documented in this file. `SentInClear`. `VeificationState::to_shield_state_{lax,strict}` never returned that code, ans so having it in the enum was somewhat misleading. ([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959)) +- Add field `forwarder` of type `ForwarderInfo` to `EncryptionInfo`, which + exposes information about the forwarder of the keys with which an event was + encrypted if they were shared as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) + room key bundle. + ([#5945](https://github.com/matrix-org/matrix-rust-sdk/pull/5945)). ### Bug Fixes @@ -21,8 +26,6 @@ All notable changes to this project will be documented in this file. ### Features -- Add field `forwarder` of type `ForwarderInfo` to `EncryptionInfo`, which which exposes information about the forwarder of the keys with which an event was encrypted if they were shared as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) room key bundle. - ([#5945](https://github.com/matrix-org/matrix-rust-sdk/pull/5945)). - [**breaking**] Cross-process lock can be dirty. The `CrossProcess::try_lock_once` now returns a new type `CrossProcessResult`, which is an enum with `Clean`, `Dirty` or `Unobtained` variants. When the lock is dirty it means it's been acquired once, then acquired another time from another holder, so the current holder may want to refresh its internal state. ([#5672](https://github.com/matrix-org/matrix-rust-sdk/pull/5672)).