From 1f94e9d20c830a2a51997a7d7c16242d4a3aaace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 9 Dec 2025 12:29:13 +0100 Subject: [PATCH 01/14] feat(ffi): Add `fn RoomPowerLevels::events` With this we can query the power level value for any event type --- bindings/matrix-sdk-ffi/src/room/power_levels.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/room/power_levels.rs b/bindings/matrix-sdk-ffi/src/room/power_levels.rs index d257aaf2d..c2986c5fe 100644 --- a/bindings/matrix-sdk-ffi/src/room/power_levels.rs +++ b/bindings/matrix-sdk-ffi/src/room/power_levels.rs @@ -29,6 +29,10 @@ impl RoomPowerLevels { self.inner.clone().into() } + fn events(&self) -> HashMap { + self.inner.events.iter().map(|(key, value)| (key.to_string(), (*value).into())).collect() + } + /// Gets a map with the `UserId` of users with power levels other than `0` /// and their power level. pub fn user_power_levels(&self) -> HashMap { From 4665b4343d1d62666709ef9c5f5ac9064f1cdc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 9 Dec 2025 13:53:20 +0100 Subject: [PATCH 02/14] refactor(ffi): Rename `TimelineEventType` to `TiemlineEventContent` since it also contains the event contents for some of the types --- bindings/matrix-sdk-ffi/src/event.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index 6f5f6d40e..be68a738c 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -39,16 +39,16 @@ impl TimelineEvent { self.0.origin_server_ts().into() } - pub fn event_type(&self) -> Result { - let event_type = match &*self.0 { + pub fn content(&self) -> Result { + let content = match &*self.0 { AnySyncTimelineEvent::MessageLike(event) => { - TimelineEventType::MessageLike { content: event.clone().try_into()? } + TimelineEventContent::MessageLike { content: event.clone().try_into()? } } AnySyncTimelineEvent::State(event) => { - TimelineEventType::State { content: event.clone().try_into()? } + TimelineEventContent::State { content: event.clone().try_into()? } } }; - Ok(event_type) + Ok(content) } /// Returns the thread root event id for the event, if it's part of a @@ -74,14 +74,14 @@ impl From for TimelineEvent { #[derive(uniffi::Enum)] // A note about this `allow(clippy::large_enum_variant)`. -// In order to reduce the size of `TimelineEventType`, we would need to +// In order to reduce the size of `TimelineEventContent`, we would need to // put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with // UniFFI. We would need to change the `uniffi::Record` of the subtypes into // `uniffi::Object`, which is a radical change. It would simplify the memory // usage, but it would slow down the performance around the FFI border. Thus, // let's consider this is a false-positive lint in this particular case. #[allow(clippy::large_enum_variant)] -pub enum TimelineEventType { +pub enum TimelineEventContent { MessageLike { content: MessageLikeEventContent }, State { content: StateEventContent }, } From 1549194b2f4882822619c1aebb5c7a08bf9bc7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 9 Dec 2025 13:54:17 +0100 Subject: [PATCH 03/14] feat(ffi): Add an actual `ffi::TimelineEventType` enum with only the type Use that for `RoomPowerLevels::events` instead. --- Cargo.lock | 5 + bindings/matrix-sdk-ffi/Cargo.toml | 25 +- bindings/matrix-sdk-ffi/src/event.rs | 257 +++++++++++++++++- .../matrix-sdk-ffi/src/room/power_levels.rs | 4 +- 4 files changed, 282 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0337c7772..c961fb3ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2906,6 +2906,9 @@ name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +dependencies = [ + "serde", +] [[package]] name = "lazy_static" @@ -4812,6 +4815,7 @@ dependencies = [ "assign", "js_int", "js_option", + "language-tags", "ruma-client-api", "ruma-common", "ruma-events", @@ -4886,6 +4890,7 @@ dependencies = [ "indexmap", "js_int", "js_option", + "language-tags", "percent-encoding", "pulldown-cmark", "regex", diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 462938e1f..f3bc87cb7 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -65,7 +65,30 @@ matrix-sdk-ffi-macros.workspace = true matrix-sdk-ui = { workspace = true, features = ["uniffi"] } mime = "0.3.17" once_cell.workspace = true -ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] } +ruma = { workspace = true, features = [ + "html", + "unstable-msc3488", + "compat-unset-avatar", + "unstable-msc3245-v1-compat", + "unstable-msc4278", + "unstable-msc3230", + # Audio event type + "unstable-msc3927", + # File event type + "unstable-msc3551", + # Image event type + "unstable-msc3552", + # Video event type + "unstable-msc3553", + # Voice event type + "unstable-msc3245", + # Emote event type + "unstable-msc3954", + # Image pack event type + "unstable-msc2545", + # Room language event type + "unstable-msc4334", +] } serde.workspace = true serde_json.workspace = true sentry = { workspace = true, optional = true, default-features = false, features = [ diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index be68a738c..810a58747 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -11,6 +11,7 @@ use ruma::{ AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent, RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent, + TimelineEventType as RumaTimelineEventType, }, EventId, }; @@ -72,6 +73,206 @@ impl From for TimelineEvent { } } +/// The timeline event type. +#[derive(uniffi::Enum, PartialEq, Eq, Hash)] +pub enum TimelineEventType { + /// The event is a message-like one and should be displayed as such. + MessageLike { value: MessageLikeEventType }, + /// The event is a state event, and may or may not be displayed in the + /// timeline. + State { value: StateEventType }, +} + +impl From for TimelineEventType { + fn from(value: RumaTimelineEventType) -> Self { + match value { + RumaTimelineEventType::Audio => { + Self::MessageLike { value: MessageLikeEventType::Audio } + } + RumaTimelineEventType::File => Self::MessageLike { value: MessageLikeEventType::File }, + RumaTimelineEventType::Image => { + Self::MessageLike { value: MessageLikeEventType::Image } + } + RumaTimelineEventType::Video => { + Self::MessageLike { value: MessageLikeEventType::Video } + } + RumaTimelineEventType::Voice => { + Self::MessageLike { value: MessageLikeEventType::Voice } + } + RumaTimelineEventType::Emote => { + Self::MessageLike { value: MessageLikeEventType::Emote } + } + RumaTimelineEventType::Encrypted => { + Self::MessageLike { value: MessageLikeEventType::Encrypted } + } + RumaTimelineEventType::RoomMessage => { + Self::MessageLike { value: MessageLikeEventType::RoomMessage } + } + RumaTimelineEventType::CallAnswer => { + Self::MessageLike { value: MessageLikeEventType::CallAnswer } + } + RumaTimelineEventType::CallInvite => { + Self::MessageLike { value: MessageLikeEventType::CallInvite } + } + RumaTimelineEventType::CallHangup => { + Self::MessageLike { value: MessageLikeEventType::CallHangup } + } + RumaTimelineEventType::CallCandidates => { + Self::MessageLike { value: MessageLikeEventType::CallCandidates } + } + RumaTimelineEventType::CallNegotiate => { + Self::MessageLike { value: MessageLikeEventType::CallNegotiate } + } + RumaTimelineEventType::CallReject => { + Self::MessageLike { value: MessageLikeEventType::CallReject } + } + RumaTimelineEventType::CallSdpStreamMetadataChanged => { + Self::MessageLike { value: MessageLikeEventType::CallSdpStreamMetadataChanged } + } + RumaTimelineEventType::CallSelectAnswer => { + Self::MessageLike { value: MessageLikeEventType::CallSelectAnswer } + } + RumaTimelineEventType::KeyVerificationReady => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationReady } + } + RumaTimelineEventType::KeyVerificationStart => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationStart } + } + RumaTimelineEventType::KeyVerificationCancel => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationCancel } + } + RumaTimelineEventType::KeyVerificationAccept => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationAccept } + } + RumaTimelineEventType::KeyVerificationKey => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationKey } + } + RumaTimelineEventType::KeyVerificationMac => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationMac } + } + RumaTimelineEventType::KeyVerificationDone => { + Self::MessageLike { value: MessageLikeEventType::KeyVerificationDone } + } + RumaTimelineEventType::Location => { + Self::MessageLike { value: MessageLikeEventType::Location } + } + RumaTimelineEventType::Message => { + Self::MessageLike { value: MessageLikeEventType::Message } + } + RumaTimelineEventType::PollStart => { + Self::MessageLike { value: MessageLikeEventType::PollStart } + } + RumaTimelineEventType::UnstablePollStart => { + Self::MessageLike { value: MessageLikeEventType::UnstablePollStart } + } + RumaTimelineEventType::PollResponse => { + Self::MessageLike { value: MessageLikeEventType::PollResponse } + } + RumaTimelineEventType::UnstablePollResponse => { + Self::MessageLike { value: MessageLikeEventType::UnstablePollResponse } + } + RumaTimelineEventType::PollEnd => { + Self::MessageLike { value: MessageLikeEventType::PollEnd } + } + RumaTimelineEventType::UnstablePollEnd => { + Self::MessageLike { value: MessageLikeEventType::UnstablePollEnd } + } + RumaTimelineEventType::Beacon => { + Self::MessageLike { value: MessageLikeEventType::Beacon } + } + RumaTimelineEventType::Reaction => { + Self::MessageLike { value: MessageLikeEventType::Reaction } + } + RumaTimelineEventType::RoomEncrypted => { + Self::MessageLike { value: MessageLikeEventType::RoomEncrypted } + } + RumaTimelineEventType::RoomRedaction => { + Self::MessageLike { value: MessageLikeEventType::RoomRedaction } + } + RumaTimelineEventType::Sticker => { + Self::MessageLike { value: MessageLikeEventType::Sticker } + } + RumaTimelineEventType::CallNotify => { + Self::MessageLike { value: MessageLikeEventType::CallNotify } + } + RumaTimelineEventType::RtcNotification => { + Self::MessageLike { value: MessageLikeEventType::RtcNotification } + } + RumaTimelineEventType::RtcDecline => { + Self::MessageLike { value: MessageLikeEventType::RtcDecline } + } + RumaTimelineEventType::PolicyRuleRoom => { + Self::State { value: StateEventType::PolicyRuleRoom } + } + RumaTimelineEventType::PolicyRuleServer => { + Self::State { value: StateEventType::PolicyRuleServer } + } + RumaTimelineEventType::PolicyRuleUser => { + Self::State { value: StateEventType::PolicyRuleUser } + } + RumaTimelineEventType::RoomAliases => { + Self::State { value: StateEventType::RoomAliases } + } + RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar }, + RumaTimelineEventType::RoomCanonicalAlias => { + Self::State { value: StateEventType::RoomCanonicalAlias } + } + RumaTimelineEventType::RoomCreate => Self::State { value: StateEventType::RoomCreate }, + RumaTimelineEventType::RoomEncryption => { + Self::State { value: StateEventType::RoomEncryption } + } + RumaTimelineEventType::RoomGuestAccess => { + Self::State { value: StateEventType::RoomGuestAccess } + } + RumaTimelineEventType::RoomHistoryVisibility => { + Self::State { value: StateEventType::RoomHistoryVisibility } + } + RumaTimelineEventType::RoomJoinRules => { + Self::State { value: StateEventType::RoomJoinRules } + } + RumaTimelineEventType::RoomMember => { + Self::State { value: StateEventType::RoomMemberEvent } + } + RumaTimelineEventType::RoomLanguage => { + Self::State { value: StateEventType::RoomLanguage } + } + RumaTimelineEventType::RoomName => Self::State { value: StateEventType::RoomName }, + RumaTimelineEventType::RoomImagePack => { + Self::State { value: StateEventType::RoomImagePack } + } + RumaTimelineEventType::RoomPinnedEvents => { + Self::State { value: StateEventType::RoomPinnedEvents } + } + RumaTimelineEventType::RoomPowerLevels => { + Self::State { value: StateEventType::RoomPowerLevels } + } + RumaTimelineEventType::RoomServerAcl => { + Self::State { value: StateEventType::RoomServerAcl } + } + RumaTimelineEventType::RoomThirdPartyInvite => { + Self::State { value: StateEventType::RoomThirdPartyInvite } + } + RumaTimelineEventType::RoomTombstone => { + Self::State { value: StateEventType::RoomTombstone } + } + RumaTimelineEventType::RoomTopic => Self::State { value: StateEventType::RoomTopic }, + RumaTimelineEventType::SpaceChild => Self::State { value: StateEventType::SpaceChild }, + RumaTimelineEventType::SpaceParent => { + Self::State { value: StateEventType::SpaceParent } + } + RumaTimelineEventType::BeaconInfo => Self::State { value: StateEventType::BeaconInfo }, + RumaTimelineEventType::CallMember => Self::State { value: StateEventType::CallMember }, + RumaTimelineEventType::MemberHints => { + Self::State { value: StateEventType::MemberHints } + } + RumaTimelineEventType::_Custom(_) => { + Self::State { value: StateEventType::Custom { value: value.to_string() } } + } + _ => Self::MessageLike { value: MessageLikeEventType::Other(value.to_string()) }, + } + } +} + #[derive(uniffi::Enum)] // A note about this `allow(clippy::large_enum_variant)`. // In order to reduce the size of `TimelineEventContent`, we would need to @@ -300,9 +501,11 @@ where Ok(original_content) } -#[derive(Clone, uniffi::Enum)] +#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)] pub enum StateEventType { + BeaconInfo, CallMember, + MemberHints, PolicyRuleRoom, PolicyRuleServer, PolicyRuleUser, @@ -313,8 +516,10 @@ pub enum StateEventType { RoomEncryption, RoomGuestAccess, RoomHistoryVisibility, + RoomImagePack, RoomJoinRules, RoomMemberEvent, + RoomLanguage, RoomName, RoomPinnedEvents, RoomPowerLevels, @@ -324,12 +529,15 @@ pub enum StateEventType { RoomTopic, SpaceChild, SpaceParent, + Custom { value: String }, } impl From for ruma::events::StateEventType { fn from(val: StateEventType) -> Self { match val { + StateEventType::BeaconInfo => Self::BeaconInfo, StateEventType::CallMember => Self::CallMember, + StateEventType::MemberHints => Self::MemberHints, StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom, StateEventType::PolicyRuleServer => Self::PolicyRuleServer, StateEventType::PolicyRuleUser => Self::PolicyRuleUser, @@ -340,7 +548,9 @@ impl From for ruma::events::StateEventType { StateEventType::RoomEncryption => Self::RoomEncryption, StateEventType::RoomGuestAccess => Self::RoomGuestAccess, StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility, + StateEventType::RoomImagePack => Self::RoomImagePack, StateEventType::RoomJoinRules => Self::RoomJoinRules, + StateEventType::RoomLanguage => Self::RoomLanguage, StateEventType::RoomMemberEvent => Self::RoomMember, StateEventType::RoomName => Self::RoomName, StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents, @@ -351,17 +561,28 @@ impl From for ruma::events::StateEventType { StateEventType::RoomTopic => Self::RoomTopic, StateEventType::SpaceChild => Self::SpaceChild, StateEventType::SpaceParent => Self::SpaceParent, + StateEventType::Custom { value } => value.into(), } } } -#[derive(Clone, uniffi::Enum)] +#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)] pub enum MessageLikeEventType { + Audio, + Beacon, CallAnswer, CallCandidates, CallHangup, CallInvite, - RtcNotification, + CallNegotiate, + CallNotify, + CallReject, + CallSdpStreamMetadataChanged, + CallSelectAnswer, + Emote, + Encrypted, + File, + Image, KeyVerificationAccept, KeyVerificationCancel, KeyVerificationDone, @@ -369,6 +590,8 @@ pub enum MessageLikeEventType { KeyVerificationMac, KeyVerificationReady, KeyVerificationStart, + Location, + Message, PollEnd, PollResponse, PollStart, @@ -376,21 +599,39 @@ pub enum MessageLikeEventType { RoomEncrypted, RoomMessage, RoomRedaction, + RtcDecline, + RtcNotification, Sticker, UnstablePollEnd, UnstablePollResponse, UnstablePollStart, + Video, + Voice, Other(String), } impl From for ruma::events::MessageLikeEventType { fn from(val: MessageLikeEventType) -> Self { match val { + MessageLikeEventType::Audio => Self::Audio, + MessageLikeEventType::File => Self::File, + MessageLikeEventType::Image => Self::Image, + MessageLikeEventType::Video => Self::Video, + MessageLikeEventType::Voice => Self::Voice, + MessageLikeEventType::Beacon => Self::Beacon, MessageLikeEventType::CallAnswer => Self::CallAnswer, - MessageLikeEventType::CallInvite => Self::CallInvite, - MessageLikeEventType::RtcNotification => Self::RtcNotification, - MessageLikeEventType::CallHangup => Self::CallHangup, MessageLikeEventType::CallCandidates => Self::CallCandidates, + MessageLikeEventType::CallInvite => Self::CallInvite, + MessageLikeEventType::CallHangup => Self::CallHangup, + MessageLikeEventType::CallNegotiate => Self::CallNegotiate, + MessageLikeEventType::CallNotify => Self::CallNotify, + MessageLikeEventType::CallReject => Self::CallReject, + MessageLikeEventType::CallSdpStreamMetadataChanged => { + Self::CallSdpStreamMetadataChanged + } + MessageLikeEventType::CallSelectAnswer => Self::CallSelectAnswer, + MessageLikeEventType::Emote => Self::Emote, + MessageLikeEventType::Encrypted => Self::Encrypted, MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady, MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart, MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel, @@ -398,14 +639,18 @@ impl From for ruma::events::MessageLikeEventType { MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey, MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac, MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone, + MessageLikeEventType::Location => Self::Location, + MessageLikeEventType::Message => Self::Message, MessageLikeEventType::Reaction => Self::Reaction, MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted, MessageLikeEventType::RoomMessage => Self::RoomMessage, MessageLikeEventType::RoomRedaction => Self::RoomRedaction, + MessageLikeEventType::RtcDecline => Self::RtcDecline, MessageLikeEventType::Sticker => Self::Sticker, MessageLikeEventType::PollEnd => Self::PollEnd, MessageLikeEventType::PollResponse => Self::PollResponse, MessageLikeEventType::PollStart => Self::PollStart, + MessageLikeEventType::RtcNotification => Self::RtcNotification, MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd, MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse, MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart, diff --git a/bindings/matrix-sdk-ffi/src/room/power_levels.rs b/bindings/matrix-sdk-ffi/src/room/power_levels.rs index c2986c5fe..f206edffb 100644 --- a/bindings/matrix-sdk-ffi/src/room/power_levels.rs +++ b/bindings/matrix-sdk-ffi/src/room/power_levels.rs @@ -29,8 +29,8 @@ impl RoomPowerLevels { self.inner.clone().into() } - fn events(&self) -> HashMap { - self.inner.events.iter().map(|(key, value)| (key.to_string(), (*value).into())).collect() + fn events(&self) -> HashMap { + self.inner.events.iter().map(|(key, value)| (key.clone().into(), (*value).into())).collect() } /// Gets a map with the `UserId` of users with power levels other than `0` From 759c5a9fcd8f7cf2f2bdba70ca8fd3a14fc6c865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 18 Dec 2025 10:15:52 +0100 Subject: [PATCH 04/14] docs: Add changelog entries --- bindings/matrix-sdk-ffi/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 91497beb5..438e2d6ff 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -23,9 +23,16 @@ All notable changes to this project will be documented in this file. `Room::new_latest_event` overwrites the `Room::latest_event` method. See the documentation of `matrix_sdk::latest_event` to learn about the new API. [#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)) ### Refactor +- [**breaking**] The existing `TimelineEventType` was renamed to `TimelineEventContent`, because it contained the + actual contents of the event. Then, we created a new `TimelineEventType` enum that actually contains *just* the + event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937)) +- [**breaking**] The function `TimelineEvent::event_type` is now `TimelineEvent::content`. + ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937)) - [**breaking**] The `SpaceService` will no longer auto-subscribe to required client events when invoking the `subscribe_to_joined_spaces` but instead do it through its, now async, constructor. From 9363745fb0019b05916a39a86399b6418ea998c8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 08:30:30 +0100 Subject: [PATCH 05/14] chore(sdk): Remove `timer!` logs. This patch removes `timer!` logs, those are no longer useful. --- crates/matrix-sdk/src/latest_events/latest_event.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index f1ede6288..77d7e206b 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -23,7 +23,7 @@ pub use matrix_sdk_base::latest_event::{ }; use matrix_sdk_base::{ RoomInfoNotableUpdateReasons, StateChanges, deserialized_responses::TimelineEvent, - store::SerializableEventContent, timer, + store::SerializableEventContent, }; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, TransactionId, UserId, @@ -658,11 +658,6 @@ impl LatestEventValueBuilder { own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) -> LatestEventValue { - let _timer = timer!( - tracing::Level::INFO, - format!("`LatestEventValueBuilder::new_remote` for {:?}", room_event_cache.room_id()) - ); - if let Ok(Some(event)) = room_event_cache .rfind_map_event_in_memory_by(|event, previous_event_id| { filter_timeline_event(event, previous_event_id, own_user_id, power_levels) @@ -687,11 +682,6 @@ impl LatestEventValueBuilder { ) -> Option { use crate::send_queue::{LocalEcho, LocalEchoContent}; - let _timer = timer!( - tracing::Level::INFO, - format!("`LatestEventValueBuilder::new_local` for {:?}", room_event_cache.room_id()) - ); - Some(match send_queue_update { // A new local event is being sent. // From 42b79d7d8a255672ef991204d1e3ede0813c1134 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 08:45:59 +0100 Subject: [PATCH 06/14] feat(base): `LatestEventValue::LocalHasBeenSent` gains an `event_id` field. This patch adds the `event_id: OwnedEventId` field to `LatestEventValue::LocalHasBeenSent`. --- crates/matrix-sdk-base/src/latest_event.rs | 38 +++++++++----- .../room_list_service/sorters/latest_event.rs | 19 ++++--- .../src/timeline/latest_event.rs | 21 ++++---- .../src/latest_events/latest_event.rs | 51 ++++++++++++++++--- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 67a48cac8..8c95f6c61 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -1,7 +1,7 @@ //! The Latest Event basic types. use matrix_sdk_common::deserialized_responses::TimelineEvent; -use ruma::MilliSecondsSinceUnixEpoch; +use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId}; use serde::{Deserialize, Serialize}; use crate::store::SerializableEventContent; @@ -21,7 +21,13 @@ pub enum LatestEventValue { /// The latest event represents a local event that has been sent /// successfully. It should come quickly as a [`Self::Remote`]. - LocalHasBeenSent(LocalLatestEventValue), + LocalHasBeenSent { + /// ID of the sent event. + event_id: OwnedEventId, + + /// Value, as for other [`Self::Local*`] variants. + value: LocalLatestEventValue, + }, /// The latest event represents a local event that cannot be sent, either /// because a previous local event, or this local event cannot be sent. @@ -46,7 +52,7 @@ impl LatestEventValue { Self::None => None, Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(), Self::LocalIsSending(LocalLatestEventValue { timestamp, .. }) - | Self::LocalHasBeenSent(LocalLatestEventValue { timestamp, .. }) + | Self::LocalHasBeenSent { value: LocalLatestEventValue { timestamp, .. }, .. } | Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp), } } @@ -58,9 +64,9 @@ impl LatestEventValue { /// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent pub fn is_local(&self) -> bool { match self { - Self::LocalIsSending(_) | Self::LocalHasBeenSent(_) | Self::LocalCannotBeSent(_) => { - true - } + Self::LocalIsSending(_) + | Self::LocalHasBeenSent { .. } + | Self::LocalCannotBeSent(_) => true, Self::None | Self::Remote(_) => false, } } @@ -73,7 +79,7 @@ impl LatestEventValue { pub fn is_unsent(&self) -> bool { match self { Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true, - Self::LocalHasBeenSent(_) | Self::Remote(_) | Self::None => false, + Self::LocalHasBeenSent { .. } | Self::Remote(_) | Self::None => false, } } @@ -104,6 +110,7 @@ mod tests_latest_event_value { use ruma::{ MilliSecondsSinceUnixEpoch, events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent}, + owned_event_id, serde::Raw, uint, }; @@ -154,13 +161,16 @@ mod tests_latest_event_value { #[test] fn test_timestamp_with_local_has_been_sent() { - let value = LatestEventValue::LocalHasBeenSent(LocalLatestEventValue { - timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), - content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( - RoomMessageEventContent::text_plain("raclette"), - )) - .unwrap(), - }); + let value = LatestEventValue::LocalHasBeenSent { + event_id: owned_event_id!("$ev0"), + value: LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }, + }; assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42)))); } diff --git a/crates/matrix-sdk-ui/src/room_list_service/sorters/latest_event.rs b/crates/matrix-sdk-ui/src/room_list_service/sorters/latest_event.rs index ab544c6f8..208f68547 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/sorters/latest_event.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/sorters/latest_event.rs @@ -74,7 +74,7 @@ mod tests { use ruma::{ MilliSecondsSinceUnixEpoch, events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent}, - room_id, + owned_event_id, room_id, serde::Raw, uint, }; @@ -114,13 +114,16 @@ mod tests { } fn local_has_been_sent() -> LatestEventValue { - LatestEventValue::LocalHasBeenSent(LocalLatestEventValue { - timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), - content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( - RoomMessageEventContent::text_plain("raclette"), - )) - .unwrap(), - }) + LatestEventValue::LocalHasBeenSent { + event_id: owned_event_id!("$ev0"), + value: LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }, + } } fn local_cannot_be_sent() -> LatestEventValue { diff --git a/crates/matrix-sdk-ui/src/timeline/latest_event.rs b/crates/matrix-sdk-ui/src/timeline/latest_event.rs index 51751efdd..b8b4f112d 100644 --- a/crates/matrix-sdk-ui/src/timeline/latest_event.rs +++ b/crates/matrix-sdk-ui/src/timeline/latest_event.rs @@ -161,7 +161,7 @@ impl LatestEventValue { } } BaseLatestEventValue::LocalIsSending(ref local_value) - | BaseLatestEventValue::LocalHasBeenSent(ref local_value) + | BaseLatestEventValue::LocalHasBeenSent { value: ref local_value, .. } | BaseLatestEventValue::LocalCannotBeSent(ref local_value) => { let LocalLatestEventValue { timestamp, content: serialized_content } = local_value; @@ -187,7 +187,7 @@ impl LatestEventValue { BaseLatestEventValue::LocalIsSending(_) => { LatestEventValueLocalState::IsSending } - BaseLatestEventValue::LocalHasBeenSent(_) => { + BaseLatestEventValue::LocalHasBeenSent { .. } => { LatestEventValueLocalState::HasBeenSent } BaseLatestEventValue::LocalCannotBeSent(_) => { @@ -313,13 +313,16 @@ mod tests { let client = server.client_builder().build().await; let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await; - let base_value = BaseLatestEventValue::LocalHasBeenSent(LocalLatestEventValue { - timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), - content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( - RoomMessageEventContent::text_plain("raclette"), - )) - .unwrap(), - }); + let base_value = BaseLatestEventValue::LocalHasBeenSent { + event_id: event_id!("$ev0").to_owned(), + value: LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }, + }; let value = LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await; diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 77d7e206b..d76ecc286 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -532,7 +532,7 @@ mod tests_latest_event { assert_matches!( latest_event.current_value.get().await, - LatestEventValue::LocalHasBeenSent(_) + LatestEventValue::LocalHasBeenSent { .. } ); } @@ -760,7 +760,7 @@ impl LatestEventValueBuilder { // “is sending”. Then, remove the calculated `LatestEventValue` from the buffer of // values. Finally, return the last `LatestEventValue` or calculate a new // one. - RoomSendQueueUpdate::SentEvent { transaction_id, .. } => { + RoomSendQueueUpdate::SentEvent { transaction_id, event_id } => { if let Some(position) = buffer_of_values_for_local_events.mark_is_sending_after(transaction_id) { @@ -774,8 +774,8 @@ impl LatestEventValueBuilder { LatestEventValue::LocalIsSending(local_value) | LatestEventValue::LocalCannotBeSent(local_value) // Technically impossible, but it's not harmful to handle this that way. - | LatestEventValue::LocalHasBeenSent(local_value ) => { - return Some(LatestEventValue::LocalHasBeenSent(local_value)); + | LatestEventValue::LocalHasBeenSent { value: local_value, .. } => { + return Some(LatestEventValue::LocalHasBeenSent { event_id: event_id.clone(), value: local_value }); } LatestEventValue::Remote(_) | LatestEventValue::None => unreachable!("Impossible to get a remote `LatestEventValue`"), } @@ -1566,6 +1566,7 @@ mod tests_latest_event_values_for_local_events { use ruma::{ MilliSecondsSinceUnixEpoch, OwnedTransactionId, events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent}, + owned_event_id, serde::Raw, }; use serde_json::json; @@ -1673,7 +1674,10 @@ mod tests_latest_event_values_for_local_events { ); buffer.push( OwnedTransactionId::from("txnid1"), - LatestEventValue::LocalHasBeenSent(local_room_message("raclette")), + LatestEventValue::LocalHasBeenSent { + event_id: owned_event_id!("$ev0"), + value: local_room_message("raclette"), + }, ); // no panic. @@ -1701,7 +1705,10 @@ mod tests_latest_event_values_for_local_events { buffer.push( OwnedTransactionId::from("txnid"), - LatestEventValue::LocalHasBeenSent(local_room_message("gruyère")), + LatestEventValue::LocalHasBeenSent { + event_id: owned_event_id!("$ev0"), + value: local_room_message("gruyère"), + }, ); let LocalLatestEventValue { content: new_content, .. } = local_room_message("comté"); @@ -1935,6 +1942,28 @@ mod tests_latest_event_value_builder { } ); }; + + ( $latest_event_value:expr, $pattern:path { + $local_value:ident with body = $body:expr + $( , $field:ident => $more:block )* + } ) => { + assert_matches!( + $latest_event_value, + Some( $pattern { $local_value, $( $field, )* .. }) => { + assert_matches!( + $local_value .content.deserialize().unwrap(), + AnyMessageLikeEventContent::RoomMessage(message_content) => { + assert_eq!(message_content.body(), $body); + + $({ + let $field = $field; + $more + })* + } + ); + } + ); + }; } #[async_test] @@ -2347,15 +2376,21 @@ mod tests_latest_event_value_builder { // hasn't changed, this is still this event, but the status has changed to // `LocalHasBeenSent`. { + let expected_event_id = event_id!("$ev1").to_owned(); let update = RoomSendQueueUpdate::SentEvent { transaction_id: transaction_id_1, - event_id: event_id!("$ev1").to_owned(), + event_id: expected_event_id.clone(), }; // The `LatestEventValue` hasn't changed. assert_local_value_matches_room_message_with_body!( LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, - LatestEventValue::LocalHasBeenSent => with body = "B" + LatestEventValue::LocalHasBeenSent { + value with body = "B", + event_id => { + assert_eq!(event_id, expected_event_id); + } + } ); assert!(buffer.buffer.is_empty()); From d5898a64ab7a10e5c3e87d9c8242cc1e955f6054 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 09:20:27 +0100 Subject: [PATCH 07/14] feat(base): Add `LatestEventValue::event_id`. This patch adds the `event_id` method on `LatestEventValue`, along with the tests. --- crates/matrix-sdk-base/src/latest_event.rs | 83 +++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 8c95f6c61..2b07b5ef3 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -25,7 +25,7 @@ pub enum LatestEventValue { /// ID of the sent event. event_id: OwnedEventId, - /// Value, as for other [`Self::Local*`] variants. + /// Value, as for other `Self::Local*` variants. value: LocalLatestEventValue, }, @@ -89,6 +89,16 @@ impl LatestEventValue { pub fn is_none(&self) -> bool { matches!(self, Self::None) } + + /// Get the event ID (if it exists) of the event representing the + /// [`LatestEventValue`]. + pub fn event_id(&self) -> Option { + match self { + Self::Remote(event) => event.event_id(), + Self::LocalHasBeenSent { event_id, .. } => Some(event_id.clone()), + Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) | Self::None => None, + } + } } /// Represents the value for [`LatestEventValue::Remote`]. @@ -187,4 +197,75 @@ mod tests_latest_event_value { assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42)))); } + + #[test] + fn test_event_id_with_none() { + let value = LatestEventValue::None; + + assert!(value.event_id().is_none()); + } + + #[test] + fn test_event_id_with_remote() { + let event_id = owned_event_id!("$ev0"); + let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext( + Raw::from_json_string( + json!({ + "content": RoomMessageEventContent::text_plain("raclette"), + "type": "m.room.message", + "event_id": event_id, + "room_id": "!r0", + "origin_server_ts": 42, + "sender": "@mnt_io:matrix.org", + }) + .to_string(), + ) + .unwrap(), + )); + + assert_eq!(value.event_id(), Some(event_id)); + } + + #[test] + fn test_event_id_with_local_is_sending() { + let value = LatestEventValue::LocalIsSending(LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }); + + assert!(value.event_id().is_none()); + } + + #[test] + fn test_event_id_with_local_has_been_sent() { + let event_id = owned_event_id!("$ev0"); + let value = LatestEventValue::LocalHasBeenSent { + event_id: event_id.clone(), + value: LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }, + }; + + assert_eq!(value.event_id(), Some(event_id)); + } + + #[test] + fn test_event_id_with_local_cannot_be_sent() { + let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue { + timestamp: MilliSecondsSinceUnixEpoch(uint!(42)), + content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage( + RoomMessageEventContent::text_plain("raclette"), + )) + .unwrap(), + }); + + assert!(value.event_id().is_none()); + } } From e2ea84f3e335bd0fb27cd16dae5880793d8793a5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 09:29:03 +0100 Subject: [PATCH 08/14] chore(sdk): Rename a variable. This patch renames a variable. Since `rfind_map_event_id_memory_by` returns the previous event instead of the previous event ID, this variable must have been renamed. --- crates/matrix-sdk/src/latest_events/latest_event.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index d76ecc286..4901ec711 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -659,8 +659,8 @@ impl LatestEventValueBuilder { power_levels: Option<&RoomPowerLevels>, ) -> LatestEventValue { if let Ok(Some(event)) = room_event_cache - .rfind_map_event_in_memory_by(|event, previous_event_id| { - filter_timeline_event(event, previous_event_id, own_user_id, power_levels) + .rfind_map_event_in_memory_by(|event, previous_event| { + filter_timeline_event(event, previous_event, own_user_id, power_levels) .then(|| event.clone()) }) .await @@ -1264,9 +1264,9 @@ mod tests_latest_event_content { // is part of another chunk that is not loaded in memory yet. In this case, // let's not consider the event as a `LatestEventValue` candidate. { - let previous_event_id = None; + let previous_event = None; - assert!(filter_timeline_event(&event, previous_event_id, user_id, None).not()); + assert!(filter_timeline_event(&event, previous_event, user_id, None).not()); } // With a previous event, but not the one being replaced. From 85f321f30a02772c61ee4381673c3dc374cb9b64 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 10:11:08 +0100 Subject: [PATCH 09/14] refactor(sdk): `LatestEventValueBuilder` receives the current value's event ID. This patch spreads the current value's event ID in `LatestEventValueBuilder`. This patch also changes `LatestEventValueBuilder::new_remote` to return an `Option`, similarly to `new_local`. The `must_overwrite_existing` variable is set to `true` to keep the existing behaviour, but it's going to change in the next patch. This patch is purely a refactoring with no feature change. Most of the changes are in test to keep track of the _previous value_ so that the current value's event ID can be calculated instead of hardcoded. --- .../src/latest_events/latest_event.rs | 301 +++++++++++------- 1 file changed, 188 insertions(+), 113 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 4901ec711..5ba2867b8 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -115,12 +115,20 @@ impl LatestEvent { return; } - let new_value = - LatestEventValueBuilder::new_remote(room_event_cache, own_user_id, power_levels).await; + let current_value_event_id = self.current_value.read().await.event_id(); + let new_value = LatestEventValueBuilder::new_remote( + room_event_cache, + current_value_event_id, + own_user_id, + power_levels, + ) + .await; info!(value = ?new_value, "Computed a remote `LatestEventValue`"); - self.update(new_value).await; + if let Some(new_value) = new_value { + self.update(new_value).await; + } } /// Update the inner latest event value, based on the send queue @@ -132,10 +140,12 @@ impl LatestEvent { own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) { + let current_value_event_id = self.current_value.read().await.event_id(); let new_value = LatestEventValueBuilder::new_local( send_queue_update, &mut self.buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -655,9 +665,12 @@ impl LatestEventValueBuilder { /// Create a new [`LatestEventValue::Remote`]. async fn new_remote( room_event_cache: &RoomEventCache, + current_value_event_id: Option, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, - ) -> LatestEventValue { + ) -> Option { + let must_overwrite_existing = true; + if let Ok(Some(event)) = room_event_cache .rfind_map_event_in_memory_by(|event, previous_event| { filter_timeline_event(event, previous_event, own_user_id, power_levels) @@ -665,9 +678,9 @@ impl LatestEventValueBuilder { }) .await { - LatestEventValue::Remote(event) + Some(LatestEventValue::Remote(event)) } else { - LatestEventValue::default() + must_overwrite_existing.then(|| LatestEventValue::default()) } } @@ -677,12 +690,13 @@ impl LatestEventValueBuilder { send_queue_update: &RoomSendQueueUpdate, buffer_of_values_for_local_events: &mut LatestEventValuesForLocalEvents, room_event_cache: &RoomEventCache, + current_value_event_id: Option, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) -> Option { use crate::send_queue::{LocalEcho, LocalEchoContent}; - Some(match send_queue_update { + match send_queue_update { // A new local event is being sent. // // Let's create the `LatestEventValue` and push it in the buffer of values. @@ -691,7 +705,7 @@ impl LatestEventValueBuilder { content: local_echo_content, }) => match local_echo_content { LocalEchoContent::Event { serialized_event: serialized_event_content, .. } => { - match serialized_event_content.deserialize() { + Some(match serialized_event_content.deserialize() { Ok(content) => { if filter_any_message_like_event_content(content, None) { let local_value = LocalLatestEventValue { @@ -728,10 +742,10 @@ impl LatestEventValueBuilder { return None; } - } + }) } - LocalEchoContent::React { .. } => return None, + LocalEchoContent::React { .. } => None, }, // A local event has been cancelled before being sent. @@ -746,6 +760,7 @@ impl LatestEventValueBuilder { Self::new_local_or_remote( buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -785,6 +800,7 @@ impl LatestEventValueBuilder { Self::new_local_or_remote( buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -826,6 +842,7 @@ impl LatestEventValueBuilder { Self::new_local_or_remote( buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -842,6 +859,7 @@ impl LatestEventValueBuilder { Self::new_local_or_remote( buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -858,6 +876,7 @@ impl LatestEventValueBuilder { Self::new_local_or_remote( buffer_of_values_for_local_events, room_event_cache, + current_value_event_id, own_user_id, power_levels, ) @@ -867,8 +886,8 @@ impl LatestEventValueBuilder { // A media upload has made progress. // // Nothing to do here. - RoomSendQueueUpdate::MediaUpload { .. } => return None, - }) + RoomSendQueueUpdate::MediaUpload { .. } => None, + } } /// Get the last [`LatestEventValue`] from the local latest event values if @@ -880,13 +899,15 @@ impl LatestEventValueBuilder { async fn new_local_or_remote( buffer_of_values_for_local_events: &mut LatestEventValuesForLocalEvents, room_event_cache: &RoomEventCache, + current_value_event_id: Option, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, - ) -> LatestEventValue { + ) -> Option { if let Some((_, value)) = buffer_of_values_for_local_events.last() { - value.clone() + Some(value.clone()) } else { - Self::new_remote(room_event_cache, own_user_id, power_levels).await + Self::new_remote(room_event_cache, current_value_event_id, own_user_id, power_levels) + .await } } } @@ -1909,10 +1930,12 @@ mod tests_latest_event_value_builder { }; macro_rules! assert_remote_value_matches_room_message_with_body { - ( $latest_event_value:expr => with body = $body:expr ) => { + ( $latest_event_value:expr => with body = $body:expr ) => {{ + let latest_event_value = $latest_event_value; + assert_matches!( - $latest_event_value, - LatestEventValue::Remote(RemoteLatestEventValue { kind: TimelineEventKind::PlainText { event }, .. }) => { + latest_event_value, + Some(LatestEventValue::Remote(RemoteLatestEventValue { kind: TimelineEventKind::PlainText { event: ref event }, .. })) => { assert_matches!( event.deserialize().unwrap(), AnySyncTimelineEvent::MessageLike( @@ -1923,33 +1946,41 @@ mod tests_latest_event_value_builder { assert_eq!(message_content.content.body(), $body); } ); + + latest_event_value.unwrap() } ); - }; + }}; } macro_rules! assert_local_value_matches_room_message_with_body { - ( $latest_event_value:expr, $pattern:path => with body = $body:expr ) => { + ( $latest_event_value:expr, $pattern:path => with body = $body:expr ) => {{ + let latest_event_value = $latest_event_value; + assert_matches!( - $latest_event_value, - Some( $pattern (local_event)) => { + latest_event_value, + Some( $pattern (ref local_event)) => { assert_matches!( local_event.content.deserialize().unwrap(), AnyMessageLikeEventContent::RoomMessage(message_content) => { assert_eq!(message_content.body(), $body); } ); + + latest_event_value.unwrap() } - ); - }; + ) + }}; ( $latest_event_value:expr, $pattern:path { $local_value:ident with body = $body:expr $( , $field:ident => $more:block )* - } ) => { + } ) => {{ + let latest_event_value = $latest_event_value; + assert_matches!( - $latest_event_value, - Some( $pattern { $local_value, $( $field, )* .. }) => { + latest_event_value, + Some( $pattern { ref $local_value, $( ref $field, )* .. }) => { assert_matches!( $local_value .content.deserialize().unwrap(), AnyMessageLikeEventContent::RoomMessage(message_content) => { @@ -1961,9 +1992,11 @@ mod tests_latest_event_value_builder { })* } ); + + latest_event_value.unwrap() } - ); - }; + ) + }}; } #[async_test] @@ -2028,7 +2061,7 @@ mod tests_latest_event_value_builder { // We get `event_id_1` because `event_id_2` isn't a candidate, // and `event_id_0` hasn't been read yet (because events are read // backwards). - LatestEventValueBuilder::new_remote(&room_event_cache, user_id, None).await => with body = "world" + LatestEventValueBuilder::new_remote(&room_event_cache, None, user_id, None).await => with body = "world" ); } @@ -2078,7 +2111,7 @@ mod tests_latest_event_value_builder { let mut buffer = LatestEventValuesForLocalEvents::new(); // Receiving one `NewLocalEvent`. - { + let previous_value = { let transaction_id = OwnedTransactionId::from("txnid0"); let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); @@ -2086,10 +2119,10 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` matches the new local event. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, None, user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" - ); - } + ) + }; // Receiving another `NewLocalEvent`, ensuring it's pushed back in the buffer. { @@ -2100,7 +2133,7 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` matches the new local event. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B" ); } @@ -2117,7 +2150,7 @@ mod tests_latest_event_value_builder { let mut buffer = LatestEventValuesForLocalEvents::new(); // Receiving one `NewLocalEvent`. - let transaction_id_0 = { + let (transaction_id_0, previous_value) = { let transaction_id = OwnedTransactionId::from("txnid0"); let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); @@ -2127,17 +2160,17 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, None, user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" ); - transaction_id + (transaction_id, value) }; // Receiving a `SendError` targeting the first event. The // `LatestEventValue` must change to indicate it “cannot be sent”. - { + let previous_value = { let update = RoomSendQueueUpdate::SendError { transaction_id: transaction_id_0.clone(), error: Arc::new(Error::UnknownError("oopsy".to_owned().into())), @@ -2146,14 +2179,16 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but it's marked as “cannot be sent”. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalCannotBeSent => with body = "A" ); assert_eq!(buffer.buffer.len(), 1); assert_matches!(&buffer.buffer[0].1, LatestEventValue::LocalCannotBeSent(_)); - } + + value + }; // Receiving another `NewLocalEvent`, ensuring it's pushed back in the buffer, // and as a `LocalCannotBeSent` because the previous value is itself @@ -2166,7 +2201,7 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` matches the new local event. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalCannotBeSent => with body = "B" ); } @@ -2184,7 +2219,7 @@ mod tests_latest_event_value_builder { let mut buffer = LatestEventValuesForLocalEvents::new(); // Receiving one `NewLocalEvent` with content of kind event. - let transaction_id_0 = { + let (transaction_id_0, previous_value) = { let transaction_id = OwnedTransactionId::from("txnid0"); let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); @@ -2194,12 +2229,12 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, None, user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" ); - transaction_id + (transaction_id, value) }; // Receiving one `NewLocalEvent` with content of kind react! This time, it is @@ -2223,12 +2258,13 @@ mod tests_latest_event_value_builder { &update, &mut buffer, &room_event_cache, + previous_value.event_id(), user_id, None ) .await, None - ); + ) } assert_eq!(buffer.buffer.len(), 1); @@ -2245,7 +2281,9 @@ mod tests_latest_event_value_builder { let transaction_id_2 = OwnedTransactionId::from("txnid2"); // Receiving three `NewLocalEvent`s. - { + let previous_value = { + let mut value = None; + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B"), (&transaction_id_2, "C")] { @@ -2257,48 +2295,54 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + value = Some(assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, value.and_then(|value: LatestEventValue| value.event_id()), user_id, None).await, LatestEventValue::LocalIsSending => with body = body - ); + )); } assert_eq!(buffer.buffer.len(), 3); - } + + value.unwrap() + }; // Receiving a `CancelledLocalEvent` targeting the second event. The // `LatestEventValue` must not change. - { + let previous_value = { let update = RoomSendQueueUpdate::CancelledLocalEvent { transaction_id: transaction_id_1.clone(), }; // The `LatestEventValue` hasn't changed, it still matches the latest local // event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "C" ); assert_eq!(buffer.buffer.len(), 2); - } + + value + }; // Receiving a `CancelledLocalEvent` targeting the second (so the last) event. // The `LatestEventValue` must point to the first local event. - { + let previous_value = { let update = RoomSendQueueUpdate::CancelledLocalEvent { transaction_id: transaction_id_2.clone(), }; // The `LatestEventValue` has changed, it matches the previous (so the first) // local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" ); assert_eq!(buffer.buffer.len(), 1); - } + + value + }; // Receiving a `CancelledLocalEvent` targeting the first (so the last) event. // The `LatestEventValue` cannot be computed from the send queue and will @@ -2314,6 +2358,7 @@ mod tests_latest_event_value_builder { &update, &mut buffer, &room_event_cache, + previous_value.event_id(), user_id, None ) @@ -2335,7 +2380,9 @@ mod tests_latest_event_value_builder { let transaction_id_1 = OwnedTransactionId::from("txnid1"); // Receiving two `NewLocalEvent`s. - { + let previous_value = { + let mut value = None; + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { let content = new_local_echo_content(&room_send_queue, transaction_id, body); @@ -2345,18 +2392,20 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + value = Some(assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, value.and_then(|value: LatestEventValue| value.event_id()), user_id, None).await, LatestEventValue::LocalIsSending => with body = body - ); + )); } assert_eq!(buffer.buffer.len(), 2); - } + + value.unwrap() + }; // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` // must not change. - { + let previous_value = { let update = RoomSendQueueUpdate::SentEvent { transaction_id: transaction_id_0.clone(), event_id: event_id!("$ev0").to_owned(), @@ -2364,27 +2413,29 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` hasn't changed, it still matches the latest local // event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B" ); assert_eq!(buffer.buffer.len(), 1); - } + + value + }; // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` // hasn't changed, this is still this event, but the status has changed to // `LocalHasBeenSent`. { - let expected_event_id = event_id!("$ev1").to_owned(); + let expected_event_id = event_id!("$ev1"); let update = RoomSendQueueUpdate::SentEvent { transaction_id: transaction_id_1, - event_id: expected_event_id.clone(), + event_id: expected_event_id.to_owned(), }; // The `LatestEventValue` hasn't changed. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalHasBeenSent { value with body = "B", event_id => { @@ -2407,7 +2458,9 @@ mod tests_latest_event_value_builder { let transaction_id_1 = OwnedTransactionId::from("txnid1"); // Receiving two `NewLocalEvent`s. - { + let previous_value = { + let mut value = None; + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { let content = new_local_echo_content(&room_send_queue, transaction_id, body); @@ -2417,18 +2470,20 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + value = Some(assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, value.and_then(|value: LatestEventValue| value.event_id()), user_id, None).await, LatestEventValue::LocalIsSending => with body = body - ); + )); } assert_eq!(buffer.buffer.len(), 2); - } + + value.unwrap() + }; // Receiving a `ReplacedLocalEvent` targeting the first event. The // `LatestEventValue` must not change. - { + let previous_value = { let transaction_id = &transaction_id_0; let LocalEchoContent::Event { serialized_event: new_content, .. } = new_local_echo_content(&room_send_queue, transaction_id, "A.") @@ -2443,13 +2498,15 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` hasn't changed, it still matches the latest local // event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B" ); assert_eq!(buffer.buffer.len(), 2); - } + + value + }; // Receiving a `ReplacedLocalEvent` targeting the second (so the last) event. // The `LatestEventValue` is changing. @@ -2469,7 +2526,7 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but with its new content. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B." ); @@ -2486,7 +2543,7 @@ mod tests_latest_event_value_builder { let transaction_id = OwnedTransactionId::from("txnid0"); // Receiving one `NewLocalEvent`. - { + let previous_value = { let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { @@ -2495,13 +2552,15 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, None, user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" ); assert_eq!(buffer.buffer.len(), 1); - } + + value + }; // Receiving a `ReplacedLocalEvent` targeting the first event. Sadly, the new // event cannot be mapped to a `LatestEventValue`! The first event is removed @@ -2527,6 +2586,7 @@ mod tests_latest_event_value_builder { &update, &mut buffer, &room_event_cache, + previous_value.event_id(), user_id, None ) @@ -2548,7 +2608,9 @@ mod tests_latest_event_value_builder { let transaction_id_1 = OwnedTransactionId::from("txnid1"); // Receiving two `NewLocalEvent`s. - { + let previous_value = { + let mut value = None; + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { let content = new_local_echo_content(&room_send_queue, transaction_id, body); @@ -2558,18 +2620,20 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + value = Some(assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, value.and_then(|value: LatestEventValue| value.event_id()), user_id, None).await, LatestEventValue::LocalIsSending => with body = body - ); + )); } assert_eq!(buffer.buffer.len(), 2); - } + + value.unwrap() + }; // Receiving a `SendError` targeting the first event. The // `LatestEventValue` must change to indicate it's “cannot be sent”. - { + let previous_value = { let update = RoomSendQueueUpdate::SendError { transaction_id: transaction_id_0.clone(), error: Arc::new(Error::UnknownError("oopsy".to_owned().into())), @@ -2578,15 +2642,17 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but it's marked as “cannot be sent”. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalCannotBeSent => with body = "B" ); assert_eq!(buffer.buffer.len(), 2); assert_matches!(&buffer.buffer[0].1, LatestEventValue::LocalCannotBeSent(_)); assert_matches!(&buffer.buffer[1].1, LatestEventValue::LocalCannotBeSent(_)); - } + + value + }; // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` // must change: since an event has been sent, the following events are now @@ -2600,7 +2666,7 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but it's “is sending”. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B" ); @@ -2619,7 +2685,9 @@ mod tests_latest_event_value_builder { let transaction_id_1 = OwnedTransactionId::from("txnid1"); // Receiving two `NewLocalEvent`s. - { + let previous_value = { + let mut value = None; + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { let content = new_local_echo_content(&room_send_queue, transaction_id, body); @@ -2629,18 +2697,20 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + value = Some(assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, value.and_then(|value: LatestEventValue| value.event_id()), user_id, None).await, LatestEventValue::LocalIsSending => with body = body - ); + )); } assert_eq!(buffer.buffer.len(), 2); - } + + value.unwrap() + }; // Receiving a `SendError` targeting the first event. The // `LatestEventValue` must change to indicate it's “cannot be sent”. - { + let previous_value = { let update = RoomSendQueueUpdate::SendError { transaction_id: transaction_id_0.clone(), error: Arc::new(Error::UnknownError("oopsy".to_owned().into())), @@ -2649,15 +2719,17 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but it's marked as “cannot be sent”. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalCannotBeSent => with body = "B" ); assert_eq!(buffer.buffer.len(), 2); assert_matches!(&buffer.buffer[0].1, LatestEventValue::LocalCannotBeSent(_)); assert_matches!(&buffer.buffer[1].1, LatestEventValue::LocalCannotBeSent(_)); - } + + value + }; // Receiving a `RetryEvent` targeting the first event. The `LatestEventValue` // must change: this local event and its following must be “is sending”. @@ -2668,7 +2740,7 @@ mod tests_latest_event_value_builder { // The `LatestEventValue` has changed, it still matches the latest local // event but it's “is sending”. assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, previous_value.event_id(), user_id, None).await, LatestEventValue::LocalIsSending => with body = "B" ); @@ -2687,7 +2759,7 @@ mod tests_latest_event_value_builder { let transaction_id = OwnedTransactionId::from("txnid"); // Receiving a `NewLocalEvent`. - { + let previous_value = { let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { @@ -2696,13 +2768,15 @@ mod tests_latest_event_value_builder { }); // The `LatestEventValue` matches the new local event. - assert_local_value_matches_room_message_with_body!( - LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, user_id, None).await, + let value = assert_local_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_local(&update, &mut buffer, &room_event_cache, None, user_id, None).await, LatestEventValue::LocalIsSending => with body = "A" ); assert_eq!(buffer.buffer.len(), 1); - } + + value + }; // Receiving a `MediaUpload` targeting the first event. The // `LatestEventValue` must not change as `MediaUpload` are ignored. @@ -2721,6 +2795,7 @@ mod tests_latest_event_value_builder { &update, &mut buffer, &room_event_cache, + previous_value.event_id(), user_id, None ) @@ -2794,11 +2869,11 @@ mod tests_latest_event_value_builder { }, &mut buffer, &room_event_cache, + None, user_id, None, ) .await - .unwrap() => with body = "hello" ); } From ebe00841fd500d1e9eadc34a7a7768e6b43cea82 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 10:45:18 +0100 Subject: [PATCH 10/14] refactor(sdk): Replace `bool` by `ControlFlow`. This patch replaces the `bool`s returned by `filter_*` functions by `ControlFlow`s. --- .../src/latest_events/latest_event.rs | 121 +++++++++++++----- 1 file changed, 87 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 5ba2867b8..eae297a49 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -14,7 +14,7 @@ use std::{ iter::once, - ops::{Deref, DerefMut, Not}, + ops::{ControlFlow, Deref, DerefMut, Not}, }; use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber}; @@ -669,18 +669,29 @@ impl LatestEventValueBuilder { own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) -> Option { - let must_overwrite_existing = true; + let mut current_value_must_be_erased = true; if let Ok(Some(event)) = room_event_cache .rfind_map_event_in_memory_by(|event, previous_event| { - filter_timeline_event(event, previous_event, own_user_id, power_levels) - .then(|| event.clone()) + match filter_timeline_event(event, previous_event, own_user_id, power_levels) { + // Let's continue, event is not suitable. + ControlFlow::Continue(FilterContinue { + current_value_must_be_erased: erased, + }) => { + current_value_must_be_erased = erased; + + None + } + + // Stop! We found a suitable event! + ControlFlow::Break(()) => Some(event.clone()), + } }) .await { Some(LatestEventValue::Remote(event)) } else { - must_overwrite_existing.then(|| LatestEventValue::default()) + current_value_must_be_erased.then(|| LatestEventValue::default()) } } @@ -707,7 +718,7 @@ impl LatestEventValueBuilder { LocalEchoContent::Event { serialized_event: serialized_event_content, .. } => { Some(match serialized_event_content.deserialize() { Ok(content) => { - if filter_any_message_like_event_content(content, None) { + if filter_any_message_like_event_content(content, None).is_break() { let local_value = LocalLatestEventValue { timestamp: MilliSecondsSinceUnixEpoch::now(), content: serialized_event_content.clone(), @@ -818,7 +829,7 @@ impl LatestEventValueBuilder { if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { match new_serialized_event_content.deserialize() { Ok(content) => { - if filter_any_message_like_event_content(content, None) { + if filter_any_message_like_event_content(content, None).is_break() { buffer_of_values_for_local_events.replace_content( position, new_serialized_event_content.clone(), @@ -1088,12 +1099,35 @@ impl LatestEventValuesForLocalEvents { } } +/// The [`ControlFlow::Continue`] value used by the filters. +#[derive(Debug)] +struct FilterContinue { + /// Whether the current [`LatestEventValue`] must be erased or not. + current_value_must_be_erased: bool, +} + +impl FilterContinue { + fn new() -> Self { + Self { current_value_must_be_erased: true } + } +} + +/// Build the [`ControlFlow::Break`] for the filters. +fn filter_break() -> ControlFlow<(), FilterContinue> { + ControlFlow::Break(()) +} + +/// Build the [`ControlFlow::Continue`] for the filters. +fn filter_continue() -> ControlFlow<(), FilterContinue> { + ControlFlow::Continue(FilterContinue::new()) +} + fn filter_timeline_event( event: &TimelineEvent, previous_event: Option<&TimelineEvent>, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, -) -> bool { +) -> ControlFlow<(), FilterContinue> { // Cast the event into an `AnySyncTimelineEvent`. If deserializing fails, we // ignore the event. let event = match event.raw().deserialize() { @@ -1104,7 +1138,7 @@ fn filter_timeline_event( "Failed to deserialize the event when looking for a suitable latest event" ); - return false; + return filter_continue(); } }; @@ -1117,7 +1151,7 @@ fn filter_timeline_event( ), // The event has been redacted. - None => false, + None => filter_continue(), } } @@ -1130,7 +1164,7 @@ fn filter_timeline_event( fn filter_any_message_like_event_content( event: AnyMessageLikeEventContent, previous_event: Option<&TimelineEvent>, -) -> bool { +) -> ControlFlow<(), FilterContinue> { match event { // `m.room.message` AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent { @@ -1140,7 +1174,7 @@ fn filter_any_message_like_event_content( }) => { // Don't show incoming verification requests. if let MessageType::VerificationRequest(_) = msgtype { - return false; + return filter_continue(); } // Not all relations are accepted. Let's filter them. @@ -1148,10 +1182,14 @@ fn filter_any_message_like_event_content( Some(Relation::Replacement(Replacement { event_id, .. })) => { // If the edit relates to the immediate previous event, this is an acceptable // latest event candidate, otherwise let's ignore it. - Some(event_id) == previous_event.and_then(|event| event.event_id()) + if Some(event_id) == previous_event.and_then(|event| event.event_id()) { + filter_break() + } else { + filter_continue() + } } - _ => true, + _ => filter_break(), } } @@ -1162,18 +1200,18 @@ fn filter_any_message_like_event_content( AnyMessageLikeEventContent::UnstablePollStart(_) | AnyMessageLikeEventContent::CallInvite(_) | AnyMessageLikeEventContent::RtcNotification(_) - | AnyMessageLikeEventContent::Sticker(_) => true, + | AnyMessageLikeEventContent::Sticker(_) => filter_break(), // `m.room.redaction` // `m.room.encrypted` AnyMessageLikeEventContent::RoomRedaction(_) | AnyMessageLikeEventContent::RoomEncrypted(_) => { // These events are **explicitly** not suitable. - false + filter_continue() } // Everything else is considered not suitable. - _ => false, + _ => filter_continue(), } } @@ -1181,7 +1219,7 @@ fn filter_any_sync_state_event( event: AnySyncStateEvent, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, -) -> bool { +) -> ControlFlow<(), FilterContinue> { match event { AnySyncStateEvent::RoomMember(member) => { match member.membership() { @@ -1200,10 +1238,14 @@ fn filter_any_sync_state_event( if can_accept_or_decline_knocks { // We can only decide whether the user can accept or decline knocks if the // event isn't redacted. - return matches!(member, SyncStateEvent::Original(_)); + return if matches!(member, SyncStateEvent::Original(_)) { + filter_break() + } else { + filter_continue() + }; } - false + filter_continue() } MembershipState::Invite => { @@ -1211,23 +1253,28 @@ fn filter_any_sync_state_event( match member { // We can only decide whether the user is invited if the event isn't // redacted. - SyncStateEvent::Original(state) => state.state_key.deref() == own_user_id, + SyncStateEvent::Original(state) => { + if state.state_key.deref() == own_user_id { + filter_break() + } else { + filter_continue() + } + } - _ => false, + _ => filter_continue(), } } - _ => false, + _ => filter_continue(), } } - _ => false, + _ => filter_continue(), } } #[cfg(test)] mod tests_latest_event_content { - use std::ops::Not; use matrix_sdk_test::event_factory::EventFactory; use ruma::{ @@ -1257,7 +1304,7 @@ mod tests_latest_event_content { $event_builder }; - assert_eq!(filter_timeline_event(&event, None, user_id!("@mnt_io:matrix.org"), None), $expect ); + assert_eq!(filter_timeline_event(&event, None, user_id!("@mnt_io:matrix.org"), None).is_break(), $expect ); }; } @@ -1287,7 +1334,7 @@ mod tests_latest_event_content { { let previous_event = None; - assert!(filter_timeline_event(&event, previous_event, user_id, None).not()); + assert!(filter_timeline_event(&event, previous_event, user_id, None).is_continue()); } // With a previous event, but not the one being replaced. @@ -1295,7 +1342,9 @@ mod tests_latest_event_content { let previous_event = Some(event_factory.text_msg("no!").event_id(event_id!("$ev1")).into_event()); - assert!(filter_timeline_event(&event, previous_event.as_ref(), user_id, None).not()); + assert!( + filter_timeline_event(&event, previous_event.as_ref(), user_id, None).is_continue() + ); } // With a previous event, and that's the one being replaced! @@ -1303,7 +1352,9 @@ mod tests_latest_event_content { let previous_event = Some(event_factory.text_msg("hello").event_id(event_id!("$ev0")).into_event()); - assert!(filter_timeline_event(&event, previous_event.as_ref(), user_id, None)); + assert!( + filter_timeline_event(&event, previous_event.as_ref(), user_id, None).is_break() + ); } } @@ -1475,7 +1526,8 @@ mod tests_latest_event_content { room_power_levels.invite = 10.into(); room_power_levels.kick = 10.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).not(), + filter_timeline_event(&event, None, user_id, Some(&room_power_levels)) + .is_continue(), "cannot accept, cannot decline", ); } @@ -1485,7 +1537,7 @@ mod tests_latest_event_content { room_power_levels.invite = 0.into(); room_power_levels.kick = 10.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)), + filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), "can accept, cannot decline", ); } @@ -1495,7 +1547,7 @@ mod tests_latest_event_content { room_power_levels.invite = 10.into(); room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)), + filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), "cannot accept, can decline", ); } @@ -1505,7 +1557,7 @@ mod tests_latest_event_content { room_power_levels.invite = 0.into(); room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)), + filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), "can accept, can decline", ); } @@ -1521,7 +1573,8 @@ mod tests_latest_event_content { room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).not(), + filter_timeline_event(&event, None, user_id, Some(&room_power_levels)) + .is_continue(), "cannot accept, can decline, at least same user levels", ); } From ca64af13902a0efcc406f1aba0c60e913891a907 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 11:18:55 +0100 Subject: [PATCH 11/14] feat(sdk): When a `m.room.redaction` targets the current [`LatestEventValue`], it must be erased. This patch implements a new feature: when a `m.room.redaction` targets the current [`LatestEventValue`], this one must be erased by the new computed `LatestEventValue`. --- .../src/latest_events/latest_event.rs | 171 ++++++++++++++---- 1 file changed, 138 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index eae297a49..667a39c40 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -34,6 +34,7 @@ use ruma::{ member::MembershipState, message::{MessageType, Relation, RoomMessageEventContent}, power_levels::RoomPowerLevels, + redaction::RoomRedactionEventContent, }, }, }; @@ -669,11 +670,17 @@ impl LatestEventValueBuilder { own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) -> Option { - let mut current_value_must_be_erased = true; + let mut current_value_must_be_erased = false; if let Ok(Some(event)) = room_event_cache .rfind_map_event_in_memory_by(|event, previous_event| { - match filter_timeline_event(event, previous_event, own_user_id, power_levels) { + match filter_timeline_event( + event, + previous_event, + current_value_event_id.as_ref(), + own_user_id, + power_levels, + ) { // Let's continue, event is not suitable. ControlFlow::Continue(FilterContinue { current_value_must_be_erased: erased, @@ -718,7 +725,13 @@ impl LatestEventValueBuilder { LocalEchoContent::Event { serialized_event: serialized_event_content, .. } => { Some(match serialized_event_content.deserialize() { Ok(content) => { - if filter_any_message_like_event_content(content, None).is_break() { + if filter_any_message_like_event_content( + content, + None, + current_value_event_id.as_ref(), + ) + .is_break() + { let local_value = LocalLatestEventValue { timestamp: MilliSecondsSinceUnixEpoch::now(), content: serialized_event_content.clone(), @@ -764,9 +777,18 @@ impl LatestEventValueBuilder { // Remove the calculated `LatestEventValue` from the buffer of values, and return the // last `LatestEventValue` or calculate a new one. RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => { - if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { + let or = if let Some(position) = + buffer_of_values_for_local_events.position(transaction_id) + { buffer_of_values_for_local_events.remove(position); - } + + // We have cancelled a local value. If there is no more local values, and if + // there is no candidate for the Event Cache, we must generate some value, here + // `LatestEventValue::None`, to erase any existing value. + Some(LatestEventValue::None) + } else { + None + }; Self::new_local_or_remote( buffer_of_values_for_local_events, @@ -776,6 +798,7 @@ impl LatestEventValueBuilder { power_levels, ) .await + .or(or) } // A local event has successfully been sent! @@ -829,7 +852,13 @@ impl LatestEventValueBuilder { if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { match new_serialized_event_content.deserialize() { Ok(content) => { - if filter_any_message_like_event_content(content, None).is_break() { + if filter_any_message_like_event_content( + content, + None, + current_value_event_id.as_ref(), + ) + .is_break() + { buffer_of_values_for_local_events.replace_content( position, new_serialized_event_content.clone(), @@ -1106,12 +1135,6 @@ struct FilterContinue { current_value_must_be_erased: bool, } -impl FilterContinue { - fn new() -> Self { - Self { current_value_must_be_erased: true } - } -} - /// Build the [`ControlFlow::Break`] for the filters. fn filter_break() -> ControlFlow<(), FilterContinue> { ControlFlow::Break(()) @@ -1119,12 +1142,28 @@ fn filter_break() -> ControlFlow<(), FilterContinue> { /// Build the [`ControlFlow::Continue`] for the filters. fn filter_continue() -> ControlFlow<(), FilterContinue> { - ControlFlow::Continue(FilterContinue::new()) + ControlFlow::Continue(FilterContinue { current_value_must_be_erased: false }) } +/// Build the [`ControlFlow::Continue`] with erasing, for the filters. +fn filter_continue_with_erasing() -> ControlFlow<(), FilterContinue> { + ControlFlow::Continue(FilterContinue { current_value_must_be_erased: true }) +} + +/// Filter a [`TimelineEvent`]. +/// +/// Be careful: +/// +/// - `event` is the current event in the collection of events that is scanned. +/// - `previous_event` is the event sitting next to `event` in this collection, +/// it's the event that comes before `event` (`previous_event` is older than +/// `event`). +/// - `current_value_event_id` is the event ID of the current +/// [`LatestEventValue`]. fn filter_timeline_event( event: &TimelineEvent, previous_event: Option<&TimelineEvent>, + current_value_event_id: Option<&OwnedEventId>, own_user_id: &UserId, power_levels: Option<&RoomPowerLevels>, ) -> ControlFlow<(), FilterContinue> { @@ -1148,6 +1187,7 @@ fn filter_timeline_event( Some(any_message_like_event_content) => filter_any_message_like_event_content( any_message_like_event_content, previous_event, + current_value_event_id, ), // The event has been redacted. @@ -1164,6 +1204,7 @@ fn filter_timeline_event( fn filter_any_message_like_event_content( event: AnyMessageLikeEventContent, previous_event: Option<&TimelineEvent>, + current_value_event_id: Option<&OwnedEventId>, ) -> ControlFlow<(), FilterContinue> { match event { // `m.room.message` @@ -1203,10 +1244,23 @@ fn filter_any_message_like_event_content( | AnyMessageLikeEventContent::Sticker(_) => filter_break(), // `m.room.redaction` + AnyMessageLikeEventContent::RoomRedaction(RoomRedactionEventContent { + redacts, .. + }) => { + // A redaction is not suitable. + // + // However, this redaction targets the current `LatestEventValue`! It means the + // current value must be erased. + if redacts.as_ref() == current_value_event_id { + filter_continue_with_erasing() + } else { + filter_continue() + } + } + // `m.room.encrypted` - AnyMessageLikeEventContent::RoomRedaction(_) - | AnyMessageLikeEventContent::RoomEncrypted(_) => { - // These events are **explicitly** not suitable. + AnyMessageLikeEventContent::RoomEncrypted(_) => { + // **explicitly** not suitable. filter_continue() } @@ -1275,7 +1329,9 @@ fn filter_any_sync_state_event( #[cfg(test)] mod tests_latest_event_content { + use std::ops::Not; + use assert_matches::assert_matches; use matrix_sdk_test::event_factory::EventFactory; use ruma::{ event_id, @@ -1283,20 +1339,20 @@ mod tests_latest_event_content { owned_user_id, user_id, }; - use super::filter_timeline_event; + use super::{ControlFlow, FilterContinue, filter_timeline_event}; macro_rules! assert_latest_event_content { ( event | $event_factory:ident | $event_builder:block is a candidate ) => { - assert_latest_event_content!(@_ | $event_factory | $event_builder, true); + assert_latest_event_content!(@_ | $event_factory | $event_builder, ControlFlow::Break(_)); }; ( event | $event_factory:ident | $event_builder:block is not a candidate ) => { - assert_latest_event_content!(@_ | $event_factory | $event_builder, false); + assert_latest_event_content!(@_ | $event_factory | $event_builder, ControlFlow::Continue(_)); }; - ( @_ | $event_factory:ident | $event_builder:block, $expect:literal ) => { + ( @_ | $event_factory:ident | $event_builder:block, $expect:pat) => { let user_id = user_id!("@mnt_io:matrix.org"); let event_factory = EventFactory::new().sender(user_id); let event = { @@ -1304,7 +1360,10 @@ mod tests_latest_event_content { $event_builder }; - assert_eq!(filter_timeline_event(&event, None, user_id!("@mnt_io:matrix.org"), None).is_break(), $expect ); + assert_matches!( + filter_timeline_event(&event, None, None, user_id!("@mnt_io:matrix.org"), None), + $expect + ); }; } @@ -1334,7 +1393,9 @@ mod tests_latest_event_content { { let previous_event = None; - assert!(filter_timeline_event(&event, previous_event, user_id, None).is_continue()); + assert!( + filter_timeline_event(&event, previous_event, None, user_id, None).is_continue() + ); } // With a previous event, but not the one being replaced. @@ -1343,7 +1404,8 @@ mod tests_latest_event_content { Some(event_factory.text_msg("no!").event_id(event_id!("$ev1")).into_event()); assert!( - filter_timeline_event(&event, previous_event.as_ref(), user_id, None).is_continue() + filter_timeline_event(&event, previous_event.as_ref(), None, user_id, None) + .is_continue() ); } @@ -1353,17 +1415,57 @@ mod tests_latest_event_content { Some(event_factory.text_msg("hello").event_id(event_id!("$ev0")).into_event()); assert!( - filter_timeline_event(&event, previous_event.as_ref(), user_id, None).is_break() + filter_timeline_event(&event, previous_event.as_ref(), None, user_id, None) + .is_break() ); } } #[test] fn test_redaction() { - assert_latest_event_content!( - event | event_factory | { event_factory.redaction(event_id!("$ev0")).into_event() } - is not a candidate - ); + let user_id = user_id!("@mnt_io:matrix.org"); + let event_factory = EventFactory::new().sender(user_id); + let event_id = event_id!("$ev0"); + let event = event_factory.redaction(event_id).into_event(); + + // `current_value_event_id` is `None`, it cannot be used to decide whether the + // current value must be erased. + { + let current_value_event_id = None; + + assert_matches!( + filter_timeline_event(&event, None, current_value_event_id, user_id, None), + ControlFlow::Continue(FilterContinue { current_value_must_be_erased }) => { + assert!(current_value_must_be_erased.not()); + } + ); + } + + // `current_value_event_id` is `Some(_)`, but the redaction event doesn't target + // this event ID. + { + let current_value_event_id = Some(event_id!("$ev1").to_owned()); + + assert_matches!( + filter_timeline_event(&event, None, current_value_event_id.as_ref(), user_id, None), + ControlFlow::Continue(FilterContinue { current_value_must_be_erased }) => { + assert!(current_value_must_be_erased.not()); + } + ); + } + + // `current_value_event_id` is `Some(_)`, and the redaction event does target + // this event ID: great, the current value must be erased! + { + let current_value_event_id = Some(event_id.to_owned()); + + assert_matches!( + filter_timeline_event(&event, None, current_value_event_id.as_ref(), user_id, None), + ControlFlow::Continue(FilterContinue { current_value_must_be_erased }) => { + assert!(current_value_must_be_erased); + } + ); + } } #[test] @@ -1526,7 +1628,7 @@ mod tests_latest_event_content { room_power_levels.invite = 10.into(); room_power_levels.kick = 10.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)) + filter_timeline_event(&event, None, None, user_id, Some(&room_power_levels)) .is_continue(), "cannot accept, cannot decline", ); @@ -1537,7 +1639,8 @@ mod tests_latest_event_content { room_power_levels.invite = 0.into(); room_power_levels.kick = 10.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), + filter_timeline_event(&event, None, None, user_id, Some(&room_power_levels)) + .is_break(), "can accept, cannot decline", ); } @@ -1547,7 +1650,8 @@ mod tests_latest_event_content { room_power_levels.invite = 10.into(); room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), + filter_timeline_event(&event, None, None, user_id, Some(&room_power_levels)) + .is_break(), "cannot accept, can decline", ); } @@ -1557,7 +1661,8 @@ mod tests_latest_event_content { room_power_levels.invite = 0.into(); room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)).is_break(), + filter_timeline_event(&event, None, None, user_id, Some(&room_power_levels)) + .is_break(), "can accept, can decline", ); } @@ -1573,7 +1678,7 @@ mod tests_latest_event_content { room_power_levels.kick = 0.into(); assert!( - filter_timeline_event(&event, None, user_id, Some(&room_power_levels)) + filter_timeline_event(&event, None, None, user_id, Some(&room_power_levels)) .is_continue(), "cannot accept, can decline, at least same user levels", ); From 5c5dcaa027b6bf79bf30581acaca4c2d3504d893 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 14:20:02 +0100 Subject: [PATCH 12/14] test(sdk): Add tests for `new_remote` and erasable values. --- .../src/latest_events/latest_event.rs | 310 +++++++++++++++++- 1 file changed, 306 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 667a39c40..15b7568dc 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -2065,14 +2065,17 @@ mod tests_latest_event_value_builder { }; use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{ - MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedTransactionId, event_id, + EventId, MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedTransactionId, event_id, events::{ AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent, reaction::ReactionEventContent, relation::Annotation, room::message::RoomMessageEventContent, }, - room_id, user_id, + room_id, + serde::Raw, + user_id, }; + use serde_json::json; use super::{ LatestEventValue, LatestEventValueBuilder, LatestEventValuesForLocalEvents, @@ -2107,7 +2110,7 @@ mod tests_latest_event_value_builder { latest_event_value.unwrap() } - ); + ) }}; } @@ -2157,6 +2160,22 @@ mod tests_latest_event_value_builder { }}; } + fn remote_room_message(event_id: &EventId, body: &str) -> RemoteLatestEventValue { + RemoteLatestEventValue::from_plaintext( + Raw::from_json_string( + json!({ + "content": RoomMessageEventContent::text_plain(body), + "type": "m.room.message", + "event_id": event_id, + "origin_server_ts": 42, + "sender": "@mnt_io:matrix.org", + }) + .to_string(), + ) + .unwrap(), + ) + } + #[async_test] async fn test_remote_is_scanning_event_backwards_from_event_cache() { let room_id = room_id!("!r0"); @@ -2223,6 +2242,289 @@ mod tests_latest_event_value_builder { ); } + #[async_test] + async fn test_remote_without_a_candidate() { + let room_id = room_id!("!r0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + + let room = client.base_client().get_or_create_room(room_id, RoomState::Joined); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + // Check initial state. + let current_value = { + let value = room.latest_event(); + + assert_matches!(value, LatestEventValue::None); + + value + }; + + // Compute a new remote value: not able to find a relevant candidate. + // + // No candidate is found, so it's just `None` here. + assert_matches!( + LatestEventValueBuilder::new_remote( + &room_event_cache, + current_value.event_id(), + user_id, + None, + ) + .await, + None + ); + } + + #[async_test] + async fn test_remote_with_a_candidate() { + let room_id = room_id!("!r0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + + let room = client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Insert a suitable candidate. + client + .event_cache_store() + .lock() + .await + .expect("Could not acquire the event cache lock") + .as_clean() + .expect("Could not acquire a clean event cache lock") + .handle_linked_chunk_updates( + LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![ + event_factory.text_msg("hello").event_id(event_id!("$ev0")).into(), + ], + }, + ], + ) + .await + .unwrap(); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + // Check initial state. + let current_value = { + let value = room.latest_event(); + + assert_matches!(value, LatestEventValue::None); + + value + }; + + // Compute a new remote value: will be able to find a relevant + // candidate. + // + // A candidate is found, so it's a `Some(LatestEventValue::Remote)` + // that is returned! Let's check the event. + assert_remote_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_remote(&room_event_cache, current_value.event_id(), user_id, None).await => with body = "hello" + ); + } + + #[async_test] + async fn test_remote_without_a_candidate_but_with_an_existing_latest_event_value() { + let room_id = room_id!("!r0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Insert a non-suitable candidate. + client + .event_cache_store() + .lock() + .await + .expect("Could not acquire the event cache lock") + .as_clean() + .expect("Could not acquire a clean event cache lock") + .handle_linked_chunk_updates( + LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(1), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(1), 0), + items: vec![event_factory.room_topic("new room topic").into()], + }, + ], + ) + .await + .unwrap(); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + // Initial state. + let current_value = + LatestEventValue::Remote(remote_room_message(event_id!("$ev0"), "hello")); + + // Compute a new remote value: with no candidate. + // + // No candidate is found, so it's just a `None` here. + assert_matches!( + LatestEventValueBuilder::new_remote( + &room_event_cache, + current_value.event_id(), + user_id, + None + ) + .await, + None + ); + } + + #[async_test] + async fn test_remote_without_a_candidate_but_with_an_erasable_existing_latest_event_value() { + let room_id = room_id!("!r0"); + let event_id = event_id!("$ev0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Insert a non-suitable candidate. + client + .event_cache_store() + .lock() + .await + .expect("Could not acquire the event cache lock") + .as_clean() + .expect("Could not acquire a clean event cache lock") + .handle_linked_chunk_updates( + LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(1), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(1), 0), + items: vec![event_factory.redaction(event_id).into()], + }, + ], + ) + .await + .unwrap(); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + // Initial state. + let current_value = LatestEventValue::Remote(remote_room_message(event_id, "hello")); + + // Compute a new remote value: with no candidate, but it's a `m.room.redaction` + // that erases `current_value`! + // + // No candidate is found, so it SHOULD BE a `None`, but since we found a + // `m.room.redaction` targeting our `current_value`, it MUST BE a + // `Some(LatestEventValue::None)` to erase it. + assert_matches!( + LatestEventValueBuilder::new_remote( + &room_event_cache, + current_value.event_id(), + user_id, + None + ) + .await, + Some(LatestEventValue::None) + ); + } + + #[async_test] + async fn test_remote_with_a_candidate_and_an_erasable_existing_latest_event_value() { + let room_id = room_id!("!r0"); + let event_id = event_id!("$ev0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let user_id = client.user_id().unwrap(); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Insert a non-suitable candidate. + client + .event_cache_store() + .lock() + .await + .expect("Could not acquire the event cache lock") + .as_clean() + .expect("Could not acquire a clean event cache lock") + .handle_linked_chunk_updates( + LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(1), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(1), 0), + items: vec![ + event_factory.redaction(event_id).into(), + event_factory.text_msg("world").into(), + ], + }, + ], + ) + .await + .unwrap(); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + // Initial state. + let current_value = LatestEventValue::Remote(remote_room_message(event_id, "hello")); + + // Compute a new remote value: with a candidate, and a `m.room.redaction` + // that erases `current_value`! + // + // A candidate is found, so it MUST BE a + // `Some(LatestEventValue::Remote(_))`, whatever the `current_value` + // is. + assert_remote_value_matches_room_message_with_body!( + LatestEventValueBuilder::new_remote(&room_event_cache, current_value.event_id(), user_id, None).await => with body = "world" + ); + } + async fn local_prelude() -> (Client, OwnedRoomId, RoomSendQueue, RoomEventCache) { let room_id = room_id!("!r0").to_owned(); @@ -2749,7 +3051,7 @@ mod tests_latest_event_value_builder { None ) .await, - Some(LatestEventValue::None) + None ); assert_eq!(buffer.buffer.len(), 0); From bf3cb6ba84c1f6f479eebc4c87f37b86d190a0d1 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 18 Dec 2025 16:03:35 +0100 Subject: [PATCH 13/14] chore(sdk): Make Clippy happy :-). --- crates/matrix-sdk/src/latest_events/latest_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 15b7568dc..d7804f37a 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -698,7 +698,7 @@ impl LatestEventValueBuilder { { Some(LatestEventValue::Remote(event)) } else { - current_value_must_be_erased.then(|| LatestEventValue::default()) + current_value_must_be_erased.then(LatestEventValue::default) } } From c3c367c54c44e707d005b503e1e27100ea3ece8d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Fri, 19 Dec 2025 08:08:48 +0100 Subject: [PATCH 14/14] doc(base): Update `CHANGELOG.md`. --- crates/matrix-sdk-base/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 2ac9014b7..e58156bc3 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -12,6 +12,12 @@ All notable changes to this project will be documented in this file. a local event that has been sent successfully. ([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968)) +### Features + +- The `LatestEventValue::LocalHasBeenSent` variant gains a new `event_id: + OwnedEventId` field. + ([#5977](https://github.com/matrix-org/matrix-rust-sdk/pull/5977)) + ### Refactor - [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now